From 9c7d6f9f98858f00c47e6ebab139b6dfdea41cdd Mon Sep 17 00:00:00 2001 From: Pegor Date: Mon, 22 Dec 2025 15:13:35 -0800 Subject: [PATCH 01/16] CHAD-16364: SLGA Zigbee: Add lockUsers and lockCredentials capabilities --- .../zigbee-lock/profiles/base-lock.yml | 4 + drivers/SmartThings/zigbee-lock/src/init.lua | 337 +--------- .../zigbee-lock/src/lock_utils.lua | 3 +- .../zigbee-lock/src/new_lock_utils.lua | 273 ++++++++ .../test_zigbee_lock_code_slga_migration.lua | 83 +++ .../test_zigbee_lock_new_capabilities.lua | 621 ++++++++++++++++++ .../test_zigbee_yale-fingerprint-lock.lua | 39 +- .../src/using-new-capabilities/init.lua | 617 +++++++++++++++++ .../lock-without-codes/init.lua | 0 .../samsungsds/init.lua | 117 ++++ .../yale-fingerprint-lock/init.lua | 56 ++ .../src/using-new-capabilities/yale/init.lua | 186 ++++++ .../yale/yale-bad-battery-reporter/init.lua | 0 .../src/using-old-capabilities/init.lua | 426 ++++++++++++ .../lock-without-codes/init.lua | 101 +++ .../samsungsds/init.lua | 0 .../yale-fingerprint-lock/init.lua | 0 .../yale/init.lua | 2 +- .../yale/yale-bad-battery-reporter/init.lua | 52 ++ .../zwave-lock/profiles/base-lock-tamper.yml | 4 + .../zwave-lock/profiles/base-lock.yml | 4 + drivers/SmartThings/zwave-lock/src/init.lua | 47 ++ .../test_zwave_lock_code_slga_migration.lua | 156 +++++ 23 files changed, 2813 insertions(+), 315 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-new-capabilities}/lock-without-codes/init.lua (100%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-new-capabilities}/yale/yale-bad-battery-reporter/init.lua (100%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/samsungsds/init.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale-fingerprint-lock/init.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale/init.lua (98%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua diff --git a/drivers/SmartThings/zigbee-lock/profiles/base-lock.yml b/drivers/SmartThings/zigbee-lock/profiles/base-lock.yml index 159e6939f3..c9c98898c6 100644 --- a/drivers/SmartThings/zigbee-lock/profiles/base-lock.yml +++ b/drivers/SmartThings/zigbee-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: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index ce6894b868..4a38d5679a 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -28,6 +28,8 @@ 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 @@ -40,21 +42,16 @@ 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)) +local function lazy_load_if_possible(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + + -- version 9 will include the lazy loading functions + if version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) 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 refresh = function(driver, device, cmd) @@ -71,30 +68,6 @@ local refresh = function(driver, device, cmd) 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)) - - -- 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 alarm_handler = function(driver, device, zb_mess) local ALARM_REPORT = { [0] = Lock.lock.unknown(), @@ -106,201 +79,25 @@ local alarm_handler = function(driver, device, zb_mess) end 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 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 + -- this command should now trigger setting the migrated field and reinjecting the command. + -- this is so we can start using the new capbilities from now on. +local function device_added(driver, device) + if device:supports_capability_by_id(LockCodes.ID) then + device:emit_event(LockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) + if device.device_added ~= nil then + -- make the driver call this command again, it will now be handled in new capabilities. + driver.lifecycle_handlers.device_added(driver, device) 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 + else + lock_utils.populate_state_from_data(device) driver:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.nameSlot.NAME, - args = {command.args.codeSlot, command.args.codeName} + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} }) - 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 function device_added(driver, device) - lock_utils.populate_state_from_data(device) - - driver:inject_capability_command(device, { - capability = capabilities.refresh.ID, - command = capabilities.refresh.commands.refresh.NAME, - args = {} - }) -end - local function init(driver, device) lock_utils.populate_state_from_data(device) -- temp fix before this can be changed to non-persistent @@ -331,69 +128,6 @@ local lock_state_handler = function(driver, device, value, zb_rx) end 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 function lock(driver, device, command) device:send_to_component(command.component, LockCluster.server.commands.LockDoor(device)) end @@ -413,30 +147,14 @@ local zigbee_lock_driver = { [Alarm.ID] = { [Alarm.client.commands.Alarm.ID] = alarm_handler }, - [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.LockState.ID] = lock_state_handler, - [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, - }, [Lock.ID] = { [Lock.commands.lock.NAME] = lock, [Lock.commands.unlock.NAME] = unlock, @@ -446,13 +164,10 @@ local zigbee_lock_driver = { } }, sub_drivers = { - require("samsungsds"), - require("yale"), - require("yale-fingerprint-lock"), - require("lock-without-codes") + lazy_load_if_possible("using-old-capabilities"), + lazy_load_if_possible("using-new-capabilities"), }, lifecycle_handlers = { - doConfigure = do_configure, added = device_added, init = init, }, diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index 0a36a9685e..3cb4903b5a 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -20,11 +20,12 @@ local LockCodes = capabilities.lockCodes local lock_utils = { -- Constants LOCK_CODES = "lockCodes", + LOCK_USERS = "lockUsers", CHECKING_CODE = "checkingCode", CODE_STATE = "codeState", MIGRATION_COMPLETE = "migrationComplete", MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped", - CHECKED_CODE_SUPPORT = "checkedCodeSupport" + CHECKED_CODE_SUPPORT = "checkedCodeSupport", } lock_utils.get_lock_codes = function(device) diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua new file mode 100644 index 0000000000..c34c839d68 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -0,0 +1,273 @@ +-- 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. +local utils = require "st.utils" +local capabilities = require "st.capabilities" +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 = "activeCredential", + STATUS_BUSY = "busy", + STATUS_DUPLICATE = "duplicate", + STATUS_FAILURE = "failure", + STATUS_INVALID_COMMAND = "invalidCommand", + STATUS_OCCUPIED = "occupied", + STATUS_RESOURCE_EXHAUSTED = "resourceExhausted", + STATUS_SUCCESS = "success", + 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, {}) + device:set_field(new_lock_utils.LOCK_USERS, users) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) +end + +new_lock_utils.get_users = function(device) + local users = 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, 0) + 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) + local credentials = 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, 0) + 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) +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(current_users, index) + 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) + 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(credentials, index) + 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) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + return status_code +end + +return new_lock_utils diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua new file mode 100644 index 0000000000..5361029c89 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua @@ -0,0 +1,83 @@ +-- 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. + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local clusters = require "st.zigbee.zcl.clusters" +local PowerConfiguration = clusters.PowerConfiguration +local DoorLock = clusters.DoorLock +local Alarm = clusters.Alarms +local capabilities = require "st.capabilities" + +local json = require "st.json" + +local mock_datastore = require "integration_test.mock_env_datastore" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + data = { + lockCodes = json.encode({ + ["1"] = "Zach", + ["5"] = "Steven" + }), + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init()end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Device called 'migrate' command", + function() + -- test.mock_device.add_test_device(mock_device) + -- -- test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + -- test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + -- test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) + -- test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) + -- test.wait_for_events() + -- -- Validate lockCodes field + -- mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" }) + -- -- Validate migration complete flag + -- mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + + -- -- Set min/max code length attributes + -- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) }) + -- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) }) + -- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) }) + -- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } }))) + -- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } }))) + -- 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 functionality. + -- 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(5, { 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.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.lockUsers.totalUsersSupported(4, { 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() + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua new file mode 100644 index 0000000000..94fe7a2ace --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua @@ -0,0 +1,621 @@ +-- 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. + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local capabilities = require "st.capabilities" + +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType + +local test_credential_index = 1 +local test_credentials = {} +local test_users = {} +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init_new_capabilities() + test_credential_index = 1 + test_credentials = {} + test_users = {} + test.mock_device.add_test_device(mock_device) +end + +local function init_migration() + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report( + mock_device, 4) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.minCodeLength(4, { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report( + mock_device, 8) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.maxCodeLength(8, { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported + :build_test_attr_report(mock_device, 4) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.wait_for_events() + 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(8, { 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({}, { 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.users({}, { 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.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() +end + +local function add_default_users() + local user_list = {} + for i = 1, 4 do + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "Guest" .. i, "guest" } + }, + }) + -- add to the user list that is now expected + table.insert(user_list, {userIndex = i, userType = "guest", userName = "Guest" .. i }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + user_list, + { visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = i}, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +end + +local function add_credential(user_index, credential_data) +test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { user_index, "guest", "pin", credential_data } + }, + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + test_credential_index, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + credential_data + ) + } + ) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, test_credential_index) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + test_credential_index, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + credential_data + ) + } + ) + table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) + 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 } }) + ) + ) + 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.set_test_init_function(test_init_new_capabilities) + +test.register_coroutine_test( + "Add User command received and commandResult is success until totalUsersSupported reached", + function() + -- make sure we have migrated and are using the new capabilities + init_migration() + -- create initial max users + add_default_users() + + -- 5th addUser call - totalUsersSupported is passsed and now commandResult should be resourceExhausted + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "resourceExhausted" }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Update User command reports a commandResult of success unless user index doesn't exist", + function() + -- make sure we have migrated and are using the new capabilities + init_migration() + -- create initial users + add_default_users() + + -- success + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = { "2", "ChangeUserName", "guest" } + }, + }) + + local users = { + { userIndex = 1, userName = "Guest1", userType = "guest" }, + { userIndex = 2, userName = "ChangeUserName", userType = "guest" }, + { userIndex = 3, userName = "Guest3", userType = "guest" }, + { userIndex = 4, userName = "Guest4", userType = "guest" }, + } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users(users, { 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 = 2 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + + -- failure - try updating non existent userIndex + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = { "6", "ChangeUserName", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "failure" }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete User command reports a commandResult of success unless user index doesn't exist", + function() + -- make sure we have migrated and are using the new capabilities + init_migration() + -- create initial users + add_default_users() + + -- success + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "deleteUser", + args = { "3" } + }, + }) + + local users = { + { userIndex = 1, userName = "Guest1", userType = "guest" }, + { userIndex = 2, userName = "Guest2", userType = "guest" }, + { userIndex = 4, userName = "Guest4", userType = "guest" }, + } + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users(users, { 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 = 3 }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + + -- failure - try updating non existent userIndex + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "deleteUser", + args = { "3" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "failure" }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +) + + +test.register_coroutine_test( + "addCredential command received and commandResult is success", + function() + init_migration() + add_credential(0, "abc123") + end +) + +test.register_coroutine_test( + "updateCredential command received and commandResult is success", + function() + init_migration() + add_credential(0, "abc123") + + -- try to update the wrong credentialIndex (4) first and expect a failure + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { "4", "4", "pin", "abc123" } + }, + }) + 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() + + -- try to update the right credential + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { "1", "1", "pin", "changedPin123" } + }, + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "changedPin123" + ) + } + ) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "abc123" + ) + } + ) + 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.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() + end +) + +test.register_coroutine_test( + "deleteCredential command received and commandResult is success", + function() + init_migration() + add_credential(0, "abc123") + add_credential(0, "test123") + add_credential(0, "321test") + + -- try to delete credential with wrong index and expect a failure + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { "4", "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() + + -- try to delete credential with correct index + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { "1", "pin"} + }, + }) + test.socket.zigbee:__expect_send({ + mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) + }) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + "" + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + { userIndex = 2, userType = "guest", userName = "Guest2" }, + { userIndex = 3, userType = "guest", userName = "Guest3" } + }, + { 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" }, + { userIndex = 3, credentialIndex = 3, 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() + end +) + +test.register_coroutine_test( + "deleteAllCredentials command received and commandResult is success", + function() + init_migration() + add_credential(0, "abc123") + add_credential(0, "test123") + add_credential(0, "321test") + + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "deleteAllCredentials", + args = {} + }, + }) + + test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot") + test.socket.zigbee:__expect_send({ + mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) + }) + + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__expect_send({ + mock_device.id,DoorLock.server.commands.GetPINCode(mock_device, 1) + }) + + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + "" + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + { userIndex = 2, userType = "guest", userName = "Guest2" }, + { userIndex = 3, userType = "guest", userName = "Guest3" } + }, + { 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" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" } + }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + 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/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua index 2255c063a3..69a2cb6633 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua @@ -27,7 +27,18 @@ zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.mock_device.add_test_device(mock_device)end -test.set_test_init_function(test_init) +local function test_init_new_capabilities() + test.mock_device.add_test_device(mock_device) + 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(8, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(0, { 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.users({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(0, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) +end test.register_message_test( "Max user code number report should be handled", @@ -43,7 +54,31 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(30)) } - } + }, + {test_init = test_init } +) + +test.register_message_test( + "Max user code number report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, + 16) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(30)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(30)) + } + }, + {test_init = test_init_new_capabilities } ) test.run_registered_tests() 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..89a9cd59ec --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -0,0 +1,617 @@ +-- 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 defaults = require "st.zigbee.defaults" +local device_management = require "st.zigbee.device_management" +local ZigbeeDriver = require "st.zigbee" +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 refresh = function(driver, device, cmd) + device:refresh() + device:send(LockCluster.attributes.LockState:read(device)) + device:send(Alarm.attributes.AlarmCount:read(device)) +end + +local device_added = function(driver, device) + lock_utils.reload_tables(device) + + driver:inject_capability_command(device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) +end + +local init = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(2, 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 + local current_users = lock_utils.get_users(device) + device:emit_event(capabilities.lockUsers.users(current_users, {state_chang = true, visibility = { displayed = true } })) + 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) + device:emit_event(capabilities.lockUsers.users(current_users, { state_change = true, visibility = { displayed = true } })) + 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) + local current_users = lock_utils.get_users(device) + device:emit_event(capabilities.lockUsers.users(current_users, { state_change = true, visibility = { displayed = true } })) + 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 status = lock_utils.STATUS_SUCCESS + 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 + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status) + 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 status = lock_utils.STATUS_SUCCESS + 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 + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status, 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 + device:emit_event(capabilities.lockUsers.users(lock_utils.get_users(device), + { state_change = true, visibility = { displayed = true } })) + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { state_change = true, visibility = { displayed = true } })) + 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 + device:emit_event(capabilities.lockUsers.users(lock_utils.get_users(device), + { state_change = true, visibility = { displayed = true } })) + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { state_change = true, visibility = { displayed = true } })) + 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.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh + }, + + [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING + [capabilities.lockCodes.commands.migrate.NAME] = migrate, + }, + }, + sub_drivers = { + require("using-new-capabilities.samsungsds"), + require("using-new-capabilities.yale-fingerprint-lock"), + require("using-new-capabilities.yale"), + require("using-new-capabilities.lock-without-codes") + }, + health_check = false, + lifecycle_handlers = { + added = device_added, + init = init, + doConfigure = do_configure + }, + can_handle = function(opts, driver, device, ...) + 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 +} + +return new_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua new file mode 100644 index 0000000000..b42f3b0f14 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua @@ -0,0 +1,117 @@ +-- 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. + +local device_management = require "st.zigbee.device_management" +local clusters = require "st.zigbee.zcl.clusters" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local capabilities = require "st.capabilities" +local cluster_base = require "st.zigbee.cluster_base" +local PowerConfiguration = clusters.PowerConfiguration +local DoorLock = clusters.DoorLock +local Lock = capabilities.lock + +local SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND = 0x1F +local SAMSUNG_SDS_MFR_CODE = 0x0003 + +local function handle_lock_state(driver, device, value, zb_rx) + if value.value == DoorLock.attributes.LockState.LOCKED then + device:emit_event(Lock.lock.locked()) + elseif value.value == DoorLock.attributes.LockState.UNLOCKED then + device:emit_event(Lock.lock.unlocked()) + end +end + +local function mfg_lock_door_handler(driver, device, zb_rx) + local cmd = zb_rx.body.zcl_body.body_bytes:byte(1) + if cmd == 0x00 then + device:emit_event(Lock.lock.unlocked()) + end +end + +local function unlock_cmd_handler(driver, device, command) + device:send(cluster_base.build_manufacturer_specific_command( + device, + DoorLock.ID, + SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND, + SAMSUNG_SDS_MFR_CODE, + "\x10\x04\x31\x32\x33\x35")) +end + +local function lock_cmd_handler(driver, device, command) + -- do nothing in lock command handler +end + +local refresh = function(driver, device, cmd) + -- do nothing in refresh capability handler +end + +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +local device_added = function(self, device) + emit_event_if_latest_state_missing(device, "main", capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) + device:emit_event(capabilities.battery.battery(100)) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, DoorLock.ID, self.environment_info.hub_zigbee_eui)) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(DoorLock.attributes.LockState:configure_reporting(device, 0, 3600, 0)) +end + +local battery_init = battery_defaults.build_linear_voltage_init(4.0, 6.0) + +local device_init = function(driver, device, event) + battery_init(driver, device, event) + device:remove_monitored_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) + device:remove_configured_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) +end + +local samsung_sds_driver = { + NAME = "SAMSUNG SDS Lock Driver", + zigbee_handlers = { + cluster = { + [DoorLock.ID] = { + [SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND] = mfg_lock_door_handler + } + }, + attr = { + [DoorLock.ID] = { + [DoorLock.attributes.LockState.ID] = handle_lock_state + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh + }, + [capabilities.lock.ID] = { + [capabilities.lock.commands.unlock.NAME] = unlock_cmd_handler, + [capabilities.lock.commands.lock.NAME] = lock_cmd_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + added = device_added, + init = device_init + }, + can_handle = function(opts, driver, device, ...) + return device:get_manufacturer() == "SAMSUNG SDS" + end +} + +return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua new file mode 100644 index 0000000000..c5e22b43f7 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua @@ -0,0 +1,56 @@ +-- 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. + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local LockCluster = clusters.DoorLock +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers + +local YALE_FINGERPRINT_MAX_CODES = 0x1E + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +local yale_fingerprint_lock_models = function(opts, driver, device) + for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local handle_max_codes = function(driver, device, value) + device:emit_event(LockCredentials.pinUsersSupported(YALE_FINGERPRINT_MAX_CODES)) + device:emit_event(LockUsers.totalUsersSupported(YALE_FINGERPRINT_MAX_CODES)) +end + +local yale_fingerprint_lock_driver = { + NAME = "YALE Fingerprint Lock", + zigbee_handlers = { + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes + } + } + }, + can_handle = yale_fingerprint_lock_models +} + +return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua new file mode 100644 index 0000000000..eb2b4f89e2 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua @@ -0,0 +1,186 @@ +-- 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 Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local LockCluster = clusters.DoorLock + +-- Capabilities +local capabilities = require "st.capabilities" +local LockCredentials = capabilities.lockCredentials +local log = require "log" +local utils = require "st.utils" + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local SHIFT_INDEX_CHECK = 256 +local YALE_MAX_USERS_OVERRIDE = 10 -- yale supports 250 codes... we're not going to iterate through all that. + +local lock_utils = (require "new_lock_utils") + +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 = YALE_MAX_USERS_OVERRIDE + 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 + device:emit_event(capabilities.lockUsers.users(lock_utils.get_users(device), + { state_change = true, visibility = { displayed = true } })) + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { state_change = true, visibility = { displayed = true } })) + 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 credential_index >= SHIFT_INDEX_CHECK then + -- Index is wonky, shift it to get proper value + credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) >> 8 + end + + 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 = false } } + )) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (zb_mess.body.zcl_body.user_id.value == 0xFFFF) then + -- All credentials deleted + local current_credentials = lock_utils.get_credentials(device) + for _, credential in pairs(current_credentials) 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 + device:emit_event(capabilities.lockUsers.users(lock_utils.get_users(device), + { state_change = true, visibility = { displayed = true } })) + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { state_change = true, visibility = { displayed = true } })) + end +end + +local yale_door_lock_driver = { + NAME = "Yale Door Lock", + zigbee_handlers = { + cluster = { + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, + } + } + }, + + sub_drivers = { require("using-new-capabilities.yale.yale-bad-battery-reporter") }, + can_handle = function(opts, driver, device, ...) + return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" + end +} + +return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua 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..a235b469d7 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -0,0 +1,426 @@ +-- 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 defaults = require "st.zigbee.defaults" +local device_management = require "st.zigbee.device_management" +local ZigbeeDriver = require "st.zigbee" + +-- 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.samsungsds"), + require("using-old-capabilities.yale"), + require("using-old-capabilities.yale-fingerprint-lock"), + require("using-old-capabilities.lock-without-codes") + }, + health_check = false, + lifecycle_handlers = { + doConfigure = do_configure + }, + can_handle = 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") + return true, subdriver + end + return false + end +} + +return old_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua new file mode 100644 index 0000000000..7272991459 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua @@ -0,0 +1,101 @@ +-- 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. + +local configurationMap = require "configurations" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local DoorLock = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +local function can_handle_lock_without_codes(opts, driver, device) + for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function device_init(driver, device) + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) + end + end +end + +local function handle_lock(driver, device, cmd) + device:send(DoorLock.commands.LockDoor(device)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) +end + +local function handle_unlock(driver, device, cmd) + device:send(DoorLock.commands.UnlockDoor(device)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) +end + +local function do_refresh(driver, device) + device:refresh() +end + +local function do_configure(driver, device) + device:configure() +end + +local function handle_lock_door(driver, device, zb_rx) + local function query_device() + device:send(DoorLock.attributes.LockState:read(device)) + end + device.thread:call_with_delay(5, query_device) +end + +local lock_without_codes = { + NAME = "Zigbee Lock Without Codes", + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure + }, + capability_handlers = { + [capabilities.lock.ID] = { + [capabilities.lock.commands.lock.NAME] = handle_lock, + [capabilities.lock.commands.unlock.NAME] = handle_unlock + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + cluster = { + [DoorLock.ID] = { + [DoorLock.commands.LockDoorResponse.ID] = handle_lock_door, + [DoorLock.commands.UnlockDoorResponse.ID] = handle_lock_door, + } + }, + attr = { + [DoorLock.ID] = { + [DoorLock.attributes.NumberOfPINUsersSupported.ID] = function() end -- just to make sure we don't switch profiles + } + } + }, + can_handle = can_handle_lock_without_codes +} + +return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua similarity index 98% rename from drivers/SmartThings/zigbee-lock/src/yale/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua index 73e984036e..a38e6be361 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua @@ -152,7 +152,7 @@ local yale_door_lock_driver = { } }, - sub_drivers = { require("yale.yale-bad-battery-reporter") }, + sub_drivers = { require("using-old-capabilities.yale.yale-bad-battery-reporter") }, can_handle = function(opts, driver, device, ...) return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua new file mode 100644 index 0000000000..59fdbf228b --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua @@ -0,0 +1,52 @@ +-- 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. + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +local is_bad_yale_lock_models = function(opts, driver, device) + for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local battery_report_handler = function(driver, device, value) + device:emit_event(capabilities.battery.battery(value.value)) +end + +local bad_yale_driver = { + NAME = "YALE BAD Lock Driver", + zigbee_handlers = { + attr = { + [clusters.PowerConfiguration.ID] = { + [clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_report_handler + } + } + }, + can_handle = is_bad_yale_lock_models +} + +return bad_yale_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 b83b196256..2710ee7a59 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -146,6 +146,48 @@ local function update_codes(driver, device, cmd) end end +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function migrate(driver, device, cmd) + local lock_users = {} + local lock_credentials = {} + local lc_data = json.decode(device.data.lockCodes) + local lock_codes = {} + local ordered_codes = {} + for k, v in pairs(lc_data) do + lock_codes[k] = v + end + + 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) + 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.users(lock_users, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) +end + local function time_get_handler(driver, device, cmd) local time = os.date("*t") device:send_to_component( @@ -162,6 +204,8 @@ local driver_template = { supported_capabilities = { capabilities.lock, capabilities.lockCodes, + capabilities.lockUsers, + capabilities.lockCredentials, capabilities.battery, capabilities.tamperAlert }, @@ -173,6 +217,9 @@ local driver_template = { [capabilities.lockCodes.ID] = { [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes }, + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.migrate.NAME] = migrate + }, [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } 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..17629c1912 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua @@ -0,0 +1,156 @@ +-- 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. + +-- 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 Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) +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 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 lockCodes = { + ["1"] = "Zach", + ["5"] = "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 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, + data = { + lockCodes = json.encode(utils.deep_copy(lockCodes)) + } + } +) + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +test.register_coroutine_test( + "Device called 'migrate' command", + 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", ["5"] = "Steven" }) + -- Validate migration complete flag + mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + -- 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.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( + "Schlage-Lock device called 'migrate' command, validate codeLength is being properly set", + function() + test.mock_device.add_test_device(schlage_mock_device) + test.socket.device_lifecycle:__queue_receive({ schlage_mock_device.id, "added" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + schlage_mock_device, + DoorLock:OperationGet({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + schlage_mock_device, + Battery:Get({}) + ) + ) + test.wait_for_events() + -- Validate lockCodes field + mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" }) + -- Validate migration complete flag + mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "migrationComplete", true) + -- 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.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 From 16cd90361b12e24c0bc965d90da77c349e8fb583 Mon Sep 17 00:00:00 2001 From: Pegor Date: Mon, 22 Dec 2025 15:27:11 -0800 Subject: [PATCH 02/16] Add reload command to samsung subdriver --- .../zigbee-lock/src/using-new-capabilities/samsungsds/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua index b42f3b0f14..6e16d6339e 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua @@ -20,6 +20,7 @@ local cluster_base = require "st.zigbee.cluster_base" local PowerConfiguration = clusters.PowerConfiguration local DoorLock = clusters.DoorLock local Lock = capabilities.lock +local lock_utils = require "new_lock_utils" local SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND = 0x1F local SAMSUNG_SDS_MFR_CODE = 0x0003 @@ -63,6 +64,7 @@ local function emit_event_if_latest_state_missing(device, component, capability, end local device_added = function(self, device) + lock_utils.reload_tables(device) emit_event_if_latest_state_missing(device, "main", capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) device:emit_event(capabilities.battery.battery(100)) end @@ -79,6 +81,7 @@ local device_init = function(driver, device, event) battery_init(driver, device, event) device:remove_monitored_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) device:remove_configured_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) + lock_utils.reload_tables(device) end local samsung_sds_driver = { From 906e247bc444afc7c320d3a17f5c995bacbdc22d Mon Sep 17 00:00:00 2001 From: Pegor Date: Mon, 22 Dec 2025 16:56:12 -0800 Subject: [PATCH 03/16] Add a way to test the old capabilities. --- drivers/SmartThings/zigbee-lock/src/init.lua | 26 +++++--- .../test/test_zigbee_lock_code_migration.lua | 6 +- .../test_zigbee_lock_code_slga_migration.lua | 61 ++++++++++--------- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 4a38d5679a..8f321efc68 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -79,23 +79,35 @@ local alarm_handler = function(driver, device, zb_mess) end end - -- this command should now trigger setting the migrated field and reinjecting the command. - -- this is so we can start using the new capbilities from now on. +-- this command should now trigger setting the migrated field and reinjecting the command. +-- this is so we can start using the new capbilities from now on. local function device_added(driver, device) - if device:supports_capability_by_id(LockCodes.ID) then - device:emit_event(LockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) - if device.device_added ~= nil then - -- make the driver call this command again, it will now be handled in new capabilities. - driver.lifecycle_handlers.device_added(driver, device) + -- this variable should only be present for test cases trying to test the old capabilities. + if device.useOldCapabilityForTesting == nil then + if device:supports_capability_by_id(LockCodes.ID) then + device:emit_event(LockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) + if device.device_added ~= nil then + -- make the driver call this command again, it will now be handled in new capabilities. + driver.lifecycle_handlers.device_added(driver, device) + end + else + lock_utils.populate_state_from_data(device) + driver:inject_capability_command(device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) end else lock_utils.populate_state_from_data(device) + driver:inject_capability_command(device, { capability = capabilities.refresh.ID, command = capabilities.refresh.commands.refresh.NAME, args = {} }) end + end local function init(driver, device) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua index 1aa9432933..f08d92d1e3 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua @@ -34,14 +34,16 @@ local mock_device = test.mock_device.build_test_zigbee_device( ["1"] = "Zach", ["2"] = "Steven" }) - } + }, + useOldCapabilityForTesting = true } ) local mock_device_no_data = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("base-lock.yml"), - data = {} + data = {}, + useOldCapabilityForTesting = true } ) zigbee_test_utils.prepare_zigbee_env_info() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua index 5361029c89..d0de6d6824 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua @@ -35,7 +35,8 @@ local mock_device = test.mock_device.build_test_zigbee_device( ["1"] = "Zach", ["5"] = "Steven" }), - } + }, + useOldCapabilityForTesting = true } ) @@ -47,36 +48,36 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Device called 'migrate' command", function() - -- test.mock_device.add_test_device(mock_device) - -- -- test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - -- test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) - -- test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) - -- test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) - -- test.wait_for_events() - -- -- Validate lockCodes field - -- mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" }) - -- -- Validate migration complete flag - -- mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) + test.wait_for_events() + -- Validate lockCodes field + mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" }) + -- Validate migration complete flag + mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - -- -- Set min/max code length attributes - -- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) }) - -- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) }) - -- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) }) - -- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } }))) - -- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } }))) - -- 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 functionality. - -- 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(5, { 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.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.lockUsers.totalUsersSupported(4, { 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() + -- Set min/max code length attributes + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) }) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) }) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } }))) + 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 functionality. + 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(5, { 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.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.lockUsers.totalUsersSupported(4, { 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() end ) From 631cd3a678c82614d91e4460fb7b092087358a8b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Sun, 21 Dec 2025 17:22:25 -0800 Subject: [PATCH 04/16] SLGA: Z-Wave lock update --- .../zigbee-lock/src/new_lock_utils.lua | 32 +- .../src/using-new-capabilities/init.lua | 18 +- .../zwave-lock/src/apiv6_bugfix/init.lua | 26 - drivers/SmartThings/zwave-lock/src/init.lua | 189 +--- .../zwave-lock/src/test/test_zwave_lock.lua | 10 +- .../test/test_zwave_lock_code_migration.lua | 246 ----- .../test_zwave_lock_code_slga_migration.lua | 97 +- .../test/test_zwave_lock_new_capabilities.lua | 982 ++++++++++++++++++ .../src/using-new-capabilities/init.lua | 532 ++++++++++ .../keywe-lock/init.lua | 86 ++ .../using-new-capabilities/new_lock_utils.lua | 227 ++++ .../samsung-lock/init.lua | 111 ++ .../schlage-lock/init.lua | 193 ++++ .../zwave-alarm-v1-lock/init.lua | 165 +++ .../src/using-old-capabilities/can_handle.lua | 10 + .../src/using-old-capabilities/init.lua | 175 ++++ .../keywe-lock/init.lua | 86 ++ .../samsung-lock/init.lua | 111 ++ .../schlage-lock/init.lua | 193 ++++ .../zwave-alarm-v1-lock/init.lua | 165 +++ 20 files changed, 3132 insertions(+), 522 deletions(-) delete mode 100644 drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_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/init.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/new_lock_utils.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/init.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 create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua index c34c839d68..f13b00eec7 100644 --- a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -220,7 +220,9 @@ new_lock_utils.delete_user = function(device, user_index) for index, user in pairs(current_users) do if user.userIndex == user_index then - table.remove(current_users, index) + -- 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 @@ -244,7 +246,9 @@ new_lock_utils.delete_credential = function(device, credential_index) for index, credential in pairs(credentials) do if credential.credentialIndex == credential_index then new_lock_utils.delete_user(device, credential.userIndex) - table.remove(credentials, index) + -- 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 @@ -270,4 +274,28 @@ new_lock_utils.update_credential = function(device, credential_index, user_index 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 + return new_lock_utils diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 89a9cd59ec..2279078d1c 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -118,8 +118,7 @@ local add_user_handler = function(driver, device, command) end if status == lock_utils.STATUS_SUCCESS then - local current_users = lock_utils.get_users(device) - device:emit_event(capabilities.lockUsers.users(current_users, {state_chang = true, visibility = { displayed = true } })) + lock_utils.send_events(device, lock_utils.LOCK_USERS) end lock_utils.clear_busy_state(device, status) @@ -142,7 +141,7 @@ local update_user_handler = function(driver, device, command) user.userName = user_name user.userType = user_type device:set_field(lock_utils.LOCK_USERS, current_users) - device:emit_event(capabilities.lockUsers.users(current_users, { state_change = true, visibility = { displayed = true } })) + lock_utils.send_events(device, lock_utils.LOCK_USERS) status = lock_utils.STATUS_SUCCESS break end @@ -175,8 +174,7 @@ local delete_user_handler = function(driver, device, command) }) else lock_utils.delete_user(device, user_index) - local current_users = lock_utils.get_users(device) - device:emit_event(capabilities.lockUsers.users(current_users, { state_change = true, visibility = { displayed = true } })) + lock_utils.send_events(device, lock_utils.LOCK_USERS) lock_utils.clear_busy_state(device, status, command.override_busy_check) end else @@ -413,10 +411,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) end if emit_event then - device:emit_event(capabilities.lockUsers.users(lock_utils.get_users(device), - { state_change = true, visibility = { displayed = true } })) - device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), - { state_change = true, visibility = { displayed = true } })) + lock_utils.send_events(device) end -- ignore handling the busy state for these commands, they are handled within their own handlers @@ -466,10 +461,7 @@ local programming_event_handler = function(driver, device, zb_mess) end if emit_events then - device:emit_event(capabilities.lockUsers.users(lock_utils.get_users(device), - { state_change = true, visibility = { displayed = true } })) - device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), - { state_change = true, visibility = { displayed = true } })) + lock_utils.send_events(device) end end diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua deleted file mode 100644 index 0204b7b2d5..0000000000 --- a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,26 +0,0 @@ -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = can_handle -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 2710ee7a59..653f99e3f3 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -19,176 +19,28 @@ 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 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) - 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 - ) - 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 - ) - end +local lazy_load_if_possible = function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) 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()) + return require(sub_driver_name) 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) + 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 ---- @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 - end - end -end - ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @param cmd table -local function migrate(driver, device, cmd) - local lock_users = {} - local lock_credentials = {} - local lc_data = json.decode(device.data.lockCodes) - local lock_codes = {} - local ordered_codes = {} - for k, v in pairs(lc_data) do - lock_codes[k] = v - end - - 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) - 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.users(lock_users, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) -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({ @@ -209,32 +61,19 @@ local driver_template = { capabilities.battery, capabilities.tamperAlert }, - lifecycle_handlers = { - added = added_handler, - init = init_handler, - }, capability_handlers = { - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes - }, - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.migrate.NAME] = migrate - }, [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("zwave-alarm-v1-lock"), - require("schlage-lock"), - require("samsung-lock"), - require("keywe-lock"), - require("apiv6_bugfix"), + require("using-new-capabilities"), + require("using-old-capabilities"), } } 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 52144295b3..3ee38c115b 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -43,10 +43,10 @@ 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 + } ) local function test_init() @@ -459,7 +459,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_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua deleted file mode 100644 index e4a9b50758..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua +++ /dev/null @@ -1,246 +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. - --- 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 -) - -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 -) - -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 -) - -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 -) - - -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 -) - -test.run_registered_tests() 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 17629c1912..d446289fc0 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 @@ -26,8 +26,9 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version 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" +--- @type st.zwave.constants +local constants = require "st.zwave.constants" local SCHLAGE_MANUFACTURER_ID = 0x003B local SCHLAGE_PRODUCT_TYPE = 0x0002 @@ -44,19 +45,11 @@ local zwave_lock_endpoints = { } } -local lockCodes = { - ["1"] = "Zach", - ["5"] = "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)) - } - } + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints, + } ) local schlage_mock_device = test.mock_device.build_test_zwave_device( @@ -66,37 +59,31 @@ local schlage_mock_device = test.mock_device.build_test_zwave_device( zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, zwave_product_id = SCHLAGE_PRODUCT_ID, - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } } ) 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() - 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", ["5"] = "Steven" }) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + 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 } }))) @@ -108,33 +95,32 @@ test.register_coroutine_test( 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(4, { 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() - test.mock_device.add_test_device(schlage_mock_device) - test.socket.device_lifecycle:__queue_receive({ schlage_mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - schlage_mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - schlage_mock_device, - Battery:Get({}) - ) - ) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" }) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "migrationComplete", true) + 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 })}) @@ -148,6 +134,7 @@ test.register_coroutine_test( 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 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..6edacbb690 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua @@ -0,0 +1,982 @@ +-- 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. + +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 + } +) + +-- start with a migrated blank device +local function test_init() + test.mock_device.add_test_device(mock_device) + 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.set_test_init_function(test_init) + + +test.register_coroutine_test( + "Add user should succeed", + function () + 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" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Add credential should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 2, "guest", "pin", "3456" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + {userIndex = 1, userType = "guest", userName = "Code 1" }, + {userIndex = 2, userType = "guest", userName = "Code 2" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + 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, + UserCode:Report({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" } + }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Add credential for existing user should succeed", + function () + 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" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + 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:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Update user should succeed", + function () + 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" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete user should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + 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.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") + 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)) + 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.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, 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.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Update credential should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + 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, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete credential should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + 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, + 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.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, 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.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete all users should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 2, "guest", "pin", "3456" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + {userIndex = 1, userType = "guest", userName = "Code 1" }, + {userIndex = 2, userType = "guest", userName = "Code 2" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + 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, + UserCode:Report({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" } + }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "deleteAllUsers", + args = {} + }, + }) + 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.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "success" }, + { state_change = true, 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.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + 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) + ) + end +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code number", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + 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 = false } } + ) + ) + ) + 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:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + 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.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.run_registered_tests() \ 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..fc171b493a --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -0,0 +1,532 @@ +local capabilities = require "st.capabilities" +local LockUsers = capabilities.lockUsers +local LockCredentials = capabilities.lockCredentials +local lock_utils = require "using-new-capabilities.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 access_control_event = Notification.event.access_control + +-- Helper methods + +local add_or_update = function(device, method, credential_index, user_index) + -- if so, add the credential to the list + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + credentials[credential_index] = { userIndex = user_index, credentialIndex = credential_index, credentialType = "pin"} + -- emit credential event + device:emit_event(LockCredentials.credentials(credentials, { visibility = { displayed = false } })) + -- emit command success + device:emit_event(LockCredentials.commandResult( + { commandName = method, statusCode = lock_utils.STATUS_SUCCESS, credentialIndex = credential_index}, { state_change = true, visibility = { displayed = false } } + )) + -- set the ongoing operation field to nil + device:set_field(method..credential_index, nil) +end + +-- returns the index of the lowest unset index less than the max +local next_empty_index = function(table, max) + local index = 1 + for i = 1, max + 1 do + if table[i] == nil then + index = i + break + end + end + return index +end + +-- Lifecycle handlers +local added_handler = function(driver, device) + -- read user/credential metadata + -- reload all codes +end + +-- Lock Users commands + +local add_user_handler = function(driver, device, cmd) + local user_name = cmd.args.userName + local user_type = cmd.args.userType + -- get the table of current users + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + -- check that we can add a new user + local max_users = device:get_latest_state("main", LockUsers.ID, LockUsers.totalUsersSupported.NAME, 8) + if utils.table_size(users) == max_users then + -- we cannot create a new user (unlikely!) + end + -- find the index to add the user at + local index = next_empty_index(users, max_users) + -- insert the user into the table + users[index] = {userIndex = index, userName = user_name, userType = user_type} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) +end + +local update_user_handler = function(driver, device, cmd) + local index = cmd.args.userIndex + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + -- does the user index already exist? + -- if not, update the user (offset user index by 1) + if users[index] ~= nil then + -- insert the user into the table + users[index] = {userIndex = index, userName = cmd.args.userName, userType = cmd.args.userType} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.UPDATE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + end +end + +local delete_user_handler = function(driver, device, cmd) + local index = cmd.args.userIndex + -- make sure the user exists + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + if users[index] ~= nil then + -- see if the user is associated with a lock code + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + for _, credential in pairs(credentials) do + if credential.userIndex == index then + -- if so, delete that code + device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) + -- save state for receipt of delete + device:set_field("_delete_credential"..credential.credentialIndex, index) + -- make sure delete went through + device.thread:call_with_delay(4.2, function(d) device:send(UserCode:Get({user_identifier = credential.credentialIndex})) end) + return -- if the user has a credential, we need confirmation that the code was deleted before proceeding + end + end + -- delete user from the list + users[index] = nil + -- emit users event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit user delete success + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + end +end + +local delete_all_users_handler = function(driver, device, cmd) + -- TODO: Z-Wave User Code v2 includes mass sets/gets that could be leveraged to make this simpler + -- delete every user + -- send users event + device:emit_event(LockUsers.users({}, { visibility = { displayed = false}})) + -- send success event + device:emit_event(LockUsers.commandResult({ commandName = lock_utils.DELETE_ALL_USERS, statusCode = lock_utils.STATUS_SUCCESS }, { state_change = true, visibility = { displayed = false}})) + + + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + -- delete every credential + local delay = 0 + for _, credential in pairs(credentials) do + device.thread:call_with_delay(delay, function(d) + device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) + end) + -- include a delay between deletes + delay = delay + .5 + end + -- send credentials event + device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false}})) + -- send success event (this would be tedious to check for every code, so assume they all went through) + device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { state_change = true, visibility = { displayed = false}})) +end + +--- Lock Credentials Commands + +local add_credential_handler = function(driver, device, cmd) + local index = cmd.args.userIndex + local user_type = cmd.args.userType + local credential_type = cmd.args.credentialType -- if this is not "pin", send an error + local data = cmd.args.credentialData + -- get the table of current credentials + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + -- does the user index already exist? + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + -- if not, create a new user (offset user index by 1) + if users[index] == nil then + -- insert the user into the table + users[index] = {userIndex = index, userName = "Code "..index, userType = user_type} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + end + -- find the index to add the credential at + local max_credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.pinUsersSupported.NAME, 8) + local credential_index = next_empty_index(credentials, max_credentials) + -- save some state so we can complete the transaction on message receipt + device:set_field(lock_utils.ADD_CREDENTIAL..credential_index, index) + -- send the credential creation message + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) +end + +local update_credential_handler = function(driver, device, cmd) + -- validate args + local user_index = cmd.args.userIndex + local credential_index = cmd.args.credentialIndex + local credential_type = cmd.args.credentialType + local data = cmd.args.credentialData + -- make sure credential already exists + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + if credentials[credential_index] ~= nil and users[user_index] ~= nil then + -- store state to track update + device:set_field(lock_utils.UPDATE_CREDENTIAL..credential_index, user_index) + -- send command to update code + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + else + -- failure + end +end + +local delete_credential_handler = function(driver, device, cmd) + -- find the user associated with this credential + local user_index = cmd.args.credentialIndex + -- run delete user with that credential + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteUser.NAME, + args = { user_index } + }) +end + +local delete_all_credentials_handler = function(driver, device, cmd) + -- check to see if we have users that do not have a code associated + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + local user_index_marked_for_individual_deletion = {} + + for _, credential in pairs(credentials) do + users[credential.userIndex] = nil + user_index_marked_for_individual_deletion[credential.userIndex] = true + end + -- if we don't, this is equivalent to delete_all_users + if utils.table_size(users) == 0 then + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteAllUsers.NAME, + args = { } + }) + return + end + -- if we do, delete all users other than those + for i, _ in pairs(user_index_marked_for_individual_deletion) do + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = i }, { state_change = true, visibility = { displayed = false } } + )) + end + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- and then delete all codes + local delay = 0 + for _, credential in pairs(credentials) do + device.thread.call_with_delay(delay, function(d) + device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) + end) + -- include a delay between deletes + delay = delay + .5 + end + -- send credentials event (these will be sent before all the actual deletes have been sent) + device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false}})) + -- send success event (this would be tedious to check for every code, so assume they all went through) + device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { visibility = { displayed = false}})) +end + + + +-- Z-Wave Message Handlers + +local user_code_report_handler = function(driver, device, cmd) + local code_id = cmd.args.user_identifier + local user_id_status = cmd.args.user_id_status + + -- is this a report about an occupied credential index? + 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 + -- are we in the middle of a user code set for this index? + local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..code_id) + local user_index_update = device:get_field(lock_utils.UPDATE_CREDENTIAL..code_id) + if user_index_add ~= nil then + add_or_update(device, lock_utils.ADD_CREDENTIAL, code_id, user_index_add) + elseif user_index_update ~= nil then + add_or_update(device, lock_utils.UPDATE_CREDENTIAL, code_id, user_index_update) + end + elseif user_id_status == UserCode.user_id_status.AVAILABLE then + -- are we in the middle of a user code delete? + local user_index = device:get_field("_delete_credential"..code_id) + if user_index ~= nil then + -- if so, delete the credential + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + credentials[code_id] = nil + -- emit credential event + device:emit_event(LockCredentials.credentials(credentials, { visibility = { displayed = false } })) + -- emit command success + device:emit_event(LockCredentials.commandResult( + { commandName = lock_utils.DELETE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS, credentialIndex = code_id }, { state_change = true, visibility = { displayed = false } } + )) + -- delete the user + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + users[user_index] = nil + -- emit users event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit command success + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index }, { state_change = true, visibility = { displayed = false } } + )) + -- clear state + device:set_field("_delete_credential"..code_id, nil) + end + end +end + +local notification_report_handler = function(driver, device, cmd) + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event = cmd.args.event + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + if (event == access_control_event.ALL_USER_CODES_DELETED) then + -- this is unexpected, but we got this out of band, so... + -- check to see if we have users that do not have a code associated + local user_index_marked_for_individual_deletion = {} + for _, credential in pairs(credentials) do + users[credential.userIndex] = nil + user_index_marked_for_individual_deletion[credential.userIndex] = true + end + -- if we don't, this is equivalent to delete_all_users + if utils.table_size(users) == 0 then + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteAllUsers.NAME, + args = { } + }) + return + end + -- if we do, delete all users other than those + for i, _ in pairs(user_index_marked_for_individual_deletion) do + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = i }, { state_change = true, visibility = { displayed = false } } + )) + end + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit empty credentials + device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { visibility = { displayed = false}})) + elseif (event == access_control_event.SINGLE_USER_CODE_DELETED) then + local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) + -- find the user index assigned to this code to delete it as well + local credential = credentials[credential_index] + if credential ~= nil then + -- we may want to check if these match + local stored_user_index = device:get_field("_delete_credential"..credential_index) + local user_index = credential.userIndex + local user = users[user_index] + if user ~= nil then + users[user_index] = nil + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index }, { state_change = true, visibility = { displayed = false } } + )) + device:set_field("_delete_credential"..credential_index, nil) + end + else + -- something bad happened + end + elseif (event == access_control_event.NEW_USER_CODE_ADDED) then + local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) + -- determine if this is due to a command or an out-of-band update + local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..credential_index) + local user_index_update = device:get_field(lock_utils.UPDATE_CREDENTIAL..credential_index) + if user_index_add ~= nil then + add_or_update(device, lock_utils.ADD_CREDENTIAL, credential_index, user_index_add) + elseif user_index_update ~= nil then + add_or_update(device, lock_utils.UPDATE_CREDENTIAL, credential_index, user_index_update) + else + -- out-of-band update + -- create a user for this code index + local max_users = device:get_latest_state("main", LockUsers.ID, LockUsers.totalUsersSupported.NAME, 8) + -- find the index to add the user at + local index = next_empty_index(users, max_users) + -- insert the user into the table + users[index] = {userIndex = index, userName = "Code "..index, userType = "guest"} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + -- add the credential + add_or_update(device, lock_utils.ADD_CREDENTIAL, credential_index, index) + end + elseif (event == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE) then + local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) + -- this is a create code failure + -- double check we have a stored add command + local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..credential_index) + -- clear that state + device:set_field(lock_utils.ADD_CREDENTIAL..credential_index, nil) + -- emit a credential add failure + device:emit_event(LockCredentials.commandResult( + { commandName = lock_utils.ADD_CREDENTIAL, statusCode = "duplicate", credentialIndex = credential_index }, { state_change = true, visibility = { displayed = false } } + )) + -- if we have a stored add command, we should delete the associated user, I think + if users[user_index_add] ~= nil then + users[user_index_add] = nil + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index_add }, { state_change = true, visibility = { displayed = false } } + )) + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + end + elseif (event == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION) then + -- master code changed + -- we might want to fire a credential updated success here? + + + -- these are all the lock operation events + elseif (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 + if (credentials ~= nil and + credentials[code_id] ~= nil) then + user_id = credentials[code_id].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 + +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, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.pinUsersSupported(cmd.args.supported_users, { visibility = { displayed = false } })) +end + +local zwave_lock = { + lifecycle_handlers = { + added = added_handler, + }, + 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, + } + }, + NAME = "Using new capabilities", + can_handle = function(opts, driver, device, ...) + if not device:supports_capability_by_id(LockUsers.ID) then return false end + 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 +} + +return zwave_lock \ 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..d39aa45d1c --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua @@ -0,0 +1,86 @@ +-- 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. + +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 KEYWE_MFR = 0x037B +local TAMPER_CLEAR_DELAY = 10 + +local function can_handle_keywe_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == KEYWE_MFR +end + +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 = can_handle_keywe_lock, +} + +return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua new file mode 100644 index 0000000000..43a22cc713 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua @@ -0,0 +1,227 @@ +-- 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. +local utils = require "st.utils" +local capabilities = require "st.capabilities" +local json = require "st.json" +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers +local INITIAL_INDEX = 1 + +local new_lock_utils = { + -- Constants + ADD_CREDENTIAL = "addCredential", + ADD_USER = "addUser", + 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", + PENDING_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", + UPDATE_CREDENTIAL = "updateCredential", + UPDATE_USER = "updateUser", + USER_INDEX = "userIndex", + USER_NAME = "userName", + USER_TYPE = "userType" +} + +new_lock_utils.get_users = function(device) + local users = 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 ipairs(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(current_data, max) + if current_data == nil and max ~= 0 then + return INITIAL_INDEX + elseif current_data ~= nil then + for index = 1, max do + if current_data["user" .. index] == nil then + return index + end + end + end + + return nil +end + +new_lock_utils.get_credentials = function(device) + local credentials = 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 ipairs(new_lock_utils.get_credentials(device)) do + if credential.credentialIndex == credential_index then + return credential + end + end + return nil +end + +new_lock_utils.get_available_credential_index = function(current_data, max) + local available_index = nil + local used_index = {} + + for i, _ in ipairs(current_data) do + used_index[i] = true + end + + if current_data ~= {} 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) + local status_code = new_lock_utils.STATUS_SUCCESS + local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, + capabilities.lockUsers.totalUsersSupported.NAME, 0) + local current_users = new_lock_utils.get_users(device) + local available_index = new_lock_utils.get_available_user_index(current_users, max_users) + + if max_users == 0 or available_index == nil then + -- Can't add any users - update commandResult statusCode + status_code = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + else + -- use the passed in index if it's set + if user_index ~= nil then + available_index = user_index + end + current_users["user"..available_index] = { userIndex = available_index, userType = user_type, userName = user_name } + device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) + end + + return status_code +end + +new_lock_utils.delete_user = function(device, user_index, deleted_by_credential_deletion) + 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 + -- also delete associated credential if this isn't being call by a credential deletion. + if not deleted_by_credential_deletion then + -- find associated credential. + for _, credential in ipairs(new_lock_utils.get_credentials(device)) do + if credential.userIndex == user_index then + new_lock_utils.delete_credential(device, credential.credentialIndex, true) + break + end + end + end + -- table.remove(current_users, index) + 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, user_type, credential_type, credential_index) + -- need to also create a user if one does not exist at the user index. + if new_lock_utils.get_user(device, user_index) == nil then + local user_name = "USER_" .. user_index + local status = new_lock_utils.create_user(device, user_name, user_type, user_index) + if status ~= new_lock_utils.STATUS_SUCCESS then + return status + end + end + + 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) + return new_lock_utils.STATUS_SUCCESS +end + +new_lock_utils.delete_credential = function(device, credential_index, deleted_by_user_deletion) + 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 + -- also delete associated user if this isn't being called by a user deletion. + if not deleted_by_user_deletion then + new_lock_utils.delete_user(device, credential.userIndex, true) + end + table.remove(credentials, index) + 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 ipairs(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 + break + end + end + return status_code +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 + +return new_lock_utils 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..813c6217b4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua @@ -0,0 +1,111 @@ +-- 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. + +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 json = require "dkjson" +local constants = require "st.zwave.constants" + +local LockDefaults = require "st.zwave.defaults.lock" +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 SAMSUNG_MFR = 0x022E + +local function can_handle_samsung_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SAMSUNG_MFR +end + +local function get_ongoing_code_set(device) + local code_id + local code_state = device:get_field(constants.CODE_STATE) + if code_state ~= nil then + for key, state in pairs(code_state) do + if state ~= nil then + code_id = key:match("setName(%d)") + end + end + end + return code_id +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.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.NEW_USER_CODE_ADDED then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + device:send(UserCode:Get({user_identifier = code_id})) + return + end + elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) + clear_code_state(device, code_id) + end + elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then + -- Update Master Code in the same way as in defaults... + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + -- ...and delete rest of them, as lock does + local lock_codes = get_lock_codes(device) + for code_id, _ in pairs(lock_codes) do + if code_id ~= "0" then + code_deleted(device, code_id) + end + end + event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) + 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) + 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()) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) +end + +local samsung_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Samsung Lock", + can_handle = can_handle_samsung_lock, +} + +return samsung_lock 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..67e649d869 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua @@ -0,0 +1,193 @@ +-- 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. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local constants = require "st.zwave.constants" +local json = require "dkjson" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local user_id_status = UserCode.user_id_status +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control +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 LockCodesDefaults = require "st.zwave.defaults.lockCodes" + +local SCHLAGE_MFR = 0x003B +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds + +local function can_handle_schlage_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SCHLAGE_MFR +end + +local function set_code_length(self, device, cmd) + local length = cmd.args.length + if length >= 4 and length <= 8 then + device:send(Configuration:Set({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = length, + size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size + })) + end +end + +local function reload_all_codes(self, device, cmd) + LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + end +end + +local function set_code(self, device, cmd) + if (cmd.args.codePIN == "") then + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = {cmd.args.codeSlot, cmd.args.codeName}, + }) + else + -- copied from defaults with additional check for Schlage's configuration + if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then + if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end + local code_state = device:get_field(constants.CODE_STATE) + code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName + device:set_field(constants.CODE_STATE, code_state, { persist = true }) + end + local send_set_user_code = function () + device:send(UserCode:Set({ + user_identifier = cmd.args.codeSlot, + user_code = cmd.args.codePIN, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + end + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + 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, send_set_user_code) + else + send_set_user_code() + end + end +end + +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.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length ~= nil and current_code_length ~= reported_code_length then + local all_codes_deleted_mocked_command = Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.ALL_USER_CODES_DELETED + }) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) + end + device:emit_event(capabilities.lockCodes.codeLength(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 code_id = cmd.args.user_identifier + if is_user_code_report_mfr_specific(device, cmd) then + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local event + + 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_name = LockCodesDefaults.get_code_name(device, code_id) + local change_type = LockCodesDefaults.get_change_type(device, code_id) + event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) + event.data = {codeName = code_name} + if code_id ~= 0 then -- ~= MASTER_CODE + LockCodesDefaults.code_set_event(device, code_id, code_name) + end + elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then + local lock_codes = LockCodesDefaults.get_lock_codes(device) + for _code_id, _ in pairs(lock_codes) do + LockCodesDefaults.code_deleted(device, _code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) + else -- user_id_status.STATUS_NOT_AVAILABLE + event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) + end + + if event ~= nil then + device:emit_event(event) + end + LockCodesDefaults.clear_code_state(device, code_id) + LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) + else + LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + end +end + +local schlage_lock = { + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, + [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [capabilities.lockCodes.commands.setCode.NAME] = set_code + } + }, + zwave_handlers = { + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + }, + [cc.BASIC] = { + [Basic.SET] = basic_set_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "Schlage Lock", + can_handle = can_handle_schlage_lock, +} + +return schlage_lock 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..44d978999b --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua @@ -0,0 +1,165 @@ +-- 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. + +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 }) +--- @type st.zwave.defaults.lockCodes +local lock_code_defaults = require "st.zwave.defaults.lockCodes" +local json = require "dkjson" + +local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + 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 +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 +end + +--- 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 lock_codes = lock_code_defaults.get_lock_codes(device) + local code_id = nil + if (cmd.args.alarm_level ~= nil) then + code_id = tostring(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 (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 18) then + event = capabilities.lock.lock.locked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, 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 user codes deleted + for code_id, _ in pairs(lock_codes) do + lock_code_defaults.code_deleted(device, code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + elseif (alarm_type == 33) then + -- user code deleted + if (code_id ~= nil) then + lock_code_defaults.clear_code_state(device, code_id) + if (lock_codes[code_id] ~= nil) then + lock_code_defaults.code_deleted(device, code_id) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + end + end + elseif (alarm_type == 13 or alarm_type == 112) then + -- user code changed/set + if (code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + local change_type = lock_code_defaults.get_change_type(device, code_id) + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) + code_changed_event["data"] = { codeName = code_name} + lock_code_defaults.code_set_event(device, code_id, code_name) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 34 or alarm_type == 113) then + -- duplicate lock code + if (code_id ~= nil) then + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + 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 = can_handle_v1_alarm, +} + +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..8151871ac3 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua @@ -0,0 +1,10 @@ +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..08cd518841 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -0,0 +1,175 @@ +-- 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. + +local capabilities = require "st.capabilities" + +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 + ) + end +end + +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 + +--- Builds up initial state for the device +--- +--- @param self st.zwave.Driver +--- @param device st.zwave.Device +local function added_handler(self, device) + 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 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 +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 = { + lifecycle_handlers = { + init = init_handler, + added = added_handler, + }, + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes + }, + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.migrate.NAME] = migrate + }, + }, + sub_drivers = { + require("using-old-capabilities.zwave-alarm-v1-lock"), + require("using-old-capabilities.schlage-lock"), + require("using-old-capabilities.samsung-lock"), + require("using-old-capabilities.keywe-lock"), + }, + can_handle = function(opts, driver, device, ...) + if not device:supports_capability_by_id(capabilities.lockCodes.ID) then return false end + 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, + NAME = "Using old capabilities" +} + +return using_old_capabilities 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 new file mode 100644 index 0000000000..d39aa45d1c --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua @@ -0,0 +1,86 @@ +-- 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. + +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 KEYWE_MFR = 0x037B +local TAMPER_CLEAR_DELAY = 10 + +local function can_handle_keywe_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == KEYWE_MFR +end + +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 = can_handle_keywe_lock, +} + +return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua new file mode 100644 index 0000000000..813c6217b4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua @@ -0,0 +1,111 @@ +-- 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. + +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 json = require "dkjson" +local constants = require "st.zwave.constants" + +local LockDefaults = require "st.zwave.defaults.lock" +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 SAMSUNG_MFR = 0x022E + +local function can_handle_samsung_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SAMSUNG_MFR +end + +local function get_ongoing_code_set(device) + local code_id + local code_state = device:get_field(constants.CODE_STATE) + if code_state ~= nil then + for key, state in pairs(code_state) do + if state ~= nil then + code_id = key:match("setName(%d)") + end + end + end + return code_id +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.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.NEW_USER_CODE_ADDED then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + device:send(UserCode:Get({user_identifier = code_id})) + return + end + elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) + clear_code_state(device, code_id) + end + elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then + -- Update Master Code in the same way as in defaults... + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + -- ...and delete rest of them, as lock does + local lock_codes = get_lock_codes(device) + for code_id, _ in pairs(lock_codes) do + if code_id ~= "0" then + code_deleted(device, code_id) + end + end + event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) + 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) + 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()) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) +end + +local samsung_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Samsung Lock", + can_handle = can_handle_samsung_lock, +} + +return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua new file mode 100644 index 0000000000..67e649d869 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua @@ -0,0 +1,193 @@ +-- 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. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local constants = require "st.zwave.constants" +local json = require "dkjson" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local user_id_status = UserCode.user_id_status +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control +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 LockCodesDefaults = require "st.zwave.defaults.lockCodes" + +local SCHLAGE_MFR = 0x003B +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds + +local function can_handle_schlage_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SCHLAGE_MFR +end + +local function set_code_length(self, device, cmd) + local length = cmd.args.length + if length >= 4 and length <= 8 then + device:send(Configuration:Set({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = length, + size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size + })) + end +end + +local function reload_all_codes(self, device, cmd) + LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + end +end + +local function set_code(self, device, cmd) + if (cmd.args.codePIN == "") then + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = {cmd.args.codeSlot, cmd.args.codeName}, + }) + else + -- copied from defaults with additional check for Schlage's configuration + if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then + if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end + local code_state = device:get_field(constants.CODE_STATE) + code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName + device:set_field(constants.CODE_STATE, code_state, { persist = true }) + end + local send_set_user_code = function () + device:send(UserCode:Set({ + user_identifier = cmd.args.codeSlot, + user_code = cmd.args.codePIN, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + end + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + 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, send_set_user_code) + else + send_set_user_code() + end + end +end + +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.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length ~= nil and current_code_length ~= reported_code_length then + local all_codes_deleted_mocked_command = Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.ALL_USER_CODES_DELETED + }) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) + end + device:emit_event(capabilities.lockCodes.codeLength(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 code_id = cmd.args.user_identifier + if is_user_code_report_mfr_specific(device, cmd) then + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local event + + 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_name = LockCodesDefaults.get_code_name(device, code_id) + local change_type = LockCodesDefaults.get_change_type(device, code_id) + event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) + event.data = {codeName = code_name} + if code_id ~= 0 then -- ~= MASTER_CODE + LockCodesDefaults.code_set_event(device, code_id, code_name) + end + elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then + local lock_codes = LockCodesDefaults.get_lock_codes(device) + for _code_id, _ in pairs(lock_codes) do + LockCodesDefaults.code_deleted(device, _code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) + else -- user_id_status.STATUS_NOT_AVAILABLE + event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) + end + + if event ~= nil then + device:emit_event(event) + end + LockCodesDefaults.clear_code_state(device, code_id) + LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) + else + LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + end +end + +local schlage_lock = { + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, + [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [capabilities.lockCodes.commands.setCode.NAME] = set_code + } + }, + zwave_handlers = { + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + }, + [cc.BASIC] = { + [Basic.SET] = basic_set_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "Schlage Lock", + can_handle = can_handle_schlage_lock, +} + +return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua new file mode 100644 index 0000000000..44d978999b --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua @@ -0,0 +1,165 @@ +-- 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. + +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 }) +--- @type st.zwave.defaults.lockCodes +local lock_code_defaults = require "st.zwave.defaults.lockCodes" +local json = require "dkjson" + +local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + 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 +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 +end + +--- 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 lock_codes = lock_code_defaults.get_lock_codes(device) + local code_id = nil + if (cmd.args.alarm_level ~= nil) then + code_id = tostring(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 (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 18) then + event = capabilities.lock.lock.locked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, 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 user codes deleted + for code_id, _ in pairs(lock_codes) do + lock_code_defaults.code_deleted(device, code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + elseif (alarm_type == 33) then + -- user code deleted + if (code_id ~= nil) then + lock_code_defaults.clear_code_state(device, code_id) + if (lock_codes[code_id] ~= nil) then + lock_code_defaults.code_deleted(device, code_id) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + end + end + elseif (alarm_type == 13 or alarm_type == 112) then + -- user code changed/set + if (code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + local change_type = lock_code_defaults.get_change_type(device, code_id) + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) + code_changed_event["data"] = { codeName = code_name} + lock_code_defaults.code_set_event(device, code_id, code_name) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 34 or alarm_type == 113) then + -- duplicate lock code + if (code_id ~= nil) then + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + 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 = can_handle_v1_alarm, +} + +return zwave_lock From 94de818d37f3eecb209ba024682dd9ba27e1ad8d Mon Sep 17 00:00:00 2001 From: Pegor Date: Tue, 23 Dec 2025 16:21:08 -0800 Subject: [PATCH 05/16] Zwave - Functioning base driver --- .../test_zigbee_lock_new_capabilities.lua | 16 +- .../zwave-lock/src/keywe-lock/init.lua | 86 -- .../zwave-lock/src/samsung-lock/init.lua | 111 --- .../zwave-lock/src/schlage-lock/init.lua | 193 ----- .../test_zwave_lock_code_slga_migration.lua | 2 +- .../test/test_zwave_lock_new_capabilities.lua | 734 +++++------------- .../src/using-new-capabilities/init.lua | 642 +++++++-------- .../using-new-capabilities/new_lock_utils.lua | 219 ++++-- .../src/using-old-capabilities/init.lua | 4 +- .../src/zwave-alarm-v1-lock/init.lua | 165 ---- 10 files changed, 660 insertions(+), 1512 deletions(-) delete mode 100644 drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua index 94fe7a2ace..ccd69baa54 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua @@ -95,7 +95,7 @@ local function add_default_users() "main", capabilities.lockUsers.users( user_list, - { visibility = { displayed = true } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -112,13 +112,13 @@ local function add_default_users() end local function add_credential(user_index, credential_data) -test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { user_index, "guest", "pin", credential_data } - }, + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { user_index, "guest", "pin", credential_data } + }, }) test.socket.zigbee:__expect_send( { diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua deleted file mode 100644 index d39aa45d1c..0000000000 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua +++ /dev/null @@ -1,86 +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. - -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 KEYWE_MFR = 0x037B -local TAMPER_CLEAR_DELAY = 10 - -local function can_handle_keywe_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == KEYWE_MFR -end - -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 = can_handle_keywe_lock, -} - -return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua deleted file mode 100644 index 813c6217b4..0000000000 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua +++ /dev/null @@ -1,111 +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. - -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 json = require "dkjson" -local constants = require "st.zwave.constants" - -local LockDefaults = require "st.zwave.defaults.lock" -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 SAMSUNG_MFR = 0x022E - -local function can_handle_samsung_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SAMSUNG_MFR -end - -local function get_ongoing_code_set(device) - local code_id - local code_state = device:get_field(constants.CODE_STATE) - if code_state ~= nil then - for key, state in pairs(code_state) do - if state ~= nil then - code_id = key:match("setName(%d)") - end - end - end - return code_id -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.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then - event = capabilities.lock.lock.unlocked() - elseif event_code == access_control_event.NEW_USER_CODE_ADDED then - local code_id = get_ongoing_code_set(device) - if code_id ~= nil then - device:send(UserCode:Get({user_identifier = code_id})) - return - end - elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then - local code_id = get_ongoing_code_set(device) - if code_id ~= nil then - event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) - clear_code_state(device, code_id) - end - elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then - -- Update Master Code in the same way as in defaults... - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - -- ...and delete rest of them, as lock does - local lock_codes = get_lock_codes(device) - for code_id, _ in pairs(lock_codes) do - if code_id ~= "0" then - code_deleted(device, code_id) - end - end - event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) - 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) - 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()) - device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) -end - -local samsung_lock = { - zwave_handlers = { - [cc.NOTIFICATION] = { - [Notification.REPORT] = notification_report_handler - } - }, - lifecycle_handlers = { - doConfigure = do_configure - }, - NAME = "Samsung Lock", - can_handle = can_handle_samsung_lock, -} - -return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua deleted file mode 100644 index 67e649d869..0000000000 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua +++ /dev/null @@ -1,193 +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. - -local capabilities = require "st.capabilities" -local cc = require "st.zwave.CommandClass" -local constants = require "st.zwave.constants" -local json = require "dkjson" - -local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) -local user_id_status = UserCode.user_id_status -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local access_control_event = Notification.event.access_control -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 LockCodesDefaults = require "st.zwave.defaults.lockCodes" - -local SCHLAGE_MFR = 0x003B -local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} - -local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds - -local function can_handle_schlage_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SCHLAGE_MFR -end - -local function set_code_length(self, device, cmd) - local length = cmd.args.length - if length >= 4 and length <= 8 then - device:send(Configuration:Set({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = length, - size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size - })) - end -end - -local function reload_all_codes(self, device, cmd) - LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) - local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - if current_code_length == nil then - device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) - end -end - -local function set_code(self, device, cmd) - if (cmd.args.codePIN == "") then - self:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.nameSlot.NAME, - args = {cmd.args.codeSlot, cmd.args.codeName}, - }) - else - -- copied from defaults with additional check for Schlage's configuration - if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then - if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end - local code_state = device:get_field(constants.CODE_STATE) - code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName - device:set_field(constants.CODE_STATE, code_state, { persist = true }) - end - local send_set_user_code = function () - device:send(UserCode:Set({ - user_identifier = cmd.args.codeSlot, - user_code = cmd.args.codePIN, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - end - local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - 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, send_set_user_code) - else - send_set_user_code() - end - end -end - -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.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - if current_code_length ~= nil and current_code_length ~= reported_code_length then - local all_codes_deleted_mocked_command = Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.ALL_USER_CODES_DELETED - }) - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) - end - device:emit_event(capabilities.lockCodes.codeLength(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 code_id = cmd.args.user_identifier - if is_user_code_report_mfr_specific(device, cmd) then - local reported_user_id_status = cmd.args.user_id_status - local user_code = cmd.args.user_code - local event - - 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_name = LockCodesDefaults.get_code_name(device, code_id) - local change_type = LockCodesDefaults.get_change_type(device, code_id) - event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) - event.data = {codeName = code_name} - if code_id ~= 0 then -- ~= MASTER_CODE - LockCodesDefaults.code_set_event(device, code_id, code_name) - end - elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then - local lock_codes = LockCodesDefaults.get_lock_codes(device) - for _code_id, _ in pairs(lock_codes) do - LockCodesDefaults.code_deleted(device, _code_id) - end - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) - else -- user_id_status.STATUS_NOT_AVAILABLE - event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) - end - - if event ~= nil then - device:emit_event(event) - end - LockCodesDefaults.clear_code_state(device, code_id) - LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) - else - LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) - end -end - -local schlage_lock = { - capability_handlers = { - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, - [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, - [capabilities.lockCodes.commands.setCode.NAME] = set_code - } - }, - zwave_handlers = { - [cc.USER_CODE] = { - [UserCode.REPORT] = user_code_report_handler - }, - [cc.CONFIGURATION] = { - [Configuration.REPORT] = configuration_report - }, - [cc.BASIC] = { - [Basic.SET] = basic_set_handler - } - }, - lifecycle_handlers = { - doConfigure = do_configure, - }, - NAME = "Schlage Lock", - can_handle = can_handle_schlage_lock, -} - -return schlage_lock 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 d446289fc0..e99d204328 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 @@ -110,7 +110,7 @@ test.register_coroutine_test( 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(4, { 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 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 6edacbb690..0c177fbd67 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 @@ -29,6 +29,8 @@ local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) 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 + -- supported comand classes local zwave_lock_endpoints = { @@ -41,6 +43,9 @@ 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( { @@ -49,6 +54,62 @@ local mock_device = test.mock_device.build_test_zwave_device( } ) +-- 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) @@ -61,6 +122,11 @@ local function test_init() 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 } }))) + + -- reset these globals + test_credential_index = 1 + test_credentials = {} + test_users = {} end test.set_test_init_function(test_init) @@ -82,7 +148,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.users( {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, - { visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -91,7 +157,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.commandResult( { commandName = "addUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -108,7 +174,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.users( {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, - { visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -117,7 +183,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.commandResult( { commandName = "addUser", statusCode = "success", userIndex = 2 }, - { state_change = true, visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -127,120 +193,10 @@ test.register_coroutine_test( test.register_coroutine_test( "Add credential should succeed", function() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 1, "guest", "pin", "1234" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "Code 1" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) - ) - ) - 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:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.wait_for_events() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 2, "guest", "pin", "3456" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { - {userIndex = 1, userType = "guest", userName = "Code 1" }, - {userIndex = 2, userType = "guest", userName = "Code 2" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) - ) - ) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = 2, - 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, - UserCode:Report({ - user_identifier = 2, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({ - { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, - { userIndex = 2, credentialIndex = 2, credentialType = "pin" } - }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 2 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) + -- these all should succeed + add_credential(0) + add_credential(0) + add_credential(0) end ) @@ -252,15 +208,15 @@ test.register_coroutine_test( { capability = capabilities.lockUsers.ID, command = "addUser", - args = { "TestUser 1", "guest" } + args = { "Guest1", "guest" } }, }) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, - { visibility = { displayed = false } } + {{userIndex = 1, userType = "guest", userName = "Guest1" }}, + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -269,47 +225,14 @@ test.register_coroutine_test( "main", capabilities.lockUsers.commandResult( { commandName = "addUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) test.wait_for_events() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 1, "guest", "pin", "1234" } - }, - }) - 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:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) + + -- add credential with the new users index (1). + add_credential(1) end ) @@ -329,7 +252,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.users( {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, - { visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -338,7 +261,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.commandResult( { commandName = "addUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -355,7 +278,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.users( {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, - { visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -364,7 +287,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.commandResult( { commandName = "addUser", statusCode = "success", userIndex = 2 }, - { state_change = true, visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -382,7 +305,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.users( {{userIndex = 1, userType = "guest", userName = "new name" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, - { visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -391,7 +314,7 @@ test.register_coroutine_test( "main", capabilities.lockUsers.commandResult( { commandName = "updateUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -401,61 +324,10 @@ test.register_coroutine_test( test.register_coroutine_test( "Delete user should succeed", function() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 1, "guest", "pin", "1234" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "Code 1" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) - ) - ) - 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:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.wait_for_events() + -- add credential + add_credential(0) + + -- delete the user which should also delete the credential test.socket.capability:__queue_receive({ mock_device.id, { @@ -470,39 +342,30 @@ test.register_coroutine_test( user_id_status = UserCode.user_id_status.AVAILABLE }):build_test_tx(mock_device.id) ) - test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") 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)) test.socket.zwave:__queue_receive({mock_device.id, - UserCode:Report({ - user_identifier = 1, - user_id_status = UserCode.user_id_status.AVAILABLE + 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.lockCredentials.credentials({}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + capabilities.lockUsers.users( + {}, + { state_change = true, visibility = { displayed = true } } ) ) ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users( - {}, - { visibility = { displayed = false } } - ) + capabilities.lockCredentials.credentials( + {}, + { state_change = true, visibility = { displayed = true } }) ) ) test.socket.capability:__expect_send( @@ -510,104 +373,62 @@ test.register_coroutine_test( "main", capabilities.lockUsers.commandResult( { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + { state_change = true, visibility = { displayed = true } } ) ) ) + test.wait_for_events() end ) test.register_coroutine_test( "Update credential should succeed", function() + -- add credential + add_credential(0) + + -- update the credential test.socket.capability:__queue_receive({mock_device.id, { capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 1, "guest", "pin", "1234" } + command = "updateCredential", + args = { 1, 1, "pin", "3456" } }, }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "Code 1" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) - ) - ) test.socket.zwave:__expect_send( UserCode:Set({ user_identifier = 1, - user_code = "1234", + 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, - UserCode:Report({ - user_identifier = 1, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + 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.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + 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.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.wait_for_events() - 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, - UserCode:Report({ - user_identifier = 1, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + 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 }, - { state_change = true, visibility = { displayed = false } } + { commandName = "updateCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } ) ) ) @@ -617,61 +438,10 @@ test.register_coroutine_test( test.register_coroutine_test( "Delete credential should succeed", function() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 1, "guest", "pin", "1234" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "Code 1" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) - ) - ) - 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:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.wait_for_events() + -- add the credential + add_credential(0) + + -- -- delete the credential test.socket.capability:__queue_receive({mock_device.id, { capability = capabilities.lockCredentials.ID, @@ -686,166 +456,53 @@ test.register_coroutine_test( }):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.AVAILABLE + 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.lockCredentials.credentials({}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + capabilities.lockUsers.users( + {}, + { state_change = true, visibility = { displayed = true } } ) ) ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users( - {}, - { visibility = { displayed = false } } - ) + 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 = false } } + 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() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 1, "guest", "pin", "1234" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex = 1, userType = "guest", userName = "Code 1" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) - ) - ) - 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:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.wait_for_events() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 2, "guest", "pin", "3456" } - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { - {userIndex = 1, userType = "guest", userName = "Code 1" }, - {userIndex = 2, userType = "guest", userName = "Code 2" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) - ) - ) - test.socket.zwave:__expect_send( - UserCode:Set({ - user_identifier = 2, - 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, - UserCode:Report({ - user_identifier = 2, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({ - { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, - { userIndex = 2, credentialIndex = 2, credentialType = "pin" } - }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 2 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.wait_for_events() + -- 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, @@ -853,36 +510,8 @@ test.register_coroutine_test( args = {} }, }) - 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.lockUsers.commandResult( - { commandName = "deleteAllUsers", statusCode = "success" }, - { state_change = true, 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.commandResult( - { commandName = "deleteAllCredentials", statusCode = "success" }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) + + 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) @@ -900,67 +529,56 @@ test.register_coroutine_test( user_id_status = UserCode.user_id_status.AVAILABLE }):build_test_tx(mock_device.id) ) - end -) + test.wait_for_events() -test.register_coroutine_test( - "The lock reporting unlock via code should include the code number", - function() - test.socket.capability:__queue_receive({mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { 1, "guest", "pin", "1234" } - }, + 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 = 1, userType = "guest", userName = "Code 1" }}, - { visibility = { displayed = false } } - ) - ) - ) - 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 = false } } - ) + { + { userIndex = 2, userName = "Guest2", userType = "guest" } + }, + { state_change = true, visibility = { displayed = true } }) ) ) - 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:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + 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 = "addCredential", statusCode = "success", credentialIndex = 1 }, - { state_change = true, visibility = { displayed = false } } + 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() + -- add credential + add_credential(0) + -- send unlock test.socket.zwave:__queue_receive( { mock_device.id, diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua index fc171b493a..2dca011678 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -10,33 +10,15 @@ local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local access_control_event = Notification.event.access_control +local log = require "log" -- Helper methods - -local add_or_update = function(device, method, credential_index, user_index) - -- if so, add the credential to the list - local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - credentials[credential_index] = { userIndex = user_index, credentialIndex = credential_index, credentialType = "pin"} - -- emit credential event - device:emit_event(LockCredentials.credentials(credentials, { visibility = { displayed = false } })) - -- emit command success - device:emit_event(LockCredentials.commandResult( - { commandName = method, statusCode = lock_utils.STATUS_SUCCESS, credentialIndex = credential_index}, { state_change = true, visibility = { displayed = false } } - )) - -- set the ongoing operation field to nil - device:set_field(method..credential_index, nil) -end - --- returns the index of the lowest unset index less than the max -local next_empty_index = function(table, max) - local index = 1 - for i = 1, max + 1 do - if table[i] == nil then - index = i - break - end +local reload_all_codes = function(device) + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then + device:set_field(lock_utils.CHECKING_CODE, 1) end - return index + + device:send(UserCode:Get({user_identifier = device:get_field(lock_utils.CHECKING_CODE)})) end -- Lifecycle handlers @@ -45,252 +27,277 @@ local added_handler = function(driver, device) -- reload all codes end +local init = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(2, 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 -local add_user_handler = function(driver, device, cmd) - local user_name = cmd.args.userName - local user_type = cmd.args.userType - -- get the table of current users - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) - -- check that we can add a new user - local max_users = device:get_latest_state("main", LockUsers.ID, LockUsers.totalUsersSupported.NAME, 8) - if utils.table_size(users) == max_users then - -- we cannot create a new user (unlikely!) + if status == lock_utils.STATUS_SUCCESS then + lock_utils.send_events(device, lock_utils.LOCK_USERS) end - -- find the index to add the user at - local index = next_empty_index(users, max_users) - -- insert the user into the table - users[index] = {userIndex = index, userName = user_name, userType = user_type} - -- emit the users table event - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- emit the command result event - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } - )) + + lock_utils.clear_busy_state(device, status) end -local update_user_handler = function(driver, device, cmd) - local index = cmd.args.userIndex - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) - -- does the user index already exist? - -- if not, update the user (offset user index by 1) - if users[index] ~= nil then - -- insert the user into the table - users[index] = {userIndex = index, userName = cmd.args.userName, userType = cmd.args.userType} - -- emit the users table event - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- emit the command result event - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.UPDATE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } - )) +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 -end -local delete_user_handler = function(driver, device, cmd) - local index = cmd.args.userIndex - -- make sure the user exists - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) - if users[index] ~= nil then - -- see if the user is associated with a lock code - local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - for _, credential in pairs(credentials) do - if credential.userIndex == index then - -- if so, delete that code - device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) - -- save state for receipt of delete - device:set_field("_delete_credential"..credential.credentialIndex, index) - -- make sure delete went through - device.thread:call_with_delay(4.2, function(d) device:send(UserCode:Get({user_identifier = credential.credentialIndex})) end) - return -- if the user has a credential, we need confirmation that the code was deleted before proceeding - 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) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + status = lock_utils.STATUS_SUCCESS + break end - -- delete user from the list - users[index] = nil - -- emit users event - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- emit user delete success - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } - )) end + + lock_utils.clear_busy_state(device, status) end -local delete_all_users_handler = function(driver, device, cmd) - -- TODO: Z-Wave User Code v2 includes mass sets/gets that could be leveraged to make this simpler - -- delete every user - -- send users event - device:emit_event(LockUsers.users({}, { visibility = { displayed = false}})) - -- send success event - device:emit_event(LockUsers.commandResult({ commandName = lock_utils.DELETE_ALL_USERS, statusCode = lock_utils.STATUS_SUCCESS }, { state_change = true, visibility = { displayed = false}})) +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 credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - -- delete every credential local delay = 0 - for _, credential in pairs(credentials) do - device.thread:call_with_delay(delay, function(d) - device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) + 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) - -- include a delay between deletes - delay = delay + .5 + delay = delay + 2 end - -- send credentials event - device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false}})) - -- send success event (this would be tedious to check for every code, so assume they all went through) - device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { state_change = true, visibility = { displayed = false}})) + + 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, cmd) - local index = cmd.args.userIndex - local user_type = cmd.args.userType - local credential_type = cmd.args.credentialType -- if this is not "pin", send an error - local data = cmd.args.credentialData - -- get the table of current credentials - local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - -- does the user index already exist? - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) - -- if not, create a new user (offset user index by 1) - if users[index] == nil then - -- insert the user into the table - users[index] = {userIndex = index, userName = "Code "..index, userType = user_type} - -- emit the users table event - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- emit the command result event - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } - )) +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 - -- find the index to add the credential at - local max_credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.pinUsersSupported.NAME, 8) - local credential_index = next_empty_index(credentials, max_credentials) - -- save some state so we can complete the transaction on message receipt - device:set_field(lock_utils.ADD_CREDENTIAL..credential_index, index) - -- send the credential creation message - device:send(UserCode:Set({ - user_identifier = credential_index, - user_code = data, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) -end -local update_credential_handler = function(driver, device, cmd) - -- validate args - local user_index = cmd.args.userIndex - local credential_index = cmd.args.credentialIndex - local credential_type = cmd.args.credentialType - local data = cmd.args.credentialData - -- make sure credential already exists - local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) - if credentials[credential_index] ~= nil and users[user_index] ~= nil then - -- store state to track update - device:set_field(lock_utils.UPDATE_CREDENTIAL..credential_index, user_index) - -- send command to update code + 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 = data, + user_code = credential_data, user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + -- clearing busy state handled in user_code_report_handler else - -- failure + lock_utils.clear_busy_state(device, status) end end -local delete_credential_handler = function(driver, device, cmd) - -- find the user associated with this credential - local user_index = cmd.args.credentialIndex - -- run delete user with that credential - driver:inject_capability_command(device, { - capability = capabilities.lockUsers.ID, - command = capabilities.lockUsers.commands.deleteUser.NAME, - args = { user_index } - }) +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 status = lock_utils.STATUS_SUCCESS + 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 + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status) + end end -local delete_all_credentials_handler = function(driver, device, cmd) - -- check to see if we have users that do not have a code associated - local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) - local user_index_marked_for_individual_deletion = {} +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 - for _, credential in pairs(credentials) do - users[credential.userIndex] = nil - user_index_marked_for_individual_deletion[credential.userIndex] = true + local credential_index = tonumber(command.args.credentialIndex) + local status = lock_utils.STATUS_SUCCESS + 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 + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status, command.override_busy_check) end - -- if we don't, this is equivalent to delete_all_users - if utils.table_size(users) == 0 then - driver:inject_capability_command(device, { - capability = capabilities.lockUsers.ID, - command = capabilities.lockUsers.commands.deleteAllUsers.NAME, - args = { } - }) +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 - -- if we do, delete all users other than those - for i, _ in pairs(user_index_marked_for_individual_deletion) do - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = i }, { state_change = true, visibility = { displayed = false } } - )) - end - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- and then delete all codes + local credentials = lock_utils.get_credentials(device) + local status = lock_utils.STATUS_SUCCESS local delay = 0 for _, credential in pairs(credentials) do - device.thread.call_with_delay(delay, function(d) - device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) - end) - -- include a delay between deletes - delay = delay + .5 + 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 - -- send credentials event (these will be sent before all the actual deletes have been sent) - device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false}})) - -- send success event (this would be tedious to check for every code, so assume they all went through) - device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { visibility = { displayed = false}})) -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 code_id = cmd.args.user_identifier + local credential_index = cmd.args.user_identifier + local command = device:get_field(lock_utils.COMMAND_NAME) local user_id_status = cmd.args.user_id_status - - -- is this a report about an occupied credential index? 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 - -- are we in the middle of a user code set for this index? - local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..code_id) - local user_index_update = device:get_field(lock_utils.UPDATE_CREDENTIAL..code_id) - if user_index_add ~= nil then - add_or_update(device, lock_utils.ADD_CREDENTIAL, code_id, user_index_add) - elseif user_index_update ~= nil then - add_or_update(device, lock_utils.UPDATE_CREDENTIAL, code_id, user_index_update) + -- 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 end elseif user_id_status == UserCode.user_id_status.AVAILABLE then - -- are we in the middle of a user code delete? - local user_index = device:get_field("_delete_credential"..code_id) - if user_index ~= nil then - -- if so, delete the credential - local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - credentials[code_id] = nil - -- emit credential event - device:emit_event(LockCredentials.credentials(credentials, { visibility = { displayed = false } })) - -- emit command success - device:emit_event(LockCredentials.commandResult( - { commandName = lock_utils.DELETE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS, credentialIndex = code_id }, { state_change = true, visibility = { displayed = false } } - )) - -- delete the user - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) - users[user_index] = nil - -- emit users event - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- emit command success - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index }, { state_change = true, visibility = { displayed = false } } - )) - -- clear state - device:set_field("_delete_credential"..code_id, nil) + -- 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_event = true + end + end + if emit_event then + lock_utils.send_events(device) + 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_event = 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 end @@ -298,107 +305,82 @@ end local notification_report_handler = function(driver, device, cmd) if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then local event = cmd.args.event - local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) - local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.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_NAME) + local emit_event = false + if (event == access_control_event.ALL_USER_CODES_DELETED) then - -- this is unexpected, but we got this out of band, so... - -- check to see if we have users that do not have a code associated - local user_index_marked_for_individual_deletion = {} - for _, credential in pairs(credentials) do - users[credential.userIndex] = nil - user_index_marked_for_individual_deletion[credential.userIndex] = true - end - -- if we don't, this is equivalent to delete_all_users - if utils.table_size(users) == 0 then - driver:inject_capability_command(device, { - capability = capabilities.lockUsers.ID, - command = capabilities.lockUsers.commands.deleteAllUsers.NAME, - args = { } - }) - return - end - -- if we do, delete all users other than those - for i, _ in pairs(user_index_marked_for_individual_deletion) do - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = i }, { state_change = true, visibility = { displayed = false } } - )) + -- all credentials have been deleted + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + emit_event = true end - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- emit empty credentials - device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false } })) - device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { visibility = { displayed = false}})) elseif (event == access_control_event.SINGLE_USER_CODE_DELETED) then - local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) - -- find the user index assigned to this code to delete it as well - local credential = credentials[credential_index] - if credential ~= nil then - -- we may want to check if these match - local stored_user_index = device:get_field("_delete_credential"..credential_index) - local user_index = credential.userIndex - local user = users[user_index] - if user ~= nil then - users[user_index] = nil - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index }, { state_change = true, visibility = { displayed = false } } - )) - device:set_field("_delete_credential"..credential_index, nil) - end - else - -- something bad happened + -- credential has been deleted. + 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 - local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) - -- determine if this is due to a command or an out-of-band update - local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..credential_index) - local user_index_update = device:get_field(lock_utils.UPDATE_CREDENTIAL..credential_index) - if user_index_add ~= nil then - add_or_update(device, lock_utils.ADD_CREDENTIAL, credential_index, user_index_add) - elseif user_index_update ~= nil then - add_or_update(device, lock_utils.UPDATE_CREDENTIAL, credential_index, user_index_update) + 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 - -- out-of-band update - -- create a user for this code index - local max_users = device:get_latest_state("main", LockUsers.ID, LockUsers.totalUsersSupported.NAME, 8) - -- find the index to add the user at - local index = next_empty_index(users, max_users) - -- insert the user into the table - users[index] = {userIndex = index, userName = "Code "..index, userType = "guest"} - -- emit the users table event - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - -- emit the command result event - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } - )) - -- add the credential - add_or_update(device, lock_utils.ADD_CREDENTIAL, credential_index, index) + -- 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) + emit_event = true + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end end elseif (event == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE) then - local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) - -- this is a create code failure - -- double check we have a stored add command - local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..credential_index) - -- clear that state - device:set_field(lock_utils.ADD_CREDENTIAL..credential_index, nil) - -- emit a credential add failure - device:emit_event(LockCredentials.commandResult( - { commandName = lock_utils.ADD_CREDENTIAL, statusCode = "duplicate", credentialIndex = credential_index }, { state_change = true, visibility = { displayed = false } } - )) - -- if we have a stored add command, we should delete the associated user, I think - if users[user_index_add] ~= nil then - users[user_index_add] = nil - device:emit_event(LockUsers.commandResult( - { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index_add }, { state_change = true, visibility = { displayed = false } } - )) - device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) - end + -- adding credential failed since code already exists. + -- 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) elseif (event == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION) then - -- master code changed - -- we might want to fire a credential updated success here? - + -- master code changed -- should we send an index with this? + device:emit_event(capabilities.lockCredentials.commandResult( + {commandName = lock_utils.UPDATE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS}, + { state_change = true, visibility = { displayed = true } } + )) + end - -- these are all the lock operation events - elseif (event >= access_control_event.MANUAL_LOCK_OPERATION and event <= access_control_event.LOCK_JAMMED) then + -- handle emitting events if any changes occured. + if emit_event 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, status) + end + + ------------ LOCK OPERATION EVENTS ------------ + if (event >= access_control_event.MANUAL_LOCK_OPERATION and event <= access_control_event.LOCK_JAMMED) then local event_to_send local METHOD = { @@ -449,10 +431,10 @@ local notification_report_handler = function(driver, device, cmd) 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 - if (credentials ~= nil and - credentials[code_id] ~= nil) then - user_id = credentials[code_id].userIndex + local user_id = nil + local credential = 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 @@ -485,13 +467,23 @@ 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, { visibility = { displayed = false } })) - device:emit_event(LockCredentials.pinUsersSupported(cmd.args.supported_users, { visibility = { displayed = false } })) + 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 = { lifecycle_handlers = { added = added_handler, + init = init, }, zwave_handlers = { [cc.NOTIFICATION] = { @@ -514,7 +506,17 @@ local zwave_lock = { [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.zwave-alarm-v1-lock"), + require("using-new-capabilities.schlage-lock"), + require("using-new-capabilities.samsung-lock"), + require("using-new-capabilities.keywe-lock"), }, NAME = "Using new capabilities", can_handle = function(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua index 43a22cc713..0ce07bf4ae 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua @@ -14,14 +14,13 @@ local utils = require "st.utils" local capabilities = require "st.capabilities" local json = require "st.json" -local LockCredentials = capabilities.lockCredentials -local LockUsers = capabilities.lockUsers 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", @@ -31,7 +30,7 @@ local new_lock_utils = { DELETE_USER = "deleteUser", LOCK_CREDENTIALS = "lockCredentials", LOCK_USERS = "lockUsers", - PENDING_CREDENTIAL = "pendingCredential", + ACTIVE_CREDENTIAL = "pendingCredential", STATUS_BUSY = "busy", STATUS_DUPLICATE = "duplicate", STATUS_FAILURE = "failure", @@ -46,13 +45,89 @@ local new_lock_utils = { 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, {}) + device:set_field(new_lock_utils.LOCK_USERS, users) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) +end + new_lock_utils.get_users = function(device) local users = 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 ipairs(new_lock_utils.get_users(device)) do + for _, user in pairs(new_lock_utils.get_users(device)) do if user.userIndex == user_index then return user end @@ -61,18 +136,26 @@ new_lock_utils.get_user = function(device, user_index) return nil end -new_lock_utils.get_available_user_index = function(current_data, max) - if current_data == nil and max ~= 0 then - return INITIAL_INDEX - elseif current_data ~= nil then +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 current_data["user" .. index] == nil then - return index + if used_index[index] == nil then + available_index = index + break end end + else + available_index = INITIAL_INDEX end - - return nil + return available_index end new_lock_utils.get_credentials = function(device) @@ -81,7 +164,7 @@ new_lock_utils.get_credentials = function(device) end new_lock_utils.get_credential = function(device, credential_index) - for _, credential in ipairs(new_lock_utils.get_credentials(device)) do + for _, credential in pairs(new_lock_utils.get_credentials(device)) do if credential.credentialIndex == credential_index then return credential end @@ -89,15 +172,26 @@ new_lock_utils.get_credential = function(device, credential_index) return nil end -new_lock_utils.get_available_credential_index = function(current_data, max) +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 i, _ in ipairs(current_data) do - used_index[i] = true + for _, credential in pairs(current_credentials) do + used_index[credential.credentialIndex] = true end - - if current_data ~= {} then + if current_credentials ~= {} then for index = 1, max do if used_index[index] == nil then available_index = index @@ -107,69 +201,37 @@ new_lock_utils.get_available_credential_index = function(current_data, max) else available_index = INITIAL_INDEX end - return available_index end new_lock_utils.create_user = function(device, user_name, user_type, user_index) - local status_code = new_lock_utils.STATUS_SUCCESS - local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, - capabilities.lockUsers.totalUsersSupported.NAME, 0) - local current_users = new_lock_utils.get_users(device) - local available_index = new_lock_utils.get_available_user_index(current_users, max_users) - - if max_users == 0 or available_index == nil then - -- Can't add any users - update commandResult statusCode - status_code = new_lock_utils.STATUS_RESOURCE_EXHAUSTED - else - -- use the passed in index if it's set - if user_index ~= nil then - available_index = user_index - end - current_users["user"..available_index] = { userIndex = available_index, userType = user_type, userName = user_name } - device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) + if user_name == nil then + user_name = "Guest" .. user_index end - return status_code + 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) end -new_lock_utils.delete_user = function(device, user_index, deleted_by_credential_deletion) +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 - -- also delete associated credential if this isn't being call by a credential deletion. - if not deleted_by_credential_deletion then - -- find associated credential. - for _, credential in ipairs(new_lock_utils.get_credentials(device)) do - if credential.userIndex == user_index then - new_lock_utils.delete_credential(device, credential.credentialIndex, true) - break - end - end - end - -- table.remove(current_users, index) + -- 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, user_type, credential_type, credential_index) - -- need to also create a user if one does not exist at the user index. - if new_lock_utils.get_user(device, user_index) == nil then - local user_name = "USER_" .. user_index - local status = new_lock_utils.create_user(device, user_name, user_type, user_index) - if status ~= new_lock_utils.STATUS_SUCCESS then - return status - end - 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 }) @@ -177,17 +239,16 @@ new_lock_utils.add_credential = function(device, user_index, user_type, credenti return new_lock_utils.STATUS_SUCCESS end -new_lock_utils.delete_credential = function(device, credential_index, deleted_by_user_deletion) +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 - -- also delete associated user if this isn't being called by a user deletion. - if not deleted_by_user_deletion then - new_lock_utils.delete_user(device, credential.userIndex, true) - end - table.remove(credentials, index) + 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 @@ -201,7 +262,7 @@ new_lock_utils.update_credential = function(device, credential_index, user_index local credentials = new_lock_utils.get_credentials(device) local status_code = new_lock_utils.STATUS_FAILURE - for _, credential in ipairs(credentials) do + for _, credential in pairs(credentials) do if credential.credentialIndex == credential_index then credential.credentialType = credential_type credential.userIndex = user_index @@ -213,6 +274,30 @@ new_lock_utils.update_credential = function(device, credential_index, user_index 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 diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua index 08cd518841..847636dd30 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -147,9 +147,7 @@ local using_old_capabilities = { }, capability_handlers = { [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes - }, - [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes, [capabilities.lockCodes.commands.migrate.NAME] = migrate }, }, 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 deleted file mode 100644 index 44d978999b..0000000000 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua +++ /dev/null @@ -1,165 +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. - -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 }) ---- @type st.zwave.defaults.lockCodes -local lock_code_defaults = require "st.zwave.defaults.lockCodes" -local json = require "dkjson" - -local METHOD = { - KEYPAD = "keypad", - MANUAL = "manual", - 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 -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 -end - ---- 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 lock_codes = lock_code_defaults.get_lock_codes(device) - local code_id = nil - if (cmd.args.alarm_level ~= nil) then - code_id = tostring(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 (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} - end - elseif (alarm_type == 18) then - event = capabilities.lock.lock.locked() - if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - event.data = {codeId = code_id, codeName = code_name, 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 user codes deleted - for code_id, _ in pairs(lock_codes) do - lock_code_defaults.code_deleted(device, code_id) - end - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) - elseif (alarm_type == 33) then - -- user code deleted - if (code_id ~= nil) then - lock_code_defaults.clear_code_state(device, code_id) - if (lock_codes[code_id] ~= nil) then - lock_code_defaults.code_deleted(device, code_id) - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) - end - end - elseif (alarm_type == 13 or alarm_type == 112) then - -- user code changed/set - if (code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - local change_type = lock_code_defaults.get_change_type(device, code_id) - local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) - code_changed_event["data"] = { codeName = code_name} - lock_code_defaults.code_set_event(device, code_id, code_name) - lock_code_defaults.clear_code_state(device, code_id) - device:emit_event(code_changed_event) - end - elseif (alarm_type == 34 or alarm_type == 113) then - -- duplicate lock code - if (code_id ~= nil) then - local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) - lock_code_defaults.clear_code_state(device, code_id) - device:emit_event(code_changed_event) - 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 = can_handle_v1_alarm, -} - -return zwave_lock From 95279be40eec4795492b9773323a9f98bd41cd7f Mon Sep 17 00:00:00 2001 From: Pegor Date: Wed, 24 Dec 2025 09:57:14 -0800 Subject: [PATCH 06/16] Add table load check --- drivers/SmartThings/zigbee-lock/src/init.lua | 9 --- .../zigbee-lock/src/new_lock_utils.lua | 26 +++++--- .../test_zigbee_lock_new_capabilities.lua | 60 ++++++++++--------- .../src/using-new-capabilities/init.lua | 20 ++----- .../src/using-new-capabilities/yale/init.lua | 10 +--- .../src/using-old-capabilities/init.lua | 2 - drivers/SmartThings/zwave-lock/src/init.lua | 4 +- .../test_zwave_lock_code_slga_migration.lua | 25 +++----- .../test/test_zwave_lock_new_capabilities.lua | 18 ++---- .../src/using-new-capabilities/can_handle.lua | 10 ++++ .../src/using-new-capabilities/init.lua | 49 ++++++++++----- .../using-new-capabilities/new_lock_utils.lua | 25 ++++++-- .../src/using-old-capabilities/init.lua | 12 +++- 13 files changed, 146 insertions(+), 124 deletions(-) create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 8f321efc68..9e70ea25fc 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -14,27 +14,18 @@ -- Zigbee Driver utilities local defaults = require "st.zigbee.defaults" -local device_management = require "st.zigbee.device_management" local ZigbeeDriver = require "st.zigbee" -- 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" diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua index f13b00eec7..0d05bc22be 100644 --- a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -11,7 +11,6 @@ -- 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. -local utils = require "st.utils" local capabilities = require "st.capabilities" local INITIAL_INDEX = 1 @@ -37,6 +36,7 @@ local new_lock_utils = { STATUS_OCCUPIED = "occupied", STATUS_RESOURCE_EXHAUSTED = "resourceExhausted", STATUS_SUCCESS = "success", + TABLES_LOADED = "tablesLoaded", UPDATE_CREDENTIAL = "updateCredential", UPDATE_USER = "updateUser", USER_INDEX = "userIndex", @@ -50,8 +50,7 @@ new_lock_utils.busy_check_and_set = function (device, command, override_busy_che if override_busy_check then -- the function was called by an injected command. return false - end - + end local c_time = os.time() local busy_state = device:get_field(new_lock_utils.BUSY) or false @@ -107,21 +106,30 @@ 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) 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, {}) - device:set_field(new_lock_utils.LOCK_USERS, users) - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + + 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 = device:get_field(new_lock_utils.LOCK_USERS) return users ~= nil and users or {} end @@ -159,6 +167,10 @@ new_lock_utils.get_available_user_index = function(device) 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 = device:get_field(new_lock_utils.LOCK_CREDENTIALS) return credentials ~= nil and credentials or {} end diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua index ccd69baa54..d23423983d 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua @@ -44,11 +44,11 @@ end local function init_migration() test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report( - mock_device, 4) }) + mock_device, 4) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(4, { visibility = { displayed = false } }))) test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report( - mock_device, 8) }) + mock_device, 8) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(8, { visibility = { displayed = false } }))) test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported @@ -88,7 +88,7 @@ local function add_default_users() }, }) -- add to the user list that is now expected - table.insert(user_list, {userIndex = i, userType = "guest", userName = "Guest" .. i }) + table.insert(user_list, { userIndex = i, userType = "guest", userName = "Guest" .. i }) test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -103,7 +103,7 @@ local function add_default_users() mock_device:generate_test_message( "main", capabilities.lockUsers.commandResult( - { commandName = "addUser", statusCode = "success", userIndex = i}, + { commandName = "addUser", statusCode = "success", userIndex = i }, { state_change = true, visibility = { displayed = true } } ) ) @@ -114,11 +114,11 @@ end local function add_credential(user_index, credential_data) test.socket.capability:__queue_receive({ mock_device.id, - { - capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { user_index, "guest", "pin", credential_data } - }, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { user_index, "guest", "pin", credential_data } + }, }) test.socket.zigbee:__expect_send( { @@ -152,8 +152,10 @@ local function add_credential(user_index, credential_data) ) } ) - table.insert(test_credentials, { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) - table.insert(test_users, {userIndex = test_credential_index, userName = "Guest" .. test_credential_index, userType = "guest"}) + table.insert(test_credentials, + { userIndex = test_credential_index, credentialIndex = test_credential_index, credentialType = "pin" }) + 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", @@ -163,14 +165,16 @@ local function add_credential(user_index, credential_data) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockCredentials.credentials(test_credentials, { state_change = true, visibility = { displayed = true } }) + 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 }, + { commandName = "addCredential", statusCode = "success", credentialIndex = test_credential_index, userIndex = + test_credential_index }, { state_change = true, visibility = { displayed = true } } ) ) @@ -229,15 +233,15 @@ test.register_coroutine_test( }) local users = { - { userIndex = 1, userName = "Guest1", userType = "guest" }, + { userIndex = 1, userName = "Guest1", userType = "guest" }, { userIndex = 2, userName = "ChangeUserName", userType = "guest" }, - { userIndex = 3, userName = "Guest3", userType = "guest" }, - { userIndex = 4, userName = "Guest4", userType = "guest" }, + { userIndex = 3, userName = "Guest3", userType = "guest" }, + { userIndex = 4, userName = "Guest4", userType = "guest" }, } test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users(users, { state_change=true, visibility = { displayed = true } }) + capabilities.lockUsers.users(users, { state_change = true, visibility = { displayed = true } }) ) ) test.socket.capability:__expect_send( @@ -288,7 +292,7 @@ test.register_coroutine_test( args = { "3" } }, }) - + local users = { { userIndex = 1, userName = "Guest1", userType = "guest" }, { userIndex = 2, userName = "Guest2", userType = "guest" }, @@ -298,7 +302,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users(users, { state_change=true, visibility = { displayed = true } }) + capabilities.lockUsers.users(users, { state_change = true, visibility = { displayed = true } }) ) ) test.socket.capability:__expect_send( @@ -396,7 +400,7 @@ test.register_coroutine_test( } ) test.wait_for_events() - test.socket.zigbee:__queue_receive( + test.socket.zigbee:__queue_receive( { mock_device.id, DoorLock.client.commands.GetPINCodeResponse.build_test_rx( @@ -477,14 +481,14 @@ test.register_coroutine_test( { capability = capabilities.lockCredentials.ID, command = "deleteCredential", - args = { "1", "pin"} + args = { "1", "pin" } }, }) test.socket.zigbee:__expect_send({ - mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) + mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) test.socket.zigbee:__expect_send({ - mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) + mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.mock_time.advance_time(2) @@ -495,7 +499,7 @@ test.register_coroutine_test( } ) test.wait_for_events() - test.socket.zigbee:__queue_receive({ + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.client.commands.GetPINCodeResponse.build_test_rx( mock_device, @@ -558,20 +562,20 @@ test.register_coroutine_test( args = {} }, }) - + test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot") test.socket.zigbee:__expect_send({ - mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) + mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.zigbee:__expect_send({ - mock_device.id,DoorLock.server.commands.GetPINCode(mock_device, 1) + mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) test.wait_for_events() test.mock_time.advance_time(2) - test.socket.zigbee:__queue_receive({ + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.client.commands.GetPINCodeResponse.build_test_rx( mock_device, diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 2279078d1c..6c489aa615 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -13,9 +13,7 @@ -- limitations under the License. -- Zigbee Driver utilities -local defaults = require "st.zigbee.defaults" local device_management = require "st.zigbee.device_management" -local ZigbeeDriver = require "st.zigbee" local log = require "log" local utils = require "st.utils" @@ -155,9 +153,8 @@ local delete_user_handler = function(driver, device, command) return end local status = lock_utils.STATUS_SUCCESS - local user_index = tonumber(command.args.userIndex) + 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 @@ -261,7 +258,6 @@ local update_credential_handler = function(driver, device, command) end local credential_index = tonumber(command.args.credentialIndex) local credential_data = command.args.credentialData - local status = lock_utils.STATUS_SUCCESS local credential = lock_utils.get_credential(device, credential_index) if credential ~= nil then @@ -277,8 +273,7 @@ local update_credential_handler = function(driver, device, command) device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) end) else - status = lock_utils.STATUS_FAILURE - lock_utils.clear_busy_state(device, status) + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) end end @@ -288,7 +283,6 @@ local delete_credential_handler = function(driver, device, command) end local credential_index = tonumber(command.args.credentialIndex) - local status = lock_utils.STATUS_SUCCESS local credential = lock_utils.get_credential(device, credential_index) if credential ~= nil then if command.override_busy_check == nil then @@ -302,8 +296,7 @@ local delete_credential_handler = function(driver, device, command) device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) end) else - status = lock_utils.STATUS_FAILURE - lock_utils.clear_busy_state(device, status, command.override_busy_check) + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) end end @@ -354,7 +347,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) 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, + lock_utils.add_credential(device, active_credential.userIndex, active_credential.credentialType, credential_index) @@ -384,7 +377,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) 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. + -- 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) @@ -413,7 +406,6 @@ local get_pin_response_handler = function(driver, device, zb_mess) 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) @@ -467,7 +459,7 @@ end -- REMOVE THIS AFTER DONE WITH TESTING local migrate = function(driver, device, value) - log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. + 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" .. diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua index eb2b4f89e2..c5ff08fcbe 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua @@ -18,13 +18,9 @@ local LockCluster = clusters.DoorLock -- Capabilities local capabilities = require "st.capabilities" -local LockCredentials = capabilities.lockCredentials -local log = require "log" -local utils = require "st.utils" -- Enums local UserStatusEnum = LockCluster.types.DrlkUserStatus -local UserTypeEnum = LockCluster.types.DrlkUserType local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode local SHIFT_INDEX_CHECK = 256 @@ -43,7 +39,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) 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, + lock_utils.add_credential(device, active_credential.userIndex, active_credential.credentialType, credential_index) @@ -74,7 +70,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) 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. + -- 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) @@ -105,7 +101,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), { state_change = true, visibility = { displayed = true } })) 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) diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua index a235b469d7..0af1877760 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -13,9 +13,7 @@ -- limitations under the License. -- Zigbee Driver utilities -local defaults = require "st.zigbee.defaults" local device_management = require "st.zigbee.device_management" -local ZigbeeDriver = require "st.zigbee" -- Zigbee Spec Utils local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 653f99e3f3..83a889e7d4 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -72,8 +72,8 @@ local driver_template = { } }, sub_drivers = { - require("using-new-capabilities"), - require("using-old-capabilities"), + lazy_load_if_possible("using-old-capabilities"), + lazy_load_if_possible("using-new-capabilities"), } } 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 e99d204328..69393f1004 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 @@ -16,17 +16,10 @@ 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 Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local t_utils = require "integration_test.utils" -local zw_test_utils = require "integration_test.zwave_test_utils" -local utils = require "st.utils" -local json = require "dkjson" --- @type st.zwave.constants local constants = require "st.zwave.constants" @@ -93,11 +86,11 @@ test.register_coroutine_test( 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.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { state_change = true, visibility = { displayed = true } }))) 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 } }))) + 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"}}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) end ) @@ -108,11 +101,11 @@ test.register_coroutine_test( 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.credentials({}, { state_change=true, visibility = { displayed = true } }))) 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 } }))) + 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.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) end ) @@ -132,11 +125,11 @@ test.register_coroutine_test( 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.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { state_change = true, visibility = { displayed = true } }))) 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 } }))) + 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"}}, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) end ) 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 0c177fbd67..47f6e8f2b2 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 @@ -15,20 +15,11 @@ 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" local access_control_event = Notification.event.access_control @@ -117,11 +108,11 @@ local function test_init() 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.credentials({}, { state_change = true, visibility = { displayed = true } }))) 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 } }))) + 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.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) -- reset these globals test_credential_index = 1 @@ -387,7 +378,7 @@ test.register_coroutine_test( -- add credential add_credential(0) - -- update the credential + -- update the credential test.socket.capability:__queue_receive({mock_device.id, { capability = capabilities.lockCredentials.ID, @@ -510,7 +501,6 @@ test.register_coroutine_test( args = {} }, }) - test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot") test.timer.__create_and_queue_test_time_advance_timer(0.5, "oneshot") 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..ad2116d67d --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua @@ -0,0 +1,10 @@ +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 index 2dca011678..2826a9d17b 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -23,8 +23,19 @@ 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 end local init = function(driver, device) @@ -86,7 +97,7 @@ local delete_user_handler = function(driver, device, command) return end local status = lock_utils.STATUS_SUCCESS - local user_index = tonumber(command.args.userIndex) + local user_index = tonumber(command.args.userIndex) if lock_utils.get_user(device, user_index) ~= nil then if command.override_busy_check == nil then @@ -191,7 +202,6 @@ local update_credential_handler = function(driver, device, command) end local credential_index = tonumber(command.args.credentialIndex) local credential_data = command.args.credentialData - local status = lock_utils.STATUS_SUCCESS local credential = lock_utils.get_credential(device, credential_index) if credential ~= nil then @@ -203,8 +213,7 @@ local update_credential_handler = function(driver, device, command) user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) -- clearing busy state handled in user_code_report_handler else - status = lock_utils.STATUS_FAILURE - lock_utils.clear_busy_state(device, status) + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) end end @@ -214,7 +223,6 @@ local delete_credential_handler = function(driver, device, command) end local credential_index = tonumber(command.args.credentialIndex) - local status = lock_utils.STATUS_SUCCESS local credential = lock_utils.get_credential(device, credential_index) if credential ~= nil then if command.override_busy_check == nil then @@ -227,8 +235,7 @@ local delete_credential_handler = function(driver, device, command) })) -- clearing busy state handled in user_code_report_handler else - status = lock_utils.STATUS_FAILURE - lock_utils.clear_busy_state(device, status, command.override_busy_check) + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) end end @@ -259,9 +266,11 @@ 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 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. + -- 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 @@ -278,12 +287,9 @@ local user_code_report_handler = function(driver, device, cmd) if lock_utils.get_credential(device, credential_index) ~= nil then -- Credential has been deleted. lock_utils.delete_credential(device, credential_index) - emit_event = true + emit_events = true end end - if emit_event then - lock_utils.send_events(device) - end -- checking code handler if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then @@ -293,13 +299,17 @@ local user_code_report_handler = function(driver, device, cmd) 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_event = true + 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 end local notification_report_handler = function(driver, device, cmd) @@ -327,7 +337,7 @@ local notification_report_handler = function(driver, device, cmd) 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, + lock_utils.add_credential(device, active_credential.userIndex, active_credential.credentialType, credential_index) @@ -378,7 +388,7 @@ local notification_report_handler = function(driver, device, cmd) 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 - + ------------ LOCK OPERATION EVENTS ------------ if (event >= access_control_event.MANUAL_LOCK_OPERATION and event <= access_control_event.LOCK_JAMMED) then local event_to_send @@ -473,7 +483,7 @@ end -- REMOVE THIS AFTER DONE WITH TESTING local migrate = function(driver, device, value) - log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. + 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" .. @@ -481,6 +491,13 @@ local migrate = function(driver, device, value) end local zwave_lock = { + supported_capabilities = { + capabilities.lock, + capabilities.lockUsers, + capabilities.lockCredentials, + capabilities.battery, + capabilities.tamperAlert + }, lifecycle_handlers = { added = added_handler, init = init, diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua index 0ce07bf4ae..a42765a65c 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua @@ -11,9 +11,7 @@ -- 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. -local utils = require "st.utils" local capabilities = require "st.capabilities" -local json = require "st.json" local INITIAL_INDEX = 1 local new_lock_utils = { @@ -38,6 +36,7 @@ local new_lock_utils = { STATUS_OCCUPIED = "occupied", STATUS_RESOURCE_EXHAUSTED = "resourceExhausted", STATUS_SUCCESS = "success", + TABLES_LOADED = "tablesLoaded", UPDATE_CREDENTIAL = "updateCredential", UPDATE_USER = "updateUser", USER_INDEX = "userIndex", @@ -51,7 +50,7 @@ new_lock_utils.busy_check_and_set = function (device, command, override_busy_che if override_busy_check then -- the function was called by an injected command. return false - end + end local c_time = os.time() local busy_state = device:get_field(new_lock_utils.BUSY) or false @@ -108,7 +107,7 @@ 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) @@ -117,11 +116,21 @@ 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, {}) - device:set_field(new_lock_utils.LOCK_USERS, users) - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + 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 = device:get_field(new_lock_utils.LOCK_USERS) return users ~= nil and users or {} end @@ -159,6 +168,10 @@ new_lock_utils.get_available_user_index = function(device) 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 = device:get_field(new_lock_utils.LOCK_CREDENTIALS) return credentials ~= nil and credentials or {} end diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua index 847636dd30..3388939a62 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -133,14 +133,20 @@ local function migrate(driver, device, cmd) 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.credentials(lock_credentials, { state_change = true, visibility = { displayed = true } })) 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 } })) + device:emit_event(capabilities.lockUsers.users(lock_users, { state_change = true, visibility = { displayed = true } })) + device:emit_event(capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) end local using_old_capabilities = { + supported_capabilities = { + capabilities.lock, + capabilities.lockCodes, + capabilities.battery, + capabilities.tamperAlert + }, lifecycle_handlers = { init = init_handler, added = added_handler, From 27a6a011f19e323fac37631e1c2656130d7006a3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 6 Jan 2026 19:07:21 -0800 Subject: [PATCH 07/16] Z-Wave Lock: first pass at sub-drivers --- drivers/SmartThings/zigbee-lock/src/init.lua | 22 +-- .../src/using-new-capabilities/init.lua | 20 -- drivers/SmartThings/zwave-lock/src/init.lua | 6 +- .../new_lock_utils.lua | 173 ++++++++++++++++++ .../zwave-lock/src/test/test_keywe_lock.lua | 20 ++ .../zwave-lock/src/test/test_schlage_lock.lua | 19 ++ .../test/test_zwave_lock_new_capabilities.lua | 6 +- .../src/using-new-capabilities/init.lua | 170 +---------------- .../keywe-lock/init.lua | 7 +- .../samsung-lock/init.lua | 53 +----- .../schlage-lock/init.lua | 114 ++---------- .../zwave-alarm-v1-lock/init.lua | 104 +++++++---- .../src/using-old-capabilities/init.lua | 7 +- 13 files changed, 332 insertions(+), 389 deletions(-) rename drivers/SmartThings/zwave-lock/src/{using-new-capabilities => }/new_lock_utils.lua (57%) diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 9e70ea25fc..deffdaad93 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -29,6 +29,7 @@ local LockCodes = capabilities.lockCodes local socket = require "cosock.socket" local lock_utils = require "lock_utils" +local new_lock_utils = require "new_lock_utils" local DELAY_LOCK_EVENT = "_delay_lock_event" local MAX_DELAY = 10 @@ -77,28 +78,19 @@ local function device_added(driver, device) if device.useOldCapabilityForTesting == nil then if device:supports_capability_by_id(LockCodes.ID) then device:emit_event(LockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) - if device.device_added ~= nil then - -- make the driver call this command again, it will now be handled in new capabilities. - driver.lifecycle_handlers.device_added(driver, device) - end + new_lock_utils.reload_tables(device) else lock_utils.populate_state_from_data(device) - driver:inject_capability_command(device, { - capability = capabilities.refresh.ID, - command = capabilities.refresh.commands.refresh.NAME, - args = {} - }) end else lock_utils.populate_state_from_data(device) - - driver:inject_capability_command(device, { - capability = capabilities.refresh.ID, - command = capabilities.refresh.commands.refresh.NAME, - args = {} - }) end + driver:inject_capability_command(device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) end local function init(driver, device) diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 6c489aa615..006e7b8ba2 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -64,22 +64,6 @@ local reload_all_codes = function(device) device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) end -local refresh = function(driver, device, cmd) - device:refresh() - device:send(LockCluster.attributes.LockState:read(device)) - device:send(Alarm.attributes.AlarmCount:read(device)) -end - -local device_added = function(driver, device) - lock_utils.reload_tables(device) - - driver:inject_capability_command(device, { - capability = capabilities.refresh.ID, - command = capabilities.refresh.commands.refresh.NAME, - args = {} - }) -end - local init = function(driver, device) lock_utils.reload_tables(device) device.thread:call_with_delay(2, function(d) @@ -567,9 +551,6 @@ local new_capabilities_driver = { [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, }, - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = refresh - }, [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING [capabilities.lockCodes.commands.migrate.NAME] = migrate, @@ -583,7 +564,6 @@ local new_capabilities_driver = { }, health_check = false, lifecycle_handlers = { - added = device_added, init = init, doConfigure = do_configure }, diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 83a889e7d4..a2d235f225 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -23,9 +23,9 @@ local defaults = require "st.zwave.defaults" local lazy_load_if_possible = function(sub_driver_name) -- gets the current lua libs api version local version = require "version" - if version.api >= 16 then - return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) - elseif version.api >= 9 then + -- if version.api >= 16 then + -- return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + if version.api >= 9 then return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) else return require(sub_driver_name) diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua similarity index 57% rename from drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua rename to drivers/SmartThings/zwave-lock/src/new_lock_utils.lua index a42765a65c..bc65678d92 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua @@ -322,4 +322,177 @@ new_lock_utils.get_code_id_from_notification_event = function(event_params, v1_a 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 + new_lock_utils.delete_user(device, active_credential.userIndex) + 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/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index d8e8ecc1bc..4d44352cfb 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -32,6 +32,10 @@ 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.yml"), @@ -42,9 +46,25 @@ 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) + 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 } }))) + + -- reset these globals + test_credential_index = 1 + test_credentials = {} + test_users = {} end + test.set_test_init_function(test_init) test.register_coroutine_test( 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 b1a5964502..067b84eb63 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -50,10 +50,29 @@ local mock_device = test.mock_device.build_test_zwave_device( } ) +local test_credential_index = 1 +local test_credentials = {} +local test_users = {} + 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) + 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 } }))) + + -- reset these globals + test_credential_index = 1 + test_credentials = {} + test_users = {} end test.set_test_init_function(test_init) 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 47f6e8f2b2..4b561b685f 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 @@ -108,11 +108,11 @@ local function test_init() 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({}, { state_change = true, visibility = { displayed = true } }))) + 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({}, { state_change = true, visibility = { displayed = true } }))) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) + 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 } }))) -- reset these globals test_credential_index = 1 diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua index 2826a9d17b..2419dd4817 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -1,7 +1,7 @@ local capabilities = require "st.capabilities" local LockUsers = capabilities.lockUsers local LockCredentials = capabilities.lockCredentials -local lock_utils = require "using-new-capabilities.new_lock_utils" +local lock_utils = require "new_lock_utils" local utils = require "st.utils" --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -9,8 +9,8 @@ local cc = require "st.zwave.CommandClass" local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local access_control_event = Notification.event.access_control local log = require "log" +local TamperDefaults = require "st.zwave.defaults.tamperAlert" -- Helper methods local reload_all_codes = function(device) @@ -313,166 +313,16 @@ local user_code_report_handler = function(driver, device, cmd) end local notification_report_handler = function(driver, device, cmd) - if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then - local event = cmd.args.event - 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_NAME) - local emit_event = false - - if (event == access_control_event.ALL_USER_CODES_DELETED) then - -- all credentials have been deleted - 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 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 == 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 - -- 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) - emit_event = true - else - 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 = lock_utils.STATUS_DUPLICATE - lock_utils.delete_user(device, active_credential.userIndex) - 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 = lock_utils.UPDATE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS}, - { state_change = true, visibility = { displayed = true } } - )) - end + ------------ USER CODE PROGRAMMING EVENTS ------------ + lock_utils.base_driver_code_event_handler(driver, device, cmd) - -- handle emitting events if any changes occured. - if emit_event 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, status) - end + ------------ LOCK OPERATION EVENTS ------------ + lock_utils.door_operation_event_handler(driver, device, cmd) - ------------ LOCK OPERATION EVENTS ------------ - 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 = 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 + ------------ 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) 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 index d39aa45d1c..16ec64f6f6 100644 --- 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 @@ -19,9 +19,8 @@ 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 lock_utils = require "new_lock_utils" local KEYWE_MFR = 0x037B local TAMPER_CLEAR_DELAY = 10 @@ -54,8 +53,8 @@ local function notification_report_handler(self, device, cmd) 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) + 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, 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 index 813c6217b4..ac3abb4572 100644 --- 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 @@ -16,17 +16,9 @@ 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 json = require "dkjson" -local constants = require "st.zwave.constants" - -local LockDefaults = require "st.zwave.defaults.lock" -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 lock_utils = require "new_lock_utils" local SAMSUNG_MFR = 0x022E @@ -34,56 +26,26 @@ local function can_handle_samsung_lock(opts, self, device, cmd, ...) return device.zwave_manufacturer_id == SAMSUNG_MFR end -local function get_ongoing_code_set(device) - local code_id - local code_state = device:get_field(constants.CODE_STATE) - if code_state ~= nil then - for key, state in pairs(code_state) do - if state ~= nil then - code_id = key:match("setName(%d)") - end - end - end - return code_id -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.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then event = capabilities.lock.lock.unlocked() - elseif event_code == access_control_event.NEW_USER_CODE_ADDED then - local code_id = get_ongoing_code_set(device) - if code_id ~= nil then - device:send(UserCode:Get({user_identifier = code_id})) - return - end - elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then - local code_id = get_ongoing_code_set(device) - if code_id ~= nil then - event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) - clear_code_state(device, code_id) - end elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then - -- Update Master Code in the same way as in defaults... - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - -- ...and delete rest of them, as lock does - local lock_codes = get_lock_codes(device) - for code_id, _ in pairs(lock_codes) do - if code_id ~= "0" then - code_deleted(device, code_id) - end + -- 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 - event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) + lock_utils.send_events(device) 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) + lock_utils.door_operation_event_handler(self, device, cmd) + lock_utils.base_driver_code_event_handler(self, device, cmd) end end @@ -92,7 +54,6 @@ 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()) - device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) end local samsung_lock = { 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 index 67e649d869..efcf0cf0e8 100644 --- 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 @@ -15,78 +15,22 @@ local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local constants = require "st.zwave.constants" -local json = require "dkjson" local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) local user_id_status = UserCode.user_id_status -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local access_control_event = Notification.event.access_control 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 LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local lock_utils = require "new_lock_utils" local SCHLAGE_MFR = 0x003B local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} -local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds - local function can_handle_schlage_lock(opts, self, device, cmd, ...) return device.zwave_manufacturer_id == SCHLAGE_MFR end -local function set_code_length(self, device, cmd) - local length = cmd.args.length - if length >= 4 and length <= 8 then - device:send(Configuration:Set({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = length, - size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size - })) - end -end - -local function reload_all_codes(self, device, cmd) - LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) - local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - if current_code_length == nil then - device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) - end -end - -local function set_code(self, device, cmd) - if (cmd.args.codePIN == "") then - self:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.nameSlot.NAME, - args = {cmd.args.codeSlot, cmd.args.codeName}, - }) - else - -- copied from defaults with additional check for Schlage's configuration - if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then - if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end - local code_state = device:get_field(constants.CODE_STATE) - code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName - device:set_field(constants.CODE_STATE, code_state, { persist = true }) - end - local send_set_user_code = function () - device:send(UserCode:Set({ - user_identifier = cmd.args.codeSlot, - user_code = cmd.args.codePIN, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - end - local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - 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, send_set_user_code) - else - send_set_user_code() - end - end -end - 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}})) @@ -101,15 +45,16 @@ 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.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + 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 - local all_codes_deleted_mocked_command = Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.ALL_USER_CODES_DELETED - }) - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) + -- 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.lockCodes.codeLength(reported_code_length)) + device:emit_event(capabilities.lockCredentials.minPinCodeLen(reported_code_length)) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(reported_code_length)) end end @@ -129,49 +74,24 @@ local function is_user_code_report_mfr_specific(device, cmd) end local function user_code_report_handler(self, device, cmd) - local code_id = cmd.args.user_identifier + 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 - local user_code = cmd.args.user_code - local event - 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_name = LockCodesDefaults.get_code_name(device, code_id) - local change_type = LockCodesDefaults.get_change_type(device, code_id) - event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) - event.data = {codeName = code_name} - if code_id ~= 0 then -- ~= MASTER_CODE - LockCodesDefaults.code_set_event(device, code_id, code_name) + 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 - elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then - local lock_codes = LockCodesDefaults.get_lock_codes(device) - for _code_id, _ in pairs(lock_codes) do - LockCodesDefaults.code_deleted(device, _code_id) - end - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) - else -- user_id_status.STATUS_NOT_AVAILABLE - event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) - end - - if event ~= nil then - device:emit_event(event) + lock_utils.send_events(device) end - LockCodesDefaults.clear_code_state(device, code_id) - LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) else - LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + local new_capabilities = require "using-new-capabilities" + new_capabilities.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) end end local schlage_lock = { - capability_handlers = { - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, - [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, - [capabilities.lockCodes.commands.setCode.NAME] = set_code - } - }, zwave_handlers = { [cc.USER_CODE] = { [UserCode.REPORT] = user_code_report_handler 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 index 44d978999b..8f539a366c 100644 --- 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 @@ -19,9 +19,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 }) ---- @type st.zwave.defaults.lockCodes -local lock_code_defaults = require "st.zwave.defaults.lockCodes" -local json = require "dkjson" +local lock_utils = require "new_lock_utils" local METHOD = { KEYPAD = "keypad", @@ -49,24 +47,31 @@ end local function alarm_report_handler(driver, device, cmd) local alarm_type = cmd.args.alarm_type local event = nil - local lock_codes = lock_code_defaults.get_lock_codes(device) - local code_id = nil + local credential_index = nil if (cmd.args.alarm_level ~= nil) then - code_id = tostring(cmd.args.alarm_level) + 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 (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + if (code_id ~= 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 (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + if (code_id ~= 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() @@ -94,37 +99,62 @@ local function alarm_report_handler(driver, device, cmd) event = capabilities.lock.lock.locked() event["data"] = {method = METHOD.AUTO} elseif (alarm_type == 32) then - -- all user codes deleted - for code_id, _ in pairs(lock_codes) do - lock_code_defaults.code_deleted(device, code_id) + -- all credentials have been deleted + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) end - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + lock_utils.send_events(device) elseif (alarm_type == 33) then - -- user code deleted - if (code_id ~= nil) then - lock_code_defaults.clear_code_state(device, code_id) - if (lock_codes[code_id] ~= nil) then - lock_code_defaults.code_deleted(device, code_id) - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) - end + -- 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 - -- user code changed/set - if (code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - local change_type = lock_code_defaults.get_change_type(device, code_id) - local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) - code_changed_event["data"] = { codeName = code_name} - lock_code_defaults.code_set_event(device, code_id, code_name) - lock_code_defaults.clear_code_state(device, code_id) - device:emit_event(code_changed_event) + 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 - -- duplicate lock code - if (code_id ~= nil) then - local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) - lock_code_defaults.clear_code_state(device, code_id) - device:emit_event(code_changed_event) + -- 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) + lock_utils.delete_user(device, active_credential.userIndex) + 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 diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua index 3388939a62..80523c02bb 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -133,11 +133,11 @@ local function migrate(driver, device, cmd) 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, { state_change = true, visibility = { displayed = true } })) + 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, { state_change = true, visibility = { displayed = true } })) - device:emit_event(capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) + 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 = { @@ -164,7 +164,6 @@ local using_old_capabilities = { require("using-old-capabilities.keywe-lock"), }, can_handle = function(opts, driver, device, ...) - if not device:supports_capability_by_id(capabilities.lockCodes.ID) then return false end local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) if not lock_codes_migrated then From 032e9504f779b5280bc2bf201cf949e2809706d6 Mon Sep 17 00:00:00 2001 From: Pegor Date: Wed, 7 Jan 2026 14:35:30 -0800 Subject: [PATCH 08/16] add zigbee yale new capabilities unit test --- .../test_zigbee_yale-new_capabilities.lua | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua new file mode 100644 index 0000000000..5ae8773a20 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua @@ -0,0 +1,177 @@ +-- 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. + +-- Mock out globals +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local json = require "dkjson" + +local PowerConfiguration = clusters.PowerConfiguration +local Alarm = clusters.Alarms + +local DoorLock = clusters.DoorLock +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zigbee_endpoints = { + [1] = { id = 1, manufacturer = "Yale", server_clusters = { 0x0001 } } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_device) + 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, { state_change = true, visibility = { displayed = true } }))) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read( + mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) +end + +test.set_test_init_function(test_init) + +local expect_reload_all_codes_messages = function() + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, + true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MinPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfTotalUsersSupported:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) +end + +test.register_coroutine_test( + "Configure should configure all necessary attributes and begin reading codes", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining + :configure_reporting(mock_device, + 600, + 21600, + 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + DoorLock.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:configure_reporting(mock_device, + 0, + 3600, + 0) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + Alarm.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:configure_reporting(mock_device, + 0, + 21600, + 0) }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + test.mock_time.advance_time(2) + expect_reload_all_codes_messages() + end +) + +test.register_coroutine_test( + "Adding a credential should succeed and report users, credentials, and command result.", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report( + mock_device, 4) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported + :build_test_attr_report(mock_device, 4) }) + 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.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.wait_for_events() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234" } } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + } + ) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + 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({{credentialIndex=1, credentialType="pin", userIndex=1}}, + { 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.run_registered_tests() From 6425a8c141ec655cbaf854e9cdf94976e4213e61 Mon Sep 17 00:00:00 2001 From: Pegor Date: Wed, 7 Jan 2026 15:44:02 -0800 Subject: [PATCH 09/16] add more zigbee yale test coverage --- .../test_zigbee_yale-new_capabilities.lua | 162 +++++++++++++++--- 1 file changed, 140 insertions(+), 22 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua index 5ae8773a20..c0545891b5 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua @@ -27,6 +27,8 @@ local Alarm = clusters.Alarms local DoorLock = clusters.DoorLock local DoorLockUserStatus = DoorLock.types.DrlkUserStatus local DoorLockUserType = DoorLock.types.DrlkUserType +local ProgrammingEventCode = DoorLock.types.ProgramEventCode + local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("base-lock.yml"), @@ -36,8 +38,19 @@ local mock_device = test.mock_device.build_test_zigbee_device({ }) zigbee_test_utils.prepare_zigbee_env_info() -local function test_init() + +local function test_init_default() test.mock_device.add_test_device(mock_device) + 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, { state_change = true, visibility = { displayed = true } }))) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read( + mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) +end + +local function test_init_add_device() test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", @@ -46,9 +59,20 @@ local function test_init() mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) + + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report( + mock_device, 4) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported + :build_test_attr_report(mock_device, 4) }) + 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.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) end -test.set_test_init_function(test_init) +test.set_test_init_function(test_init_default) local expect_reload_all_codes_messages = function() test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, @@ -102,17 +126,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Adding a credential should succeed and report users, credentials, and command result.", function() - test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report( - mock_device, 4) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) - test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported - :build_test_attr_report(mock_device, 4) }) - 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.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) - test.wait_for_events() test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234" } } }) test.socket.zigbee:__expect_send( @@ -152,26 +165,131 @@ test.register_coroutine_test( 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 } }) + 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({{credentialIndex=1, credentialType="pin", userIndex=1}}, + capabilities.lockCredentials.credentials({ { credentialIndex = 1, credentialType = "pin", userIndex = 1 } }, { 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 } } - ) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = true } } ) + ) ) - end + end, + { + test_init = function() + test_init_add_device() + end + } +) + +test.register_message_test( + "The lock reporting a single code has been set and then deleted should be handled", + { + -- add credential + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_ADDED, + 1, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockUsers.users({ { userIndex = 1, userName = "Guest1", userType = "guest" } }, + { state_change = true, visibility = { displayed = true } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ { credentialIndex = 1, credentialType = "pin", userIndex = 1 } }, + { state_change = true, visibility = { displayed = true } })) + }, + + -- delete the credential + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 1, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + 0x0000, + "data" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } })) + } + }, + { test_init = test_init_add_device } +) + +test.register_message_test( + "The lock reporting master code changed", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.MASTER_CODE_CHANGED + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult({ commandName = "updateCredential", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } })) + } + } ) test.run_registered_tests() From 890902e985cccc2fb5a2319be99f069d84668ea3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 7 Jan 2026 22:14:03 -0800 Subject: [PATCH 10/16] updates to z-wave sub-drivers and tests --- .../test_zigbee_yale-new_capabilities.lua | 268 +++++++++++++++++- .../lock-without-codes/init.lua | 2 +- .../samsungsds/init.lua | 2 +- .../yale-fingerprint-lock/init.lua | 2 +- .../src/using-new-capabilities/yale/init.lua | 2 +- .../yale/yale-bad-battery-reporter/init.lua | 2 +- drivers/SmartThings/zwave-lock/src/init.lua | 74 ++++- .../zwave-lock/src/lazy_load_subdriver.lua | 15 + .../zwave-lock/src/new_lock_utils.lua | 2 +- .../zwave-lock/src/sub_drivers.lua | 9 + .../zwave-lock/src/test/test_keywe_lock.lua | 21 +- .../test/test_keywe_lock_new_capabilities.lua | 130 +++++++++ .../zwave-lock/src/test/test_lock_battery.lua | 3 +- .../zwave-lock/src/test/test_samsung_lock.lua | 3 +- .../test_samsung_lock_new_capabilities.lua | 230 +++++++++++++++ .../zwave-lock/src/test/test_schlage_lock.lua | 21 +- .../zwave-lock/src/test/test_zwave_lock.lua | 9 +- .../test/test_zwave_lock_new_capabilities.lua | 131 ++++++++- .../src/using-new-capabilities/init.lua | 44 ++- .../keywe-lock/can_handle.lua | 11 + .../keywe-lock/init.lua | 7 +- .../samsung-lock/can_handle.lua | 11 + .../samsung-lock/init.lua | 17 +- .../schlage-lock/can_handle.lua | 11 + .../schlage-lock/init.lua | 7 +- .../using-new-capabilities/sub_drivers.lua | 11 + .../zwave-alarm-v1-lock/can_handle.lua | 10 + .../zwave-alarm-v1-lock/init.lua | 11 +- .../src/using-old-capabilities/can_handle.lua | 3 + .../src/using-old-capabilities/init.lua | 40 +-- .../keywe-lock/can_handle.lua | 11 + .../keywe-lock/init.lua | 7 +- .../samsung-lock/can_handle.lua | 11 + .../samsung-lock/init.lua | 8 +- .../schlage-lock/can_handle.lua | 11 + .../schlage-lock/init.lua | 7 +- .../using-old-capabilities/sub_drivers.lua | 11 + .../zwave-alarm-v1-lock/can_handle.lua | 10 + .../zwave-alarm-v1-lock/init.lua | 11 +- 39 files changed, 1012 insertions(+), 184 deletions(-) create mode 100644 drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-lock/src/sub_drivers.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/using-new-capabilities/keywe-lock/can_handle.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/schlage-lock/can_handle.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-old-capabilities/keywe-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua index c0545891b5..d7db3f443d 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. @@ -19,7 +19,6 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils" local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" -local json = require "dkjson" local PowerConfiguration = clusters.PowerConfiguration local Alarm = clusters.Alarms @@ -82,6 +81,16 @@ local expect_reload_all_codes_messages = function() test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfTotalUsersSupported:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + "" + ) + }) end test.register_coroutine_test( @@ -193,6 +202,155 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Updating a credential should succeed and report users, credentials, and command result.", + function() + -- add credential first + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234" } } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + } + ) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + 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({ { credentialIndex = 1, credentialType = "pin", userIndex = 1 } }, + { 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.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.mock_time.advance_time(4) + test.wait_for_events() + + -- update the credential + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { "1", "1", "pin", "changedPin123" } + }, + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "changedPin123" + ) + } + ) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "abc123" + ) + } + ) + 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.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() + end, + { + test_init = function() + test_init_add_device() + end + } +) + test.register_message_test( "The lock reporting a single code has been set and then deleted should be handled", { @@ -292,4 +450,110 @@ test.register_message_test( } ) +test.register_message_test( + "The lock reporting all codes have been deleted should be handled", + { + -- add a credential + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_ADDED, + 1, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockUsers.users({ { userIndex = 1, userName = "Guest1", userType = "guest" } }, + { state_change = true, visibility = { displayed = true } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ { credentialIndex = 1, credentialType = "pin", userIndex = 1 } }, + { state_change = true, visibility = { displayed = true } })) + }, + + -- delete all credentials + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 0xFFFF + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } })) + } + }, + { test_init = test_init_add_device } +) + +test.register_coroutine_test( + "Out of band get pin call should add credential if it doesn't exist (happens during reload all codes).", + function() + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + 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({ { credentialIndex = 1, credentialType = "pin", userIndex = 1 } }, + { state_change = true, visibility = { displayed = true } }) + ) + ) + end, + { + test_init = function() + test_init_add_device() + end + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua index 7272991459..d89cc01580 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua index 6e16d6339e..03802b26ef 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua index c5e22b43f7..4e74bbe79f 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua index c5ff08fcbe..3b70c910f4 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua index 59fdbf228b..8b3528c4c0 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index a2d235f225..d9d5c8ee63 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -20,18 +20,6 @@ local ZwaveDriver = require "st.zwave.driver" --- @type st.zwave.defaults local defaults = require "st.zwave.defaults" -local lazy_load_if_possible = function(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - -- if version.api >= 16 then - -- return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) - if version.api >= 9 then - return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end -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 }) @@ -39,6 +27,62 @@ 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 + ) + end +end + +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 + +local do_added = function(driver, device) + -- this variable should only be present for test cases trying to test the old capabilities. + if device.useOldCapabilityForTesting == nil then + 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 + else + -- 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(d) + 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 + end +end + local function time_get_handler(driver, device, cmd) local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) local time = os.date("*t") @@ -61,6 +105,9 @@ local driver_template = { capabilities.battery, capabilities.tamperAlert }, + lifecycle_handlers = { + added = do_added + }, capability_handlers = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh @@ -72,8 +119,7 @@ local driver_template = { } }, sub_drivers = { - lazy_load_if_possible("using-old-capabilities"), - lazy_load_if_possible("using-new-capabilities"), + 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 new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- 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 version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(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 index bc65678d92..366c6a451f 100644 --- a/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua @@ -384,7 +384,7 @@ new_lock_utils.base_driver_code_event_handler = function(driver, device, cmd) -- 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 - new_lock_utils.delete_user(device, active_credential.userIndex) + 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( diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..e7ed53082b --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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"), + lazy_load_if_possible("using-new-capabilities"), +} +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 4d44352cfb..d58c1be66b 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -32,37 +32,20 @@ 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.yml"), 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) - 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 } }))) - - -- reset these globals - test_credential_index = 1 - test_credentials = {} - test_users = {} end test.set_test_init_function(test_init) 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..9e56531652 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_new_capabilities.lua @@ -0,0 +1,130 @@ +-- Copyright 2026 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. + +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 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) + 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}}))) +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 +) + +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 +) + +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"}})) + }, + -- not a special case for this lock, should be handled as usual + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Notification:Report({notification_type = 6, event = 6, event_parameter = "\x01"}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}})) + } + } +) + +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 9aac02c6b2..e8d7df0e72 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -52,7 +52,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 7707b8a850..8e21d8f690 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -30,7 +30,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..7f10041e0a --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua @@ -0,0 +1,230 @@ +-- Copyright 2026 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. + +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, + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) + 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}}))) +end +test.set_test_init_function(test_init) + +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() + 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() + 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() + 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() + 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 067b84eb63..65dc07a036 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -46,33 +46,16 @@ 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 test_credential_index = 1 -local test_credentials = {} -local test_users = {} - 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) - 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 } }))) - - -- reset these globals - test_credential_index = 1 - test_credentials = {} - test_users = {} end test.set_test_init_function(test_init) 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 3ee38c115b..2799ee7d73 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -43,10 +43,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() 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 4b561b685f..99e2aaf7d2 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 @@ -19,6 +19,10 @@ local zw = require "st.zwave" 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 @@ -104,16 +108,37 @@ end -- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) - 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:__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 } }))) + + 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) + -- ) + -- test.socket.zwave:__expect_send( + -- UserCode:Get({user_identifier = 1}):build_test_tx(mock_device.id) + -- ) + -- reset these globals test_credential_index = 1 test_credentials = {} @@ -587,4 +612,94 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "When the device is added it should be set up and start reading codes", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + UserCode:Get({user_identifier = 1}):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:__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.zwave:__expect_send( + UserCode:Get({user_identifier = 2}):build_test_tx(mock_device.id) + ) + end +) + +test.register_coroutine_test( + "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.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.run_registered_tests() \ 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 index 2419dd4817..7102c1299e 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -14,6 +14,12 @@ 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 @@ -36,6 +42,7 @@ local added_handler = function(driver, device) 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) @@ -265,6 +272,7 @@ end 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 @@ -281,6 +289,20 @@ local user_code_report_handler = function(driver, device, cmd) 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. @@ -310,6 +332,12 @@ local user_code_report_handler = function(driver, device, cmd) 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) @@ -380,22 +408,10 @@ local zwave_lock = { }, }, sub_drivers = { - require("using-new-capabilities.zwave-alarm-v1-lock"), - require("using-new-capabilities.schlage-lock"), - require("using-new-capabilities.samsung-lock"), - require("using-new-capabilities.keywe-lock"), + require("using-new-capabilities.sub_drivers") }, NAME = "Using new capabilities", - can_handle = function(opts, driver, device, ...) - if not device:supports_capability_by_id(LockUsers.ID) then return false end - 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 + 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..84b85b85f5 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 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 index 16ec64f6f6..6e779862f7 100644 --- 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 @@ -22,13 +22,8 @@ local access_control_event = Notification.event.access_control local TamperDefaults = require "st.zwave.defaults.tamperAlert" local lock_utils = require "new_lock_utils" -local KEYWE_MFR = 0x037B local TAMPER_CLEAR_DELAY = 10 -local function can_handle_keywe_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == KEYWE_MFR -end - 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 @@ -79,7 +74,7 @@ local keywe_lock = { doConfigure = do_configure }, NAME = "Keywe Lock", - can_handle = can_handle_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..35c6566776 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 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 index ac3abb4572..a3b9c11177 100644 --- 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 @@ -16,28 +16,31 @@ 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 SAMSUNG_MFR = 0x022E - -local function can_handle_samsung_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SAMSUNG_MFR -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.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 @@ -66,7 +69,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = can_handle_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..109d4f43d1 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 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 index efcf0cf0e8..3ff09fc4f5 100644 --- 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 @@ -24,13 +24,8 @@ local Association = (require "st.zwave.CommandClass.Association")({version=1}) local lock_utils = require "new_lock_utils" -local SCHLAGE_MFR = 0x003B local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} -local function can_handle_schlage_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SCHLAGE_MFR -end - 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}})) @@ -107,7 +102,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = can_handle_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..4bbdcbfcea --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-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-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..ea09d0024e --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 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 index 8f539a366c..eb64d79f65 100644 --- 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 @@ -28,15 +28,6 @@ local METHOD = { 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 -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 -end - --- Default handler for alarm command class reports, these were largely OEM-defined --- --- This converts alarm V1 reports to correct lock events @@ -189,7 +180,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = can_handle_v1_alarm, + 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 index 8151871ac3..1fe9815bb9 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua @@ -1,3 +1,6 @@ +-- 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, diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua index 80523c02bb..995202181e 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -40,30 +40,6 @@ local init_handler = function(driver, device, event) device:set_field(constants.CODE_STATE, nil, { persist = true }) end ---- Builds up initial state for the device ---- ---- @param self st.zwave.Driver ---- @param device st.zwave.Device -local function added_handler(self, device) - 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 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 -end - --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @param cmd table @@ -149,7 +125,6 @@ local using_old_capabilities = { }, lifecycle_handlers = { init = init_handler, - added = added_handler, }, capability_handlers = { [capabilities.lockCodes.ID] = { @@ -158,20 +133,9 @@ local using_old_capabilities = { }, }, sub_drivers = { - require("using-old-capabilities.zwave-alarm-v1-lock"), - require("using-old-capabilities.schlage-lock"), - require("using-old-capabilities.samsung-lock"), - require("using-old-capabilities.keywe-lock"), + require("using-old-capabilities.sub_drivers") }, - can_handle = 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") - return true, subdriver - end - return false - end, + can_handle = require("using-old-capabilities.can_handle"), NAME = "Using old capabilities" } diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua new file mode 100644 index 0000000000..16c3de85ac --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 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-old-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-old-capabilities/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua index d39aa45d1c..0bafa6c14a 100644 --- 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 @@ -23,13 +23,8 @@ local LockDefaults = require "st.zwave.defaults.lock" local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local TamperDefaults = require "st.zwave.defaults.tamperAlert" -local KEYWE_MFR = 0x037B local TAMPER_CLEAR_DELAY = 10 -local function can_handle_keywe_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == KEYWE_MFR -end - 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 @@ -80,7 +75,7 @@ local keywe_lock = { doConfigure = do_configure }, NAME = "Keywe Lock", - can_handle = can_handle_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/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua new file mode 100644 index 0000000000..c44172664e --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 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-old-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-old-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua index 813c6217b4..e082275e98 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua @@ -28,12 +28,6 @@ local get_lock_codes = LockCodesDefaults.get_lock_codes local clear_code_state = LockCodesDefaults.clear_code_state local code_deleted = LockCodesDefaults.code_deleted -local SAMSUNG_MFR = 0x022E - -local function can_handle_samsung_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SAMSUNG_MFR -end - local function get_ongoing_code_set(device) local code_id local code_state = device:get_field(constants.CODE_STATE) @@ -105,7 +99,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = can_handle_samsung_lock, + can_handle = require("using-old-capabilities.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/using-old-capabilities/schlage-lock/can_handle.lua new file mode 100644 index 0000000000..be2ec41d75 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 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-old-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-old-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua index 67e649d869..17c280bb71 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua @@ -27,15 +27,10 @@ local Association = (require "st.zwave.CommandClass.Association")({version=1}) local LockCodesDefaults = require "st.zwave.defaults.lockCodes" -local SCHLAGE_MFR = 0x003B local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds -local function can_handle_schlage_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SCHLAGE_MFR -end - local function set_code_length(self, device, cmd) local length = cmd.args.length if length >= 4 and length <= 8 then @@ -187,7 +182,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = can_handle_schlage_lock, + 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/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua new file mode 100644 index 0000000000..19b1bd1131 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 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-old-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/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua index 44d978999b..4d21e6c794 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua @@ -30,15 +30,6 @@ local METHOD = { 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 -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 -end - --- Default handler for alarm command class reports, these were largely OEM-defined --- --- This converts alarm V1 reports to correct lock events @@ -159,7 +150,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = can_handle_v1_alarm, + can_handle = require("using-old-capabilities.zwave-alarm-v1-lock.can_handle") } return zwave_lock From 7a8439aa7b9ab60e8dd8c19aaaa4f3464ad9a0dd Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 8 Jan 2026 12:59:35 -0800 Subject: [PATCH 11/16] finish sub-driver unit tests --- drivers/SmartThings/zwave-lock/src/init.lua | 23 +- .../zwave-lock/src/lazy_load_subdriver.lua | 6 +- .../zwave-lock/src/new_lock_utils.lua | 16 +- .../zwave-lock/src/test/test_keywe_lock.lua | 15 +- .../test/test_keywe_lock_new_capabilities.lua | 15 +- .../zwave-lock/src/test/test_lock_battery.lua | 15 +- .../zwave-lock/src/test/test_samsung_lock.lua | 15 +- .../test_samsung_lock_new_capabilities.lua | 15 +- .../zwave-lock/src/test/test_schlage_lock.lua | 15 +- .../test_schlage_lock_new_capabilities.lua | 254 ++++++++++++++++++ .../zwave-lock/src/test/test_zwave_lock.lua | 15 +- .../test_zwave_lock_code_slga_migration.lua | 35 +-- .../test/test_zwave_lock_new_capabilities.lua | 15 +- .../src/using-new-capabilities/can_handle.lua | 3 + .../src/using-new-capabilities/init.lua | 3 + .../keywe-lock/can_handle.lua | 2 +- .../keywe-lock/init.lua | 15 +- .../samsung-lock/can_handle.lua | 2 +- .../samsung-lock/init.lua | 15 +- .../schlage-lock/can_handle.lua | 2 +- .../schlage-lock/init.lua | 35 ++- .../using-new-capabilities/sub_drivers.lua | 2 +- .../zwave-alarm-v1-lock/can_handle.lua | 2 +- .../zwave-alarm-v1-lock/init.lua | 19 +- .../src/using-old-capabilities/init.lua | 35 +-- .../keywe-lock/init.lua | 15 +- .../samsung-lock/init.lua | 15 +- .../schlage-lock/init.lua | 15 +- .../zwave-alarm-v1-lock/init.lua | 15 +- 29 files changed, 343 insertions(+), 306 deletions(-) create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index d9d5c8ee63..77dff1fe01 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -47,12 +36,6 @@ local function periodic_codes_state_verification(driver, device) end end -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 - local do_added = function(driver, device) -- this variable should only be present for test cases trying to test the old capabilities. if device.useOldCapabilityForTesting == nil then @@ -69,7 +52,7 @@ local do_added = function(driver, device) args = {} }) device.thread:call_with_delay( SCAN_CODES_CHECK_INTERVAL, - function(d) + function() periodic_codes_state_verification(driver, device) end ) diff --git a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua index 0bee6d2a75..db3fd691cf 100644 --- a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua +++ b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua @@ -4,11 +4,11 @@ return function(sub_driver_name) -- gets the current lua libs api version local version = require "version" - local ZigbeeDriver = require "st.zigbee" + local ZwaveDriver = require "st.zwave.driver" if version.api >= 16 then - return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) elseif version.api >= 9 then - return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) else return require(sub_driver_name) end diff --git a/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua index 366c6a451f..05d08fc063 100644 --- a/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zwave-lock/src/new_lock_utils.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local INITIAL_INDEX = 1 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 d58c1be66b..d116a84edb 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 9e56531652..111bd9def0 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,16 +1,5 @@ --- Copyright 2026 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. +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 e8d7df0e72..714859eeb8 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 8e21d8f690..60c24522f4 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 7f10041e0a..ebb5191bb1 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,16 +1,5 @@ --- Copyright 2026 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. +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 65dc07a036..1c4e5bad31 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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..4c1bfae85a --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_new_capabilities.lua @@ -0,0 +1,254 @@ +-- 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 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) + 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}}))) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Setting a user code should result in the named code changed event firing", + function() + 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() + 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() + 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 () + 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() + 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 () + 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 2799ee7d73..22bd44d9fe 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 69393f1004..81946aa759 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,16 +1,5 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -42,6 +31,7 @@ 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, } ) @@ -52,6 +42,7 @@ local schlage_mock_device = test.mock_device.build_test_zwave_device( zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, zwave_product_id = SCHLAGE_PRODUCT_ID, + useOldCapabilityForTesting = true, } ) @@ -86,11 +77,11 @@ test.register_coroutine_test( 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}}, { state_change = true, visibility = { displayed = true } }))) + 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"}}, { state_change = true, visibility = { displayed = true } }))) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) + 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 ) @@ -101,11 +92,11 @@ test.register_coroutine_test( 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({}, { state_change=true, visibility = { displayed = true } }))) + 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({}, { state_change = true, visibility = { displayed = true } }))) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) + 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 ) @@ -125,11 +116,11 @@ test.register_coroutine_test( 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}}, { state_change = true, visibility = { displayed = true } }))) + 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"}}, { state_change = true, visibility = { displayed = true } }))) - test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } }))) + 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 ) 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 99e2aaf7d2..53e3268017 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,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 index ad2116d67d..5618b59b30 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/can_handle.lua @@ -1,3 +1,6 @@ +-- 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, diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua index 7102c1299e..f461707387 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -1,3 +1,6 @@ +-- 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 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 index 84b85b85f5..3b12277f18 100644 --- 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 @@ -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, cmd) 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 index 6e779862f7..26873a9309 100644 --- 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 @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" 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 index 35c6566776..d59b49e2e2 100644 --- 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 @@ -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, cmd) 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 index a3b9c11177..22f4c963eb 100644 --- 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 @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" 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 index 109d4f43d1..4f1428dc5d 100644 --- 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 @@ -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, cmd) 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 index 3ff09fc4f5..d303dbd466 100644 --- 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 @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -86,6 +75,21 @@ local function user_code_report_handler(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] = { @@ -98,6 +102,11 @@ local schlage_lock = { [Basic.SET] = basic_set_handler } }, + capability_handlers = { + [capabilities.lockCredentials.ID] = { + [capabilities.lockCredentials.commands.addCredential.NAME] = add_credential_handler + } + }, lifecycle_handlers = { doConfigure = do_configure, }, 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 index 4bbdcbfcea..4520fbf68f 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/sub_drivers.lua @@ -1,4 +1,4 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright © 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local lazy_load_if_possible = require "lazy_load_subdriver" 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 index ea09d0024e..f622dcf569 100644 --- 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 @@ -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, cmd) 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 index eb64d79f65..92fb4f4465 100644 --- 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 @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -46,7 +35,7 @@ local function alarm_report_handler(driver, device, cmd) event = capabilities.lock.lock.unknown() elseif (alarm_type == 16 or alarm_type == 19) then event = capabilities.lock.lock.unlocked() - if (code_id ~= nil) then + if (credential_index ~= nil) then local user_id = nil local credential = lock_utils.get_credential(device, credential_index) if (credential ~= nil) then @@ -56,7 +45,7 @@ local function alarm_report_handler(driver, device, cmd) end elseif (alarm_type == 18) then event = capabilities.lock.lock.locked() - if (code_id ~= nil) then + if (credential_index ~= nil) then local user_id = nil local credential = lock_utils.get_credential(device, credential_index) if (credential ~= nil) then diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua index 995202181e..71fdd494ba 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -1,39 +1,8 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" -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 - ) - end -end - 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 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 index 0bafa6c14a..325db40bec 100644 --- 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 @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua index e082275e98..964d8b321b 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua index 17c280bb71..0b8d1fb0d3 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua index 4d21e6c794..5b65f6c112 100644 --- a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass From 470f666307116e62b3cbff0ff887e7feda3b09d4 Mon Sep 17 00:00:00 2001 From: Pegor Date: Thu, 8 Jan 2026 13:21:32 -0800 Subject: [PATCH 12/16] lazy load sub drivers --- drivers/SmartThings/zigbee-lock/src/init.lua | 15 +-------------- .../zigbee-lock/src/lazy_load_subdriver.lua | 15 +++++++++++++++ .../zigbee-lock/src/sub_drivers.lua | 9 +++++++++ .../src/using-new-capabilities/init.lua | 5 +---- .../lock-without-codes/can_handle.lua | 17 +++++++++++++++++ .../lock-without-codes/init.lua | 16 +--------------- .../samsungsds/can_handle.lua | 10 ++++++++++ .../samsungsds/init.lua | 4 +--- .../using-new-capabilities/sub_drivers.lua | 11 +++++++++++ .../yale-fingerprint-lock/can_handle.lua | 19 +++++++++++++++++++ .../yale-fingerprint-lock/init.lua | 18 +----------------- .../yale/can_handle.lua | 10 ++++++++++ .../src/using-new-capabilities/yale/init.lua | 4 +--- .../src/using-old-capabilities/init.lua | 5 +---- .../lock-without-codes/can_handle.lua | 17 +++++++++++++++++ .../lock-without-codes/init.lua | 16 +--------------- .../samsungsds/can_handle.lua | 10 ++++++++++ .../samsungsds/init.lua | 4 +--- .../using-old-capabilities/sub_drivers.lua | 11 +++++++++++ .../yale-fingerprint-lock/can_handle.lua | 19 +++++++++++++++++++ .../yale-fingerprint-lock/init.lua | 18 +----------------- .../yale/can_handle.lua | 10 ++++++++++ .../src/using-old-capabilities/yale/init.lua | 4 +--- 23 files changed, 169 insertions(+), 98 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index deffdaad93..5509163ec3 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -34,18 +34,6 @@ local new_lock_utils = require "new_lock_utils" local DELAY_LOCK_EVENT = "_delay_lock_event" local MAX_DELAY = 10 -local function lazy_load_if_possible(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - - -- version 9 will include the lazy loading functions - if version.api >= 9 then - return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end -end - local refresh = function(driver, device, cmd) device:refresh() device:send(LockCluster.attributes.LockState:read(device)) @@ -159,8 +147,7 @@ local zigbee_lock_driver = { } }, sub_drivers = { - lazy_load_if_possible("using-old-capabilities"), - lazy_load_if_possible("using-new-capabilities"), + require("sub_drivers") }, lifecycle_handlers = { added = device_added, diff --git a/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..6cbf22575d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- 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 version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..cee6ff6954 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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"), + lazy_load_if_possible("using-new-capabilities"), +} +return sub_drivers \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 006e7b8ba2..0f9062823a 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -557,10 +557,7 @@ local new_capabilities_driver = { }, }, sub_drivers = { - require("using-new-capabilities.samsungsds"), - require("using-new-capabilities.yale-fingerprint-lock"), - require("using-new-capabilities.yale"), - require("using-new-capabilities.lock-without-codes") + require("using-new-capabilities.sub_drivers") }, health_check = false, lifecycle_handlers = { diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/can_handle.lua new file mode 100644 index 0000000000..21984e938b --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +return function(opts, driver, device, cmd) + for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("using-new-capabilities.lock-without-codes") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua index d89cc01580..8363377bf9 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua @@ -19,20 +19,6 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration -local LOCK_WITHOUT_CODES_FINGERPRINTS = { - { model = "E261-KR0B0Z0-HA" }, - { mfr = "Danalock", model = "V3-BTZB" } -} - -local function can_handle_lock_without_codes(opts, driver, device) - for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function device_init(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then @@ -95,7 +81,7 @@ local lock_without_codes = { } } }, - can_handle = can_handle_lock_without_codes + can_handle = require("using-new-capabilities.lock-without-codes.can_handle") } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua new file mode 100644 index 0000000000..f9ff67fcf5 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + if device:get_manufacturer() == "SAMSUNG SDS" then + local subdriver = require("using-new-capabilities.samsungsds") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua index 03802b26ef..ed61935e4b 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua @@ -112,9 +112,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SAMSUNG SDS" - end + can_handle = require("using-new-capabilities.samsungsds.can_handle") } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/sub_drivers.lua new file mode 100644 index 0000000000..8167f51d98 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-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-new-capabilities.samsungsds"), + lazy_load_if_possible("using-new-capabilities.yale-fingerprint-lock"), + lazy_load_if_possible("using-new-capabilities.yale"), + lazy_load_if_possible("using-new-capabilities.lock-without-codes") +} +return sub_drivers \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/can_handle.lua new file mode 100644 index 0000000000..6f206ed9b5 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return function(opts, driver, device) + for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("using-new-capabilities.yale-fingerprint-lock") + return true, subdriver + end + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua index 4e74bbe79f..d0ab156b65 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua @@ -20,22 +20,6 @@ local LockUsers = capabilities.lockUsers local YALE_FINGERPRINT_MAX_CODES = 0x1E -local YALE_FINGERPRINT_LOCK = { - { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} - -local yale_fingerprint_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local handle_max_codes = function(driver, device, value) device:emit_event(LockCredentials.pinUsersSupported(YALE_FINGERPRINT_MAX_CODES)) device:emit_event(LockUsers.totalUsersSupported(YALE_FINGERPRINT_MAX_CODES)) @@ -50,7 +34,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = yale_fingerprint_lock_models + can_handle = require("using-new-capabilities.yale-fingerprint-lock.can_handle") } return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua new file mode 100644 index 0000000000..ecefc56b4c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + if device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" then + local subdriver = require("using-new-capabilities.yale") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua index 3b70c910f4..d8096eaeaf 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua @@ -174,9 +174,7 @@ local yale_door_lock_driver = { }, sub_drivers = { require("using-new-capabilities.yale.yale-bad-battery-reporter") }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" - end + can_handle = require("using-new-capabilities.yale.can_handle") } return yale_door_lock_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 index 0af1877760..cc5bb9911f 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -401,10 +401,7 @@ local old_capabilities_driver = { }, }, sub_drivers = { - require("using-old-capabilities.samsungsds"), - require("using-old-capabilities.yale"), - require("using-old-capabilities.yale-fingerprint-lock"), - require("using-old-capabilities.lock-without-codes") + require("using-old-capabilities.sub_drivers") }, health_check = false, lifecycle_handlers = { diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua new file mode 100644 index 0000000000..a580765e8d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +return function(opts, driver, device, cmd) + for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("using-old-capabilities.lock-without-codes") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua index 7272991459..f1e93bc9a6 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua @@ -19,20 +19,6 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration -local LOCK_WITHOUT_CODES_FINGERPRINTS = { - { model = "E261-KR0B0Z0-HA" }, - { mfr = "Danalock", model = "V3-BTZB" } -} - -local function can_handle_lock_without_codes(opts, driver, device) - for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function device_init(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then @@ -95,7 +81,7 @@ local lock_without_codes = { } } }, - can_handle = can_handle_lock_without_codes + can_handle = require("using-old-capabilities.lock-without-codes.can_handle") } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua new file mode 100644 index 0000000000..c2419a7b01 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + if device:get_manufacturer() == "SAMSUNG SDS" then + local subdriver = require("using-old-capabilities.samsungsds") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua index b529dd3fd1..b551bdd106 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua @@ -112,9 +112,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SAMSUNG SDS" - end + can_handle = require("using-old-capabilities.samsungsds.can_handle") } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/sub_drivers.lua new file mode 100644 index 0000000000..1f6149587c --- /dev/null +++ b/drivers/SmartThings/zigbee-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.samsungsds"), + lazy_load_if_possible("using-old-capabilities.yale"), + lazy_load_if_possible("using-old-capabilities.yale-fingerprint-lock"), + lazy_load_if_possible("using-old-capabilities.lock-without-codes") +} +return sub_drivers \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/can_handle.lua new file mode 100644 index 0000000000..064fe8de10 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return function(opts, driver, device) + for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("using-old-capabilities.yale-fingerprint-lock") + return true, subdriver + end + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua index 9d0a0b4148..abf36ce497 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua @@ -19,22 +19,6 @@ local LockCodes = capabilities.lockCodes local YALE_FINGERPRINT_MAX_CODES = 0x1E -local YALE_FINGERPRINT_LOCK = { - { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} - -local yale_fingerprint_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local handle_max_codes = function(driver, device, value) device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) end @@ -48,7 +32,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = yale_fingerprint_lock_models + can_handle = require("using-old-capabilities.yale-fingerprint-lock.can_handle") } return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua new file mode 100644 index 0000000000..25a4a6caee --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + if device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" then + local subdriver = require("using-old-capabilities.yale") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua index a38e6be361..3fd143beef 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua @@ -153,9 +153,7 @@ local yale_door_lock_driver = { }, sub_drivers = { require("using-old-capabilities.yale.yale-bad-battery-reporter") }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" - end + can_handle = require("using-old-capabilities.yale.can_handle") } return yale_door_lock_driver From 439f357a10f2fae573f1cdd180645b99157b6d91 Mon Sep 17 00:00:00 2001 From: Pegor Date: Thu, 8 Jan 2026 13:39:27 -0800 Subject: [PATCH 13/16] Add supportedCredentials event --- drivers/SmartThings/zigbee-lock/src/init.lua | 1 + .../src/test/test_zigbee_yale-new_capabilities.lua | 4 ++++ .../src/test/test_schlage_lock_new_capabilities.lua | 1 - 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 5509163ec3..4a961ac7c1 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -66,6 +66,7 @@ local function device_added(driver, device) if device.useOldCapabilityForTesting == nil then if device:supports_capability_by_id(LockCodes.ID) then device:emit_event(LockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) new_lock_utils.reload_tables(device) else lock_utils.populate_state_from_data(device) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua index d7db3f443d..7d38e80173 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua @@ -43,6 +43,8 @@ local function test_init_default() 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, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read( mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) @@ -54,6 +56,8 @@ local function test_init_add_device() 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, { state_change = true, visibility = { displayed = true } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read( mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) 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 4c1bfae85a..4f2f59f4fa 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 @@ -4,7 +4,6 @@ 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" From 633172848d3fb9a758593538db36525f30ec80dd Mon Sep 17 00:00:00 2001 From: Pegor Date: Thu, 8 Jan 2026 13:50:16 -0800 Subject: [PATCH 14/16] breakout can_handle for new and old capabilites --- .../src/test/test_zigbee_yale-new_capabilities.lua | 10 ---------- .../src/using-new-capabilities/can_handle.lua | 13 +++++++++++++ .../zigbee-lock/src/using-new-capabilities/init.lua | 10 +--------- .../src/using-old-capabilities/can_handle.lua | 13 +++++++++++++ .../zigbee-lock/src/using-old-capabilities/init.lua | 10 +--------- .../src/test/test_zwave_lock_new_capabilities.lua | 10 ---------- 6 files changed, 28 insertions(+), 38 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/can_handle.lua diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua index 7d38e80173..ede507e503 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua @@ -85,16 +85,6 @@ local expect_reload_all_codes_messages = function() test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfTotalUsersSupported:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) - test.socket.zigbee:__queue_receive({ - mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 0x01, - DoorLockUserType.UNRESTRICTED, - DoorLockUserStatus.AVAILABLE, - "" - ) - }) end test.register_coroutine_test( diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/can_handle.lua new file mode 100644 index 0000000000..5618b59b30 --- /dev/null +++ b/drivers/SmartThings/zigbee-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/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 0f9062823a..b4290d74f7 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -564,15 +564,7 @@ local new_capabilities_driver = { init = init, doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - 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 + can_handle = require("using-new-capabilities.can_handle") } return new_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/can_handle.lua new file mode 100644 index 0000000000..1fe9815bb9 --- /dev/null +++ b/drivers/SmartThings/zigbee-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/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua index cc5bb9911f..f8fcae368f 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -407,15 +407,7 @@ local old_capabilities_driver = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = 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") - return true, subdriver - end - return false - end + can_handle = require("using-old-capabilities.can_handle") } return old_capabilities_driver 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 53e3268017..ba22067b2f 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 @@ -97,15 +97,6 @@ end -- start with a migrated blank device local function test_init() test.mock_device.add_test_device(mock_device) - -- 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 } }))) test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) @@ -604,7 +595,6 @@ test.register_coroutine_test( test.register_coroutine_test( "When the device is added it should be set up and start reading codes", function() - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.wait_for_events() test.mock_time.advance_time(2) test.socket.zwave:__expect_send( From f9dd9042caa8f881c223bee81bece441115c8d35 Mon Sep 17 00:00:00 2001 From: Pegor Date: Thu, 8 Jan 2026 14:58:30 -0800 Subject: [PATCH 15/16] breakout sub driver for yale --- .../src/using-new-capabilities/yale/init.lua | 2 +- .../yale/sub_drivers.lua | 8 + .../yale-bad-battery-reporter/can_handle.lua | 21 +++ .../yale/yale-bad-battery-reporter/init.lua | 20 +-- .../src/using-old-capabilities/yale/init.lua | 2 +- .../yale/sub_drivers.lua | 8 + .../yale-bad-battery-reporter/can_handle.lua | 21 +++ .../yale/yale-bad-battery-reporter/init.lua | 20 +-- drivers/SmartThings/zwave-lock/src/init.lua | 14 +- .../test/test_keywe_lock_new_capabilities.lua | 97 ++++++----- .../test_samsung_lock_new_capabilities.lua | 46 +++++- .../test_schlage_lock_new_capabilities.lua | 51 +++++- .../test/test_zwave_lock_new_capabilities.lua | 156 ++++++++++++------ .../zwave-alarm-v1-lock/init.lua | 2 +- 14 files changed, 317 insertions(+), 151 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/can_handle.lua diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua index d8096eaeaf..b6a81c9931 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua @@ -173,7 +173,7 @@ local yale_door_lock_driver = { } }, - sub_drivers = { require("using-new-capabilities.yale.yale-bad-battery-reporter") }, + sub_drivers = { require("using-new-capabilities.yale.sub_drivers") }, can_handle = require("using-new-capabilities.yale.can_handle") } diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/sub_drivers.lua new file mode 100644 index 0000000000..19d92e6f3e --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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-new-capabilities.yale.yale-bad-battery-reporter"), +} +return sub_drivers \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/can_handle.lua new file mode 100644 index 0000000000..39fffc666c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return function(opts, driver, device) + for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("using-new-capabilities.yale.yale-bad-battery-reporter") + return true, subdriver + end + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua index 8b3528c4c0..0a1bcaceb3 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua @@ -15,24 +15,6 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" -local BAD_YALE_LOCK_FINGERPRINTS = { - { mfr = "Yale", model = "YRD220/240 TSDB" }, - { mfr = "Yale", model = "YRL220 TS LL" }, - { mfr = "Yale", model = "YRD210 PB DB" }, - { mfr = "Yale", model = "YRL210 PB LL" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} - -local is_bad_yale_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) end @@ -46,7 +28,7 @@ local bad_yale_driver = { } } }, - can_handle = is_bad_yale_lock_models + can_handle = require("using-new-capabilities.yale.yale-bad-battery-reporter.can_handle") } return bad_yale_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua index 3fd143beef..54df48cb10 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua @@ -152,7 +152,7 @@ local yale_door_lock_driver = { } }, - sub_drivers = { require("using-old-capabilities.yale.yale-bad-battery-reporter") }, + sub_drivers = { require("using-new-capabilities.yale.sub_drivers") }, can_handle = require("using-old-capabilities.yale.can_handle") } diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua new file mode 100644 index 0000000000..4d06a46db4 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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.yale.yale-bad-battery-reporter"), +} +return sub_drivers \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/can_handle.lua new file mode 100644 index 0000000000..eb58c8473c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return function(opts, driver, device) + for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("using-old-capabilities.yale.yale-bad-battery-reporter") + return true, subdriver + end + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua index 59fdbf228b..8ff76e55dd 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua @@ -15,24 +15,6 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" -local BAD_YALE_LOCK_FINGERPRINTS = { - { mfr = "Yale", model = "YRD220/240 TSDB" }, - { mfr = "Yale", model = "YRL220 TS LL" }, - { mfr = "Yale", model = "YRD210 PB DB" }, - { mfr = "Yale", model = "YRL210 PB LL" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} - -local is_bad_yale_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) end @@ -46,7 +28,7 @@ local bad_yale_driver = { } } }, - can_handle = is_bad_yale_lock_models + can_handle = require("using-old-capabilities.yale.yale-bad-battery-reporter.can_handle") } return bad_yale_driver diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 77dff1fe01..19cda5fb58 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -38,13 +38,7 @@ 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 == nil then - 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 - else + if device.useOldCapabilityForTesting == true then -- added handler from using old capabilities driver:inject_capability_command(device, { capability = capabilities.lockCodes.ID, @@ -63,6 +57,12 @@ local do_added = function(driver, device) if (device:supports_capability(capabilities.tamperAlert)) then device:emit_event(capabilities.tamperAlert.tamper.clear()) 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") + end end end 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 111bd9def0..463a968dad 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 @@ -8,6 +8,7 @@ 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 @@ -35,6 +36,12 @@ 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 + +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 } }))) @@ -46,13 +53,46 @@ local function test_init() ) 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.set_test_init_function(test_init) - 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}) }) @@ -63,6 +103,7 @@ test.register_coroutine_test( 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}) }) @@ -70,50 +111,18 @@ test.register_coroutine_test( 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"}})) - }, + 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 - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Notification:Report({notification_type = 6, event = 6, event_parameter = "\x01"}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="keypad"}})) - } - } + 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_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_new_capabilities.lua index ebb5191bb1..ec68657690 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 @@ -24,8 +24,15 @@ 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 + +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 } }))) @@ -36,8 +43,41 @@ local function test_init() 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.set_test_init_function(test_init) local function init_code_slot(slot_number, name, device) local credentials = device.transient_store[lock_utils.LOCK_CREDENTIALS] @@ -57,6 +97,7 @@ 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()) @@ -68,6 +109,7 @@ test.register_coroutine_test( 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( @@ -128,6 +170,7 @@ test.register_coroutine_test( 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( @@ -157,6 +200,7 @@ test.register_coroutine_test( 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) 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 4f2f59f4fa..34356601e9 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 @@ -45,6 +45,12 @@ 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 } }))) @@ -55,12 +61,46 @@ local function test_init() 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.set_test_init_function(test_init) 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( @@ -109,6 +149,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Configuration report should be handled", function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -128,6 +169,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Configuration report indicating code deletion should be handled", function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -175,7 +217,8 @@ test.register_coroutine_test( 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({ @@ -204,6 +247,7 @@ test.register_coroutine_test( 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( @@ -228,7 +272,8 @@ test.register_coroutine_test( 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({ 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 ba22067b2f..73057ac7c8 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 @@ -12,6 +12,8 @@ local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) 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 @@ -98,6 +100,15 @@ 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 } }))) @@ -110,27 +121,46 @@ local function test_init() ) 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) - -- ) - -- test.socket.zwave:__expect_send( - -- UserCode:Get({user_identifier = 1}):build_test_tx(mock_device.id) - -- ) - - -- reset these globals - test_credential_index = 1 - test_credentials = {} - test_users = {} + 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.set_test_init_function(test_init) - - test.register_coroutine_test( "Add user should succeed", - function () + function() + added() test.socket.capability:__queue_receive({ mock_device.id, { @@ -189,6 +219,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Add credential should succeed", function() + added() -- these all should succeed add_credential(0) add_credential(0) @@ -198,7 +229,8 @@ test.register_coroutine_test( test.register_coroutine_test( "Add credential for existing user should succeed", - function () + function() + added() test.socket.capability:__queue_receive({ mock_device.id, { @@ -234,7 +266,8 @@ test.register_coroutine_test( test.register_coroutine_test( "Update user should succeed", - function () + function() + added() test.socket.capability:__queue_receive({ mock_device.id, { @@ -320,6 +353,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Delete user should succeed", function() + added() -- add credential add_credential(0) @@ -380,6 +414,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Update credential should succeed", function() + added() -- add credential add_credential(0) @@ -434,6 +469,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Delete credential should succeed", function() + added() -- add the credential add_credential(0) @@ -493,6 +529,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Delete all users should succeed", function() + added() -- add credential add_credential(0) -- add second credential @@ -571,6 +608,7 @@ test.register_coroutine_test( test.register_coroutine_test( "The lock reporting unlock via code should include the code number", function() + added() -- add credential add_credential(0) -- send unlock @@ -592,45 +630,10 @@ test.register_coroutine_test( end ) -test.register_coroutine_test( - "When the device is added it should be set up and start reading codes", - function() - test.wait_for_events() - test.mock_time.advance_time(2) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - UserCode:Get({user_identifier = 1}):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:__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.zwave:__expect_send( - UserCode:Get({user_identifier = 2}):build_test_tx(mock_device.id) - ) - 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, @@ -681,4 +684,47 @@ test.register_coroutine_test( 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/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua index 92fb4f4465..06c5e2980e 100644 --- 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 @@ -132,7 +132,7 @@ local function alarm_report_handler(driver, device, cmd) -- 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) - lock_utils.delete_user(device, active_credential.userIndex) + 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 From 966847a6e93992f9cfc449bb1ba83d579d85b25d Mon Sep 17 00:00:00 2001 From: Pegor Date: Fri, 9 Jan 2026 09:55:03 -0800 Subject: [PATCH 16/16] increase init delay to wait for radio up --- .../SmartThings/zigbee-lock/src/using-new-capabilities/init.lua | 2 +- .../SmartThings/zwave-lock/src/using-new-capabilities/init.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index b4290d74f7..bfc627c6bc 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -66,7 +66,7 @@ end local init = function(driver, device) lock_utils.reload_tables(device) - device.thread:call_with_delay(2, function(d) + device.thread:call_with_delay(10, function(d) reload_all_codes(device) end) end diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua index f461707387..dbd13e5738 100644 --- a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -50,7 +50,7 @@ end local init = function(driver, device) lock_utils.reload_tables(device) - device.thread:call_with_delay(2, function () + device.thread:call_with_delay(10, function () reload_all_codes(device) end) end