From 6a807a2dc953eb5270c7391937f95d5ffa57f3e5 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 30 Apr 2026 12:55:03 -0500 Subject: [PATCH 1/7] CHAD-16364: Update Zigbee lock capabilities --- .../zigbee-lock/profiles/base-lock.yml | 4 + drivers/SmartThings/zigbee-lock/src/init.lua | 323 +-------- .../src/lock-without-codes/can_handle.lua | 14 - .../zigbee-lock/src/lock_utils.lua | 3 +- .../zigbee-lock/src/new_lock_utils.lua | 316 +++++++++ .../zigbee-lock/src/sub_drivers.lua | 6 +- .../test/test_zigbee_lock_code_migration.lua | 6 +- .../test_zigbee_lock_code_slga_migration.lua | 84 +++ .../test_zigbee_lock_new_capabilities.lua | 625 ++++++++++++++++++ .../test_zigbee_yale-fingerprint-lock.lua | 37 +- .../test_zigbee_yale-new_capabilities.lua | 553 ++++++++++++++++ .../src/using-new-capabilities/can_handle.lua | 13 + .../src/using-new-capabilities/init.lua | 570 ++++++++++++++++ .../lock-without-codes/can_handle.lua | 17 + .../lock-without-codes/init.lua | 87 +++ .../samsungsds/can_handle.lua | 7 +- .../samsungsds/init.lua | 118 ++++ .../using-new-capabilities/sub_drivers.lua | 11 + .../yale-fingerprint-lock/can_handle.lua | 19 + .../yale-fingerprint-lock/init.lua | 40 ++ .../yale/can_handle.lua | 7 +- .../src/using-new-capabilities/yale/init.lua | 171 +++++ .../yale/sub_drivers.lua | 8 + .../yale-bad-battery-reporter/can_handle.lua | 21 + .../yale/yale-bad-battery-reporter/init.lua | 34 + .../src/using-old-capabilities/can_handle.lua | 13 + .../src/using-old-capabilities/init.lua | 413 ++++++++++++ .../lock-without-codes/can_handle.lua | 13 + .../lock-without-codes/fingerprints.lua | 0 .../lock-without-codes/init.lua | 5 +- .../samsungsds/can_handle.lua | 10 + .../samsungsds/init.lua | 2 +- .../using-old-capabilities/sub_drivers.lua | 11 + .../yale-fingerprint-lock/can_handle.lua | 13 + .../yale-fingerprint-lock/fingerprints.lua | 0 .../yale-fingerprint-lock/init.lua | 5 +- .../yale/can_handle.lua | 10 + .../yale/init.lua | 4 +- .../yale/sub_drivers.lua | 2 +- .../yale-bad-battery-reporter/can_handle.lua | 13 + .../fingerprints.lua | 0 .../yale/yale-bad-battery-reporter/init.lua | 5 +- .../src/yale-fingerprint-lock/can_handle.lua | 14 - .../yale-bad-battery-reporter/can_handle.lua | 14 - 44 files changed, 3256 insertions(+), 385 deletions(-) delete mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua 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/test/test_zigbee_yale-new_capabilities.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.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/lock-without-codes/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-new-capabilities}/samsungsds/can_handle.lua (55%) 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/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-fingerprint-lock/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-new-capabilities}/yale/can_handle.lua (62%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua 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-new-capabilities/yale/yale-bad-battery-reporter/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/can_handle.lua 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/can_handle.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/lock-without-codes/fingerprints.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/lock-without-codes/init.lua (96%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/samsungsds/init.lua (98%) 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 rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale-fingerprint-lock/fingerprints.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale-fingerprint-lock/init.lua (90%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale/init.lua (97%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale/sub_drivers.lua (69%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/can_handle.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale/yale-bad-battery-reporter/fingerprints.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale/yale-bad-battery-reporter/init.lua (86%) delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.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 94f5adc0c4..3b72804fac 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -4,14 +4,12 @@ -- 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" @@ -19,34 +17,13 @@ local Battery = capabilities.battery local Lock = capabilities.lock local LockCodes = capabilities.lockCodes --- 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 new_lock_utils = require "new_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 refresh = function(driver, device, cmd) device:refresh() device:send(LockCluster.attributes.LockState:read(device)) @@ -61,30 +38,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(), @@ -96,193 +49,21 @@ 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, {}) +-- 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) + -- 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 } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) + new_lock_utils.reload_tables(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 + lock_utils.populate_state_from_data(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 - 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) + lock_utils.populate_state_from_data(device) 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, @@ -321,69 +102,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 @@ -403,30 +121,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, @@ -437,7 +139,6 @@ local zigbee_lock_driver = { }, sub_drivers = require("sub_drivers"), lifecycle_handlers = { - doConfigure = do_configure, added = device_added, init = init, }, diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua deleted file mode 100644 index 543e43a8b1..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua +++ /dev/null @@ -1,14 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_lock_without_codes(opts, driver, device) - local FINGERPRINTS = require("lock-without-codes.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("lock-without-codes") - end - end - return false -end - -return can_handle_lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index a02a59963c..d9c06f41f6 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -9,11 +9,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..9d5848b21b --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -0,0 +1,316 @@ +-- 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 capabilities = require "st.capabilities" +local utils = require "st.utils" +local INITIAL_INDEX = 1 + +local new_lock_utils = { + -- Constants + ADD_CREDENTIAL = "addCredential", + ADD_USER = "addUser", + BUSY = "busy", + COMMAND_NAME = "commandName", + CREDENTIAL_TYPE = "pin", + CHECKING_CODE = "checkingCode", + DELETE_ALL_CREDENTIALS = "deleteAllCredentials", + DELETE_ALL_USERS = "deleteAllUsers", + DELETE_CREDENTIAL = "deleteCredential", + DELETE_USER = "deleteUser", + LOCK_CREDENTIALS = "lockCredentials", + LOCK_USERS = "lockUsers", + ACTIVE_CREDENTIAL = "activeCredential", + STATUS_BUSY = "busy", + STATUS_DUPLICATE = "duplicate", + STATUS_FAILURE = "failure", + STATUS_INVALID_COMMAND = "invalidCommand", + STATUS_OCCUPIED = "occupied", + STATUS_RESOURCE_EXHAUSTED = "resourceExhausted", + STATUS_SUCCESS = "success", + TABLES_LOADED = "tablesLoaded", + UPDATE_CREDENTIAL = "updateCredential", + UPDATE_USER = "updateUser", + USER_INDEX = "userIndex", + USER_NAME = "userName", + USER_TYPE = "userType" +} + +-- check if we are currently busy performing a task. +-- if we aren't then set as busy. +new_lock_utils.busy_check_and_set = function (device, command, override_busy_check) + if override_busy_check then + -- the function was called by an injected command. + return false + end + local c_time = os.time() + local busy_state = device:get_field(new_lock_utils.BUSY) or false + + if busy_state == false or c_time - busy_state > 10 then + device:set_field(new_lock_utils.COMMAND_NAME, command) + device:set_field(new_lock_utils.BUSY, c_time) + return false + else + local command_result_info = { + commandName = command.name, + statusCode = new_lock_utils.STATUS_BUSY + } + if command.type == new_lock_utils.LOCK_USERS then + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + else + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + end + return true + end +end + +new_lock_utils.clear_busy_state = function(device, status, override_busy_check) + if override_busy_check then + return + end + local command = device:get_field(new_lock_utils.COMMAND_NAME) + local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) + if command ~= nil then + local command_result_info = { + commandName = command.name, + statusCode = status + } + if command.type == new_lock_utils.LOCK_USERS then + if active_credential ~= nil and active_credential.userIndex ~= nil then + command_result_info.userIndex = active_credential.userIndex + end + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + else + if active_credential ~= nil and active_credential.userIndex ~= nil then + command_result_info.userIndex = active_credential.userIndex + end + if active_credential ~= nil and active_credential.credentialIndex ~= nil then + command_result_info.credentialIndex = active_credential.credentialIndex + end + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, { state_change = true, visibility = { displayed = true } } + )) + end + end + + device:set_field(new_lock_utils.ACTIVE_CREDENTIAL, nil) + device:set_field(new_lock_utils.COMMAND_NAME, nil) + device:set_field(new_lock_utils.BUSY, false) +end + +new_lock_utils.reload_tables = function(device) + local users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.users.NAME, {}) + local credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.credentials.NAME, {}) + + if next(users) ~= nil then + device:set_field(new_lock_utils.LOCK_USERS, users) + end + if next(credentials) ~= nil then + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + end + device:set_field(new_lock_utils.TABLES_LOADED, true) +end + +new_lock_utils.get_users = function(device) + if not device:get_field(new_lock_utils.TABLES_LOADED) then + new_lock_utils.reload_tables(device) + end + + local users = utils.deep_copy(device:get_field(new_lock_utils.LOCK_USERS)) + return users ~= nil and users or {} +end + +new_lock_utils.get_user = function(device, user_index) + for _, user in pairs(new_lock_utils.get_users(device)) do + if user.userIndex == user_index then + return user + end + end + + return nil +end + +new_lock_utils.get_available_user_index = function(device) + local max = device:get_latest_state("main", capabilities.lockUsers.ID, + capabilities.lockUsers.totalUsersSupported.NAME, 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) + if not device:get_field(new_lock_utils.TABLES_LOADED) then + new_lock_utils.reload_tables(device) + end + + local credentials = utils.deep_copy(device:get_field(new_lock_utils.LOCK_CREDENTIALS)) + return credentials ~= nil and credentials or {} +end + +new_lock_utils.get_credential = function(device, credential_index) + for _, credential in pairs(new_lock_utils.get_credentials(device)) do + if credential.credentialIndex == credential_index then + return credential + end + end + return nil +end + +new_lock_utils.get_credential_by_user_index = function(device, user_index) + for _, credential in pairs(new_lock_utils.get_credentials(device)) do + if credential.userIndex == user_index then + return credential + end + end + + return nil +end + +new_lock_utils.get_available_credential_index = function(device) + local max = device:get_latest_state("main", capabilities.lockCredentials.ID, + capabilities.lockCredentials.pinUsersSupported.NAME, 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, { persist = true }) +end + +new_lock_utils.delete_user = function(device, user_index) + local current_users = new_lock_utils.get_users(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for index, user in pairs(current_users) do + if user.userIndex == user_index then + -- table.remove causes issues if we are removing while iterating. + -- instead set the value as nil and let `prep_table` handle removing it. + current_users[index] = nil + device:set_field(new_lock_utils.LOCK_USERS, current_users) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + return status_code +end + +new_lock_utils.add_credential = function(device, user_index, credential_type, credential_index) + local credentials = new_lock_utils.get_credentials(device) + table.insert(credentials, + { userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type }) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + return new_lock_utils.STATUS_SUCCESS +end + +new_lock_utils.delete_credential = function(device, credential_index) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for index, credential in pairs(credentials) do + if credential.credentialIndex == credential_index then + new_lock_utils.delete_user(device, credential.userIndex) + -- table.remove causes issues if we are removing while iterating. + -- instead set the value as nil and let `prep_table` handle removing it. + credentials[index] = nil + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + + return status_code +end + +new_lock_utils.update_credential = function(device, credential_index, user_index, credential_type) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for _, credential in pairs(credentials) do + if credential.credentialIndex == credential_index then + credential.credentialType = credential_type + credential.userIndex = user_index + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + return status_code +end + +-- emit_event doesn't like having `nil` values in the table. Remove any if they are present. +new_lock_utils.prep_table = function(data) + local clean_table = {} + for _, value in pairs(data) do + if value ~= nil then + clean_table[#clean_table + 1] = value -- Append to the end of the new array + end + end + return clean_table +end + +new_lock_utils.send_events = function(device, type) + if type == nil or type == new_lock_utils.LOCK_USERS then + local current_users = new_lock_utils.prep_table(new_lock_utils.get_users(device)) + device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) + 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:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + 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/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua index ff4bf8980d..e7ed53082b 100644 --- a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua @@ -3,9 +3,7 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("samsungsds"), - lazy_load_if_possible("yale"), - lazy_load_if_possible("yale-fingerprint-lock"), - lazy_load_if_possible("lock-without-codes"), + lazy_load_if_possible("using-old-capabilities"), + lazy_load_if_possible("using-new-capabilities"), } return sub_drivers 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 d4ee3f770f..bc1c2b54ef 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 @@ -23,14 +23,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 new file mode 100644 index 0000000000..d0de6d6824 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua @@ -0,0 +1,84 @@ +-- 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" + }), + }, + useOldCapabilityForTesting = true + } +) + +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..d23423983d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua @@ -0,0 +1,625 @@ +-- 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, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 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 4e9c92bafe..7e1f9ed9c6 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 @@ -16,7 +16,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", @@ -34,8 +45,32 @@ test.register_message_test( } }, { + test_init = test_init, min_api_version = 17 } ) +test.register_message_test( + "Max user code number report should be handled for migrated locks", + { + { + 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/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..73030a792e --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua @@ -0,0 +1,553 @@ +-- 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 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 PowerConfiguration = clusters.PowerConfiguration +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"), + zigbee_endpoints = { + [1] = { id = 1, manufacturer = "Yale", server_clusters = { 0x0001 } } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +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.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) }) + 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", + 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) }) + 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_default) + +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.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_init = function() + test_init_add_device() + end + } +) + +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", + { + -- 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 = true } })) + } + } +) + +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/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 new file mode 100644 index 0000000000..4b3c6a4096 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -0,0 +1,570 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Zigbee Driver utilities +local device_management = require "st.zigbee.device_management" +local log = require "log" +local utils = require "st.utils" + + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +-- Capabilities +local capabilities = require "st.capabilities" +local Battery = capabilities.battery +local Lock = capabilities.lock +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local socket = require "cosock.socket" +local lock_utils = require "new_lock_utils" + +local DELAY_LOCK_EVENT = "_delay_lock_event" +local MAX_DELAY = 10 + +local reload_all_codes = function(device) + -- starts at first user code index then iterates through all lock codes as they come in + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) == nil) then + device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + end + if (device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfTotalUsersSupported:read(device)) + end + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then + device:set_field(lock_utils.CHECKING_CODE, 1) + end + + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +end + +local init = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(15, function(d) + reload_all_codes(device) + end) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) + + device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) + device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) + + device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) + device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) + + device.thread:call_with_delay(2, function(d) + reload_all_codes(device) + end) +end + +local add_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then + return + end + local available_index = lock_utils.get_available_user_index(device) + local status = lock_utils.STATUS_SUCCESS + if available_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + else + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) + lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) + end + + if status == lock_utils.STATUS_SUCCESS then + lock_utils.send_events(device, lock_utils.LOCK_USERS) + end + + lock_utils.clear_busy_state(device, status) +end + +local update_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then + return + end + + local user_name = command.args.userName + local user_type = command.args.userType + local user_index = tonumber(command.args.userIndex) + local current_users = lock_utils.get_users(device) + local status = lock_utils.STATUS_FAILURE + + for _, user in pairs(current_users) do + if user.userIndex == user_index then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index}) + user.userName = user_name + user.userType = user_type + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + status = lock_utils.STATUS_SUCCESS + break + end + end + + lock_utils.clear_busy_state(device, status) +end + +local delete_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then + return + end + local status = lock_utils.STATUS_SUCCESS + local user_index = tonumber(command.args.userIndex) + if lock_utils.get_user(device, user_index) ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) + end + + local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) + if associated_credential ~= nil then + -- if there is an associated credential with this user then delete the credential + -- this command also handles the user deletion + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteCredential.NAME, + args = { associated_credential.credentialIndex, "pin" }, + override_busy_check = true + }) + else + lock_utils.delete_user(device, user_index) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end + else + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end +end + +local delete_all_users_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then + return + end + local status = lock_utils.STATUS_SUCCESS + local current_users = lock_utils.get_users(device) + + local delay = 0 + for _, user in pairs(current_users) do + device.thread:call_with_delay(delay, function() + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteUser.NAME, + args = {user.userIndex}, + override_busy_check = true + }) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +local add_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local user_index = tonumber(command.args.userIndex) + local user_type = command.args.userType + local credential_type = command.args.credentialType + local credential_data = command.args.credentialData + local status = lock_utils.STATUS_SUCCESS + + local credential_index = lock_utils.get_available_credential_index(device) + if credential_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then + status = lock_utils.STATUS_OCCUPIED + elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then + status = lock_utils.STATUS_FAILURE + end + + if user_index == 0 then + user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, user_type, user_index) + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + + if status == lock_utils.STATUS_SUCCESS then + -- set the pin code and then validate it was successful when the GetPINCode response is received. + -- the credential creation and events will also be handled in that response. + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) + device:send(LockCluster.server.commands.SetPINCode(device, + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) + ) + device.thread:call_with_delay(4, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, status) + end +end + +local update_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credential_index = tonumber(command.args.credentialIndex) + local credential_data = command.args.credentialData + local credential = lock_utils.get_credential(device, credential_index) + + if credential ~= nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + device:send(LockCluster.server.commands.SetPINCode(device, + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) + ) + device.thread:call_with_delay(4, function() + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) + end +end + +local delete_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then + return + end + + local credential_index = tonumber(command.args.credentialIndex) + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + end + + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) + end +end + +local delete_all_credentials_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credentials = lock_utils.get_credentials(device) + local status = lock_utils.STATUS_SUCCESS + local delay = 0 + for _, credential in pairs(credentials) do + local credential_index = tonumber(credential.credentialIndex) + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) + end) + device.thread:call_with_delay(delay + 2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +local max_code_length_handler = function(driver, device, value) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +local min_code_length_handler = function(driver, device, value) + device:emit_event(capabilities.lockCredentials.minPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +local max_codes_handler = function(driver, device, value) + device:emit_event(capabilities.lockUsers.totalUsersSupported(value.value, {visibility = {displayed = false}})) + device:emit_event(capabilities.lockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) +end + +local get_pin_response_handler = function(driver, device, zb_mess) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_NAME) + local status = lock_utils.STATUS_SUCCESS + local emit_event = false + + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then + -- create credential if not already present. + if lock_utils.get_credential(device, credential_index) == nil then + lock_utils.add_credential(device, + active_credential.userIndex, + active_credential.credentialType, + credential_index) + emit_event = true + end + elseif command ~= nil and command.name == lock_utils.UPDATE_CREDENTIAL then + -- update credential + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + emit_event = true + end + else + -- Called by reloading the codes. Don't add if already in table. + if lock_utils.get_credential(device, credential_index) == nil then + local new_user_index = lock_utils.get_available_user_index(device) + if new_user_index ~= nil then + lock_utils.create_user(device, nil, "guest", new_user_index) + lock_utils.add_credential(device, + new_user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_event = true + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + end + elseif zb_mess.body.zcl_body.user_status.value == UserStatusEnum.AVAILABLE and command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then + -- tried to add a code that already is in use. + -- remove the created user if one got made. There is no associated credential. + status = lock_utils.STATUS_DUPLICATE + lock_utils.delete_user(device, active_credential.userIndex) + else + if lock_utils.get_credential(device, credential_index) ~= nil then + -- Credential has been deleted. + lock_utils.delete_credential(device, credential_index) + emit_event = true + end + end + + if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then + -- the credential we're checking has arrived + local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, + capabilities.lockCredentials.pinUsersSupported.NAME) + if (credential_index >= last_slot) then + device:set_field(lock_utils.CHECKING_CODE, nil) + emit_event = true + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) + end + end + + if emit_event then + lock_utils.send_events(device) + end + -- ignore handling the busy state for these commands, they are handled within their own handlers + if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, status) + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) + local command = device:get_field(lock_utils.COMMAND_NAME) + local emit_events = false + + if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then + -- Master code updated + device:emit_event(capabilities.lockCredentials.commandResult( + {commandName = lock_utils.UPDATE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS}, + { state_change = true, visibility = { displayed = true } } + )) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (zb_mess.body.zcl_body.user_id.value == 0xFF) then + -- All credentials deleted + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + emit_events = true + end + else + -- One credential deleted + if (lock_utils.get_credential(device, credential_index) ~= nil) then + lock_utils.delete_credential(device, credential_index) + emit_events = true + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + if lock_utils.get_credential(device, credential_index) == nil and command == nil then + local user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, "guest", user_index) + lock_utils.add_credential(device, + user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_events = true + end + end + end + + if emit_events then + lock_utils.send_events(device) + end +end + +-- REMOVE THIS AFTER DONE WITH TESTING +local migrate = function(driver, device, value) + log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_users(device)).."\n" .. + "\n--- PK -- CURRENT CREDENTIALS ---- \n" .. + "\n" ..utils.stringify_table(lock_utils.get_credentials(device)).."\n" .. + "\n --------------------------------- \n") +end + +local lock_operation_event_handler = function(driver, device, zb_rx) + local event_code = zb_rx.body.zcl_body.operation_event_code.value + local source = zb_rx.body.zcl_body.operation_event_source.value + local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" + local METHOD = { + [0] = "keypad", + [1] = "command", + [2] = "manual", + [3] = "rfid", + [4] = "fingerprint", + [5] = "bluetooth" + } + local STATUS = { + [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local event = STATUS[event_code] + if (event ~= nil) then + event["data"] = {} + if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or + event_code == OperationEventCode.SCHEDULE_LOCK or + event_code == OperationEventCode.SCHEDULE_UNLOCK + ) then + event.data.method = "auto" + else + event.data.method = METHOD[source] + end + if (source == 0 and device:supports_capability_by_id(capabilities.lockUsers.ID)) then --keypad + local code_id = zb_rx.body.zcl_body.user_id.value + local code_name = "Code " .. code_id + local user = lock_utils.get_user(device, code_id) + if user ~= nil then + code_name = user.userName + end + + event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), + capabilities.lock.ID, + capabilities.lock.lock.ID) == event.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) + end +end + + +local new_capabilities_driver = { + NAME = "Lock Driver Using New Capabilities", + supported_capabilities = { + Lock, + LockCredentials, + LockUsers, + Battery, + }, + zigbee_handlers = { + cluster = { + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, + [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler, + } + }, + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.MaxPINCodeLength.ID] = max_code_length_handler, + [LockCluster.attributes.MinPINCodeLength.ID] = min_code_length_handler, + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = max_codes_handler, + } + } + }, + capability_handlers = { + [LockUsers.ID] = { + [LockUsers.commands.addUser.NAME] = add_user_handler, + [LockUsers.commands.updateUser.NAME] = update_user_handler, + [LockUsers.commands.deleteUser.NAME] = delete_user_handler, + [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, + }, + [LockCredentials.ID] = { + [LockCredentials.commands.addCredential.NAME] = add_credential_handler, + [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, + [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, + [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, + }, + + [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING + [capabilities.lockCodes.commands.migrate.NAME] = migrate, + }, + }, + sub_drivers = { + require("using-new-capabilities.sub_drivers") + }, + health_check = false, + lifecycle_handlers = { + init = init, + doConfigure = do_configure + }, + can_handle = require("using-new-capabilities.can_handle") +} + +return new_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-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 new file mode 100644 index 0000000000..8363377bf9 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua @@ -0,0 +1,87 @@ +-- 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 configurationMap = require "configurations" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local DoorLock = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +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 = require("using-new-capabilities.lock-without-codes.can_handle") +} + +return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua similarity index 55% rename from drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua index c483b2fe27..f9ff67fcf5 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua @@ -1,11 +1,10 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function samsungsds_can_handle(opts, driver, device, ...) +return function(opts, driver, device) if device:get_manufacturer() == "SAMSUNG SDS" then - return true, require("samsungsds") + local subdriver = require("using-new-capabilities.samsungsds") + return true, subdriver end return false end - -return samsungsds_can_handle 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..ed61935e4b --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua @@ -0,0 +1,118 @@ +-- 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 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 lock_utils = require "new_lock_utils" + +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) + 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 + +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) + lock_utils.reload_tables(device) +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 = 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 new file mode 100644 index 0000000000..d0ab156b65 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua @@ -0,0 +1,40 @@ +-- 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 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 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 = require("using-new-capabilities.yale-fingerprint-lock.can_handle") +} + +return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua similarity index 62% rename from drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua index 54340c7811..ecefc56b4c 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua @@ -1,11 +1,10 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function yale_can_handle(opts, driver, device, ...) +return function(opts, driver, device) if device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" then - return true, require("yale") + local subdriver = require("using-new-capabilities.yale") + return true, subdriver end return false end - -return yale_can_handle 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..2e88e950f1 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua @@ -0,0 +1,171 @@ +-- 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 Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local LockCluster = clusters.DoorLock + +-- Capabilities +local capabilities = require "st.capabilities" + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +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 + lock_utils.send_events(device) + end + -- ignore handling the busy state for these commands, they are handled within their own handlers + if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, status) + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) + local command = device:get_field(lock_utils.COMMAND_NAME) + local emit_events = false + + if 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 = 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 == 0xFFFF) then + -- All credentials deleted + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + emit_events = true + end + else + -- One credential deleted + if (lock_utils.get_credential(device, credential_index) ~= nil) then + lock_utils.delete_credential(device, credential_index) + emit_events = true + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + if lock_utils.get_credential(device, credential_index) == nil and command == nil then + local user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, "guest", user_index) + lock_utils.add_credential(device, + user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_events = true + end + end + end + + if emit_events then + lock_utils.send_events(device) + end +end + +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.sub_drivers") }, + can_handle = require("using-new-capabilities.yale.can_handle") +} + +return yale_door_lock_driver 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 new file mode 100644 index 0000000000..0a1bcaceb3 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua @@ -0,0 +1,34 @@ +-- 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +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 = 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/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 new file mode 100644 index 0000000000..f8fcae368f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -0,0 +1,413 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Zigbee Driver utilities +local device_management = require "st.zigbee.device_management" + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +-- Capabilities +local capabilities = require "st.capabilities" +local Battery = capabilities.battery +local Lock = capabilities.lock +local LockCodes = capabilities.lockCodes +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local socket = require "cosock.socket" +local lock_utils = require "lock_utils" + +local DELAY_LOCK_EVENT = "_delay_lock_event" +local MAX_DELAY = 10 + +local reload_all_codes = function(driver, device, command) + -- starts at first user code index then iterates through all lock codes as they come in + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + end + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then device:set_field(lock_utils.CHECKING_CODE, 0) end + device:emit_event(LockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +end + +local get_pin_response_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = { codeName = lock_utils.get_code_name(device, code_slot) } + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + -- Code slot is occupied + event.value = code_slot .. lock_utils.get_change_type(device, code_slot) + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = event.data.codeName + device:emit_event(event) + lock_utils.lock_codes_event(device, lock_codes) + lock_utils.reset_code_state(device, code_slot) + else + -- Code slot is unoccupied + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + -- Code has been deleted + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + else + -- Code is unset + event.value = code_slot .. " unset" + device:emit_event(event) + end + end + + code_slot = tonumber(code_slot) + if (code_slot == device:get_field(lock_utils.CHECKING_CODE)) then + -- the code we're checking has arrived + local last_slot = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - 1 + if (code_slot >= last_slot) then + device:emit_event(LockCodes.scanCodes("Complete", { visibility = { displayed = false } })) + device:set_field(lock_utils.CHECKING_CODE, nil) + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) + end + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = {} + if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then + -- Master code changed + event.value = "0 set" + event.data = { codeName = "Master Code" } + device:emit_event(event) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (zb_mess.body.zcl_body.user_id.value == 0xFF) then + -- All codes deleted + for cs, _ in pairs(lock_utils.get_lock_codes(device)) do + lock_utils.code_deleted(device, cs) + end + lock_utils.lock_codes_event(device, {}) + else + -- One code deleted + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + -- Code added or changed + local change_type = lock_utils.get_change_type(device, code_slot) + local code_name = lock_utils.get_code_name(device, code_slot) + event.value = code_slot .. change_type + event.data = { codeName = code_name } + device:emit_event(event) + if (change_type == " set") then + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = code_name + lock_utils.lock_codes_event(device, lock_codes) + end + end +end + +local handle_max_codes = function(driver, device, value) + if value.value ~= 0 then + -- Here's where we'll end up if we queried a lock whose profile does not have lock codes, + -- but it gave us a non-zero number of pin users, so we want to switch the profile + if not device:supports_capability_by_id(LockCodes.ID) then + device:try_update_metadata({ profile = "base-lock" }) -- switch to a lock with codes + lock_utils.populate_state_from_data(device) -- if this was a migrated device, try to migrate the lock codes + if not device:get_field(lock_utils.MIGRATION_COMPLETE) then -- this means we didn't find any pre-migration lock codes + -- so we'll load them manually + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end + end + device:emit_event(LockCodes.maxCodes(value.value, { visibility = { displayed = false } })) + end +end + +local handle_max_code_length = function(driver, device, value) + device:emit_event(LockCodes.maxCodeLength(value.value, { visibility = { displayed = false } })) +end + +local handle_min_code_length = function(driver, device, value) + device:emit_event(LockCodes.minCodeLength(value.value, { visibility = { displayed = false } })) +end + +local lock_operation_event_handler = function(driver, device, zb_rx) + local event_code = zb_rx.body.zcl_body.operation_event_code.value + local source = zb_rx.body.zcl_body.operation_event_source.value + local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" + local METHOD = { + [0] = "keypad", + [1] = "command", + [2] = "manual", + [3] = "rfid", + [4] = "fingerprint", + [5] = "bluetooth" + } + local STATUS = { + [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local event = STATUS[event_code] + if (event ~= nil) then + event["data"] = {} + if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or + event_code == OperationEventCode.SCHEDULE_LOCK or + event_code == OperationEventCode.SCHEDULE_UNLOCK + ) then + event.data.method = "auto" + else + event.data.method = METHOD[source] + end + if (source == 0 and device:supports_capability_by_id(capabilities.lockCodes.ID)) then --keypad + local code_id = zb_rx.body.zcl_body.user_id.value + local code_name = "Code " .. code_id + local lock_codes = device:get_field("lockCodes") + if (lock_codes ~= nil and + lock_codes[code_id] ~= nil) then + code_name = lock_codes[code_id] + end + event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), + capabilities.lock.ID, + capabilities.lock.lock.ID) == event.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) + end +end + +local update_codes = function(driver, device, command) + local delay = 0 + -- args.codes is json + for name, code in pairs(command.args.codes) do + -- these seem to come in the format "code[slot#]: code" + local code_slot = tonumber(string.gsub(name, "code", ""), 10) + if (code_slot ~= nil) then + if (code ~= nil and (code ~= "0" and code ~= "")) then + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.SetPINCode(device, + code_slot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + code)) + end) + delay = delay + 2 + else + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) + end) + delay = delay + 2 + end + device.thread:call_with_delay(delay, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, code_slot)) + end) + delay = delay + 2 + end + end +end + +local delete_code = function(driver, device, command) + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, command.args.codeSlot)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) +end + +local request_code = function(driver, device, command) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) +end + +local set_code = function(driver, device, command) + if (command.args.codePIN == "") then + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = { command.args.codeSlot, command.args.codeName } + }) + else + device:send(LockCluster.server.commands.SetPINCode(device, + command.args.codeSlot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + command.args.codePIN) + ) + if (command.args.codeName ~= nil) then + -- wait for confirmation from the lock to commit this to memory + -- Groovy driver has a lot more info passed here as a description string, may need to be investigated + local codeState = device:get_field(lock_utils.CODE_STATE) or {} + codeState["setName" .. command.args.codeSlot] = command.args.codeName + device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) + end + + device.thread:call_with_delay(4, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) + end +end + +local name_slot = function(driver, device, command) + local code_slot = tostring(command.args.codeSlot) + local lock_codes = lock_utils.get_lock_codes(device) + if (lock_codes[code_slot] ~= nil) then + lock_codes[code_slot] = command.args.codeName + device:emit_event(LockCodes.codeChanged(code_slot .. " renamed", { state_change = true })) + lock_utils.lock_codes_event(device, lock_codes) + end +end + +local migrate = function(driver, device, command) + local lock_users = {} + local lock_credentials = {} + local lock_codes = lock_utils.get_lock_codes(device) + local ordered_codes = {} + + for code in pairs(lock_codes) do + table.insert(ordered_codes, code) + end + + table.sort(ordered_codes) + for index, code_slot in ipairs(ordered_codes) do + table.insert(lock_users, { userIndex = index, userType = "guest", userName = lock_codes[code_slot] }) + table.insert(lock_credentials, { userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin" }) + end + + local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.minCodeLength.NAME, 4) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.maxCodeLength.NAME, 8) + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME, 0) + if (code_length ~= nil) then + max_code_len = code_length + min_code_len = code_length + end + + device:emit_event(LockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) + device:emit_event(LockUsers.users(lock_users, { visibility = { displayed = false } })) + device:emit_event(LockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(LockCodes.migrated(true, { visibility = { displayed = false } })) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) + + device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) + device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) + + device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) + device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) + + -- Don't send a reload all codes if this is a part of migration + if device.data.lockCodes == nil or device:get_field(lock_utils.MIGRATION_RELOAD_SKIPPED) == true then + device.thread:call_with_delay(2, function(d) + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end) + else + device:set_field(lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) + end +end + +local old_capabilities_driver = { + NAME = "Lock Driver Using Old Capabilities", + supported_capabilities = { + Lock, + LockCodes, + Battery, + }, + zigbee_handlers = { + cluster = { + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, + [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler + } + }, + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.MaxPINCodeLength.ID] = handle_max_code_length, + [LockCluster.attributes.MinPINCodeLength.ID] = handle_min_code_length, + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes, + } + } + }, + capability_handlers = { + [LockCodes.ID] = { + [LockCodes.commands.updateCodes.NAME] = update_codes, + [LockCodes.commands.deleteCode.NAME] = delete_code, + [LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [LockCodes.commands.requestCode.NAME] = request_code, + [LockCodes.commands.setCode.NAME] = set_code, + [LockCodes.commands.nameSlot.NAME] = name_slot, + [LockCodes.commands.migrate.NAME] = migrate, + }, + }, + sub_drivers = { + require("using-old-capabilities.sub_drivers") + }, + health_check = false, + lifecycle_handlers = { + doConfigure = do_configure + }, + can_handle = require("using-old-capabilities.can_handle") +} + +return old_capabilities_driver diff --git a/drivers/SmartThings/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..0eac7666c6 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local fingerprints = require("using-old-capabilities.lock-without-codes.fingerprints") + for _, fingerprint in ipairs(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/lock-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua similarity index 96% rename from drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua index e5c6de3408..743a2cc6f6 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua @@ -8,9 +8,6 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration - - - local function device_init(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then @@ -73,7 +70,7 @@ local lock_without_codes = { } } }, - can_handle = require("lock-without-codes.can_handle"), + 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/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua similarity index 98% rename from drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua index fff290df5d..51418f6e03 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua @@ -102,7 +102,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = require("samsungsds.can_handle"), + 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..a281fc16da --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/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 fingerprints = require("using-old-capabilities.yale-fingerprint-lock.fingerprints") + for _, fingerprint in ipairs(fingerprints) 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 diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/fingerprints.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 90% 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 index b78d043784..12619c01d5 100644 --- 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 @@ -8,9 +8,6 @@ local LockCluster = clusters.DoorLock local LockCodes = capabilities.lockCodes local YALE_FINGERPRINT_MAX_CODES = 0x1E - - - local handle_max_codes = function(driver, device, value) device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) end @@ -24,7 +21,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = require("yale-fingerprint-lock.can_handle"), + 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/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua similarity index 97% rename from drivers/SmartThings/zigbee-lock/src/yale/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua index c315fbfa06..4d6b8f8431 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua @@ -142,8 +142,8 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - sub_drivers = require("yale.sub_drivers"), - can_handle = require("yale.can_handle"), + sub_drivers = require("using-old-capabilities.yale.sub_drivers"), + can_handle = require("using-old-capabilities.yale.can_handle") } return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua similarity index 69% rename from drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua index 4b546979d3..d31cfb314a 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua @@ -3,6 +3,6 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("yale.yale-bad-battery-reporter"), + lazy_load_if_possible("using-old-capabilities.yale.yale-bad-battery-reporter"), } return sub_drivers 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..8c3106156a --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/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 fingerprints = require("using-old-capabilities.yale.yale-bad-battery-reporter.fingerprints") + for _, fingerprint in ipairs(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 diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua similarity index 86% rename from drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua index 3b77f32563..358dcbd7b1 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua @@ -4,9 +4,6 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" - - - local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) end @@ -20,7 +17,7 @@ local bad_yale_driver = { } } }, - can_handle = require("yale.yale-bad-battery-reporter.can_handle"), + can_handle = require("using-old-capabilities.yale.yale-bad-battery-reporter.can_handle") } return bad_yale_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua deleted file mode 100644 index a80632bf80..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua +++ /dev/null @@ -1,14 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local yale_fingerprint_lock_models = function(opts, driver, device) - local FINGERPRINTS = require("yale-fingerprint-lock.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("yale-fingerprint-lock") - end - end - return false -end - -return yale_fingerprint_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua deleted file mode 100644 index 67169e9268..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua +++ /dev/null @@ -1,14 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local is_bad_yale_lock_models = function(opts, driver, device) - local FINGERPRINTS = require("yale.yale-bad-battery-reporter.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("yale.yale-bad-battery-reporter") - end - end - return false -end - -return is_bad_yale_lock_models From 2f1d62f12f52dcb2beff5958203217fa62683c35 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 1 May 2026 13:35:20 -0500 Subject: [PATCH 2/7] Restructure zigbee-lock First, I had AI do the following: - Rename subdrivers: using-old-capabilities -> legacy-handlers, promote using-new-capabilities handlers to top-level defaults - Make new capability handling the driver default; legacy-handlers subdriver overrides for pre-migration devices only - Top-level vendor subdrivers (yale, samsungsds, yale-fingerprint-lock) now only activate for migrated devices to avoid double-dispatch - Remove duplicate lock-without-codes top-level subdriver (handled entirely within legacy-handlers since these devices never migrate) - Add init lifecycle handler to legacy-handlers to populate lockCodes state for pre-migration devices on driver restart Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Then, I cleaned up the driver further by fixing the git understanding of what files were made in what year, and by introducing small changes to altogether remove some sub-subdrivers from the legacy subdriver, and vice versa. --- .../can_handle.lua | 6 +- .../fingerprints.lua | 4 +- .../init.lua | 11 +- drivers/SmartThings/zigbee-lock/src/init.lua | 553 +++++++++++++++++- .../can_handle.lua | 6 +- .../init.lua | 60 +- .../legacy_lock_utils.lua} | 51 +- .../lock-without-codes/can_handle.lua | 6 +- .../lock-without-codes/fingerprints.lua | 0 .../lock-without-codes/init.lua | 5 +- .../src/legacy-handlers/sub_drivers.lua | 10 + .../yale-fingerprint-lock/can_handle.lua | 6 +- .../yale-fingerprint-lock/init.lua | 4 +- .../yale/can_handle.lua | 7 +- .../yale/init.lua | 5 +- .../samsungsds/can_handle.lua | 7 +- .../samsungsds/init.lua | 18 +- .../zigbee-lock/src/sub_drivers.lua | 6 +- .../test_zigbee_lock_code_slga_migration.lua | 15 +- .../test_zigbee_lock_new_capabilities.lua | 15 +- .../test_zigbee_yale-new_capabilities.lua | 15 +- .../src/using-new-capabilities/can_handle.lua | 13 - .../lock-without-codes/can_handle.lua | 17 - .../lock-without-codes/init.lua | 87 --- .../samsungsds/init.lua | 118 ---- .../using-new-capabilities/sub_drivers.lua | 11 - .../yale-fingerprint-lock/can_handle.lua | 19 - .../src/using-new-capabilities/yale/init.lua | 171 ------ .../yale/sub_drivers.lua | 8 - .../yale-bad-battery-reporter/can_handle.lua | 21 - .../yale/yale-bad-battery-reporter/init.lua | 34 -- .../samsungsds/can_handle.lua | 10 - .../using-old-capabilities/sub_drivers.lua | 11 - .../yale/can_handle.lua | 10 - .../yale/sub_drivers.lua | 8 - .../src/yale-fingerprint-lock/can_handle.lua | 18 + .../yale-fingerprint-lock/fingerprints.lua | 0 .../yale-fingerprint-lock/init.lua | 20 +- ...w_lock_utils.lua => zigbee_lock_utils.lua} | 18 +- 39 files changed, 665 insertions(+), 739 deletions(-) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities/yale/yale-bad-battery-reporter => bad-battery-reporter}/can_handle.lua (57%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities/yale/yale-bad-battery-reporter => bad-battery-reporter}/fingerprints.lua (79%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities/yale/yale-bad-battery-reporter => bad-battery-reporter}/init.lua (74%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/can_handle.lua (79%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/init.lua (87%) rename drivers/SmartThings/zigbee-lock/src/{lock_utils.lua => legacy-handlers/legacy_lock_utils.lua} (50%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/lock-without-codes/can_handle.lua (60%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/lock-without-codes/fingerprints.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/lock-without-codes/init.lua (96%) create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/yale-fingerprint-lock/can_handle.lua (59%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/yale-fingerprint-lock/init.lua (85%) rename drivers/SmartThings/zigbee-lock/src/{using-new-capabilities => legacy-handlers}/yale/can_handle.lua (61%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => legacy-handlers}/yale/init.lua (97%) rename drivers/SmartThings/zigbee-lock/src/{using-new-capabilities => }/samsungsds/can_handle.lua (55%) rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => }/samsungsds/init.lua (86%) delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/sub_drivers.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/sub_drivers.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/sub_drivers.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua rename drivers/SmartThings/zigbee-lock/src/{using-old-capabilities => }/yale-fingerprint-lock/fingerprints.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{using-new-capabilities => }/yale-fingerprint-lock/init.lua (53%) rename drivers/SmartThings/zigbee-lock/src/{new_lock_utils.lua => zigbee_lock_utils.lua} (94%) 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/bad-battery-reporter/can_handle.lua similarity index 57% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/can_handle.lua index 8c3106156a..7d7d566118 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/can_handle.lua @@ -1,11 +1,11 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device) - local fingerprints = require("using-old-capabilities.yale.yale-bad-battery-reporter.fingerprints") + local fingerprints = require("bad-battery-reporter.fingerprints") for _, fingerprint in ipairs(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") + local subdriver = require("bad-battery-reporter") return true, subdriver end end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/fingerprints.lua similarity index 79% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/fingerprints.lua index cbb7c3404f..3024aeeace 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/fingerprints.lua +++ b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/fingerprints.lua @@ -1,7 +1,7 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local BAD_YALE_LOCK_FINGERPRINTS = { +local BAD_BATTERY_REPORTING_LOCK_FINGERPRINTS = { { mfr = "Yale", model = "YRD220/240 TSDB" }, { mfr = "Yale", model = "YRL220 TS LL" }, { mfr = "Yale", model = "YRD210 PB DB" }, @@ -10,4 +10,4 @@ local BAD_YALE_LOCK_FINGERPRINTS = { { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } } -return BAD_YALE_LOCK_FINGERPRINTS +return BAD_BATTERY_REPORTING_LOCK_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/init.lua similarity index 74% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua rename to drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/init.lua index 358dcbd7b1..5fbdabd1b7 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/init.lua @@ -4,12 +4,15 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" + + + 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", +local bad_battery_reporter_driver = { + NAME = "Bad Battery Reporter Driver", zigbee_handlers = { attr = { [clusters.PowerConfiguration.ID] = { @@ -17,7 +20,7 @@ local bad_yale_driver = { } } }, - can_handle = require("using-old-capabilities.yale.yale-bad-battery-reporter.can_handle") + can_handle = require("bad-battery-reporter.can_handle") } -return bad_yale_driver +return bad_battery_reporter_driver diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 3b72804fac..1c4c74a29e 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -4,26 +4,36 @@ -- 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 new_lock_utils = require "new_lock_utils" +local legacy_lock_utils = require "legacy-handlers.legacy_lock_utils" +local lock_utils = require "zigbee_lock_utils" local DELAY_LOCK_EVENT = "_delay_lock_event" local MAX_DELAY = 10 + local refresh = function(driver, device, cmd) device:refresh() device:send(LockCluster.attributes.LockState:read(device)) @@ -31,10 +41,10 @@ local refresh = function(driver, device, cmd) -- we can't determine from fingerprints if devices support lock codes, so -- here in the driver we'll do a check once to see if the device responds here -- and if it does, we'll switch it to a profile with lock codes - if not device:supports_capability_by_id(LockCodes.ID) and not device:get_field(lock_utils.CHECKED_CODE_SUPPORT) then + if not device:supports_capability_by_id(LockCodes.ID) and not device:get_field(legacy_lock_utils.CHECKED_CODE_SUPPORT) then device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) -- we won't make this value persist because it's not that important - device:set_field(lock_utils.CHECKED_CODE_SUPPORT, true) + device:set_field(legacy_lock_utils.CHECKED_CODE_SUPPORT, true) end end @@ -49,20 +59,19 @@ 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 triggers setting the migrated field so new devices use new capabilities from the start. local function 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 } })) device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) - new_lock_utils.reload_tables(device) + lock_utils.reload_tables(device) else - lock_utils.populate_state_from_data(device) + legacy_lock_utils.populate_state_from_data(device) end else - lock_utils.populate_state_from_data(device) + legacy_lock_utils.populate_state_from_data(device) end driver:inject_capability_command(device, { @@ -72,12 +81,6 @@ local function device_added(driver, device) }) end -local function init(driver, device) - lock_utils.populate_state_from_data(device) - -- temp fix before this can be changed to non-persistent - device:set_field(lock_utils.CODE_STATE, nil, { persist = true }) -end - -- The following two functions are from the lock defaults. They are in the base driver temporarily -- until the fix is widely released in the lua libs local lock_state_handler = function(driver, device, value, zb_rx) @@ -102,18 +105,501 @@ local lock_state_handler = function(driver, device, value, zb_rx) end end -local function lock(driver, device, command) +local function handle_lock(driver, device, command) device:send_to_component(command.component, LockCluster.server.commands.LockDoor(device)) end -local function unlock(driver, device, command) +local function handle_unlock(driver, device, command) device:send_to_component(command.component, LockCluster.server.commands.UnlockDoor(device)) end +-- Default (post-migration) handlers -- + +local reload_all_codes = function(device) + -- starts at first user code index then iterates through all lock codes as they come in + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) == nil) then + device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + end + if (device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfTotalUsersSupported:read(device)) + end + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then + device:set_field(lock_utils.CHECKING_CODE, 1) + end + + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +end + +local init = function(driver, device) + lock_utils.reload_tables(device) + device.thread:call_with_delay(15, function(d) + reload_all_codes(device) + end) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) + + device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) + device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) + + device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) + device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) + + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + device.thread:call_with_delay(2, function(d) reload_all_codes(device) end) + else + -- Don't send a reload all codes if this is a part of migration + if device.data.lockCodes == nil or device:get_field(legacy_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(legacy_lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) + end + end +end + +local add_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then + return + end + local available_index = lock_utils.get_available_user_index(device) + local status = lock_utils.STATUS_SUCCESS + if available_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + else + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) + lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) + end + + if status == lock_utils.STATUS_SUCCESS then + lock_utils.send_events(device, lock_utils.LOCK_USERS) + end + + lock_utils.clear_busy_state(device, status) +end + +local update_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then + return + end + + local user_name = command.args.userName + local user_type = command.args.userType + local user_index = tonumber(command.args.userIndex) + local current_users = lock_utils.get_users(device) + local status = lock_utils.STATUS_FAILURE + + for _, user in pairs(current_users) do + if user.userIndex == user_index then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index}) + user.userName = user_name + user.userType = user_type + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + status = lock_utils.STATUS_SUCCESS + break + end + end + + lock_utils.clear_busy_state(device, status) +end + +local delete_user_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then + return + end + local status = lock_utils.STATUS_SUCCESS + local user_index = tonumber(command.args.userIndex) + if lock_utils.get_user(device, user_index) ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) + end + + local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) + if associated_credential ~= nil then + -- if there is an associated credential with this user then delete the credential + -- this command also handles the user deletion + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteCredential.NAME, + args = { associated_credential.credentialIndex, "pin" }, + override_busy_check = true + }) + else + lock_utils.delete_user(device, user_index) + lock_utils.send_events(device, lock_utils.LOCK_USERS) + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end + else + status = lock_utils.STATUS_FAILURE + lock_utils.clear_busy_state(device, status, command.override_busy_check) + end +end + +local delete_all_users_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then + return + end + local status = lock_utils.STATUS_SUCCESS + local current_users = lock_utils.get_users(device) + + local delay = 0 + for _, user in pairs(current_users) do + device.thread:call_with_delay(delay, function() + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteUser.NAME, + args = {user.userIndex}, + override_busy_check = true + }) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +local add_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local user_index = tonumber(command.args.userIndex) + local user_type = command.args.userType + local credential_type = command.args.credentialType + local credential_data = command.args.credentialData + local status = lock_utils.STATUS_SUCCESS + + local credential_index = lock_utils.get_available_credential_index(device) + if credential_index == nil then + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then + status = lock_utils.STATUS_OCCUPIED + elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then + status = lock_utils.STATUS_FAILURE + end + + if user_index == 0 then + user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, user_type, user_index) + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + + if status == lock_utils.STATUS_SUCCESS then + -- set the pin code and then validate it was successful when the GetPINCode response is received. + -- the credential creation and events will also be handled in that response. + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) + device:send(LockCluster.server.commands.SetPINCode(device, + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) + ) + device.thread:call_with_delay(4, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, status) + end +end + +local update_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credential_index = tonumber(command.args.credentialIndex) + local credential_data = command.args.credentialData + local credential = lock_utils.get_credential(device, credential_index) + + if credential ~= nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + device:send(LockCluster.server.commands.SetPINCode(device, + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) + ) + device.thread:call_with_delay(4, function() + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) + end +end + +local delete_credential_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then + return + end + + local credential_index = tonumber(command.args.credentialIndex) + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + if command.override_busy_check == nil then + device:set_field(lock_utils.ACTIVE_CREDENTIAL, + { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) + end + + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + else + lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) + end +end + +local delete_all_credentials_handler = function(driver, device, command) + if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then + return + end + local credentials = lock_utils.get_credentials(device) + local status = lock_utils.STATUS_SUCCESS + local delay = 0 + for _, credential in pairs(credentials) do + local credential_index = tonumber(credential.credentialIndex) + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) + end) + device.thread:call_with_delay(delay + 2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + delay = delay + 2 + end + + device.thread:call_with_delay(delay + 4, function() + lock_utils.clear_busy_state(device, status) + end) +end + +local max_code_length_handler = function(driver, device, value) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +local min_code_length_handler = function(driver, device, value) + device:emit_event(capabilities.lockCredentials.minPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +local max_codes_handler = function(driver, device, value) + device:emit_event(capabilities.lockUsers.totalUsersSupported(value.value, {visibility = {displayed = false}})) + device:emit_event(capabilities.lockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) +end + +local get_pin_response_handler = function(driver, device, zb_mess) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_NAME) + local status = lock_utils.STATUS_SUCCESS + local emit_event = false + + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then + -- create credential if not already present. + if lock_utils.get_credential(device, credential_index) == nil then + lock_utils.add_credential(device, + active_credential.userIndex, + active_credential.credentialType, + credential_index) + emit_event = true + end + elseif command ~= nil and command.name == lock_utils.UPDATE_CREDENTIAL then + -- update credential + local credential = lock_utils.get_credential(device, credential_index) + if credential ~= nil then + lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) + emit_event = true + end + else + -- Called by reloading the codes. Don't add if already in table. + if lock_utils.get_credential(device, credential_index) == nil then + local new_user_index = lock_utils.get_available_user_index(device) + if new_user_index ~= nil then + lock_utils.create_user(device, nil, "guest", new_user_index) + lock_utils.add_credential(device, + new_user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_event = true + else + status = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + end + end + elseif zb_mess.body.zcl_body.user_status.value == UserStatusEnum.AVAILABLE and command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then + -- tried to add a code that already is in use. + -- remove the created user if one got made. There is no associated credential. + status = lock_utils.STATUS_DUPLICATE + lock_utils.delete_user(device, active_credential.userIndex) + else + if lock_utils.get_credential(device, credential_index) ~= nil then + -- Credential has been deleted. + lock_utils.delete_credential(device, credential_index) + emit_event = true + end + end + + if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then + -- the credential we're checking has arrived + local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, + capabilities.lockCredentials.pinUsersSupported.NAME) + if (credential_index >= last_slot) then + device:set_field(lock_utils.CHECKING_CODE, nil) + emit_event = true + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) + end + end + + if emit_event then + lock_utils.send_events(device) + end + -- ignore handling the busy state for these commands, they are handled within their own handlers + if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then + lock_utils.clear_busy_state(device, status) + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) + local command = device:get_field(lock_utils.COMMAND_NAME) + local emit_events = false + + if credential_index >= 256 then -- Index is incorrectly written, attempt to shift it to get an actual 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 = true } } + )) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (credential_index == lock_utils.DELETE_ALL_USERS) then + -- All credentials deleted + for _, credential in pairs(lock_utils.get_credentials(device)) do + lock_utils.delete_credential(device, credential.credentialIndex) + emit_events = true + end + else + -- One credential deleted + if (lock_utils.get_credential(device, credential_index) ~= nil) then + lock_utils.delete_credential(device, credential_index) + emit_events = true + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + if lock_utils.get_credential(device, credential_index) == nil and command == nil then + local user_index = lock_utils.get_available_user_index(device) + if user_index ~= nil then + lock_utils.create_user(device, nil, "guest", user_index) + lock_utils.add_credential(device, + user_index, + lock_utils.CREDENTIAL_TYPE, + credential_index) + emit_events = true + end + end + end + + if emit_events then + lock_utils.send_events(device) + end +end + +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 zigbee_lock_driver = { supported_capabilities = { Lock, - LockCodes, + LockCredentials, + LockUsers, Battery, }, zigbee_handlers = { @@ -121,30 +607,51 @@ 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] = max_code_length_handler, + [LockCluster.attributes.MinPINCodeLength.ID] = min_code_length_handler, + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = max_codes_handler, } } }, capability_handlers = { [Lock.ID] = { - [Lock.commands.lock.NAME] = lock, - [Lock.commands.unlock.NAME] = unlock, + [Lock.commands.lock.NAME] = handle_lock, + [Lock.commands.unlock.NAME] = handle_unlock, }, [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = refresh - } + }, + [LockUsers.ID] = { + [LockUsers.commands.addUser.NAME] = add_user_handler, + [LockUsers.commands.updateUser.NAME] = update_user_handler, + [LockUsers.commands.deleteUser.NAME] = delete_user_handler, + [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, + }, + [LockCredentials.ID] = { + [LockCredentials.commands.addCredential.NAME] = add_credential_handler, + [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, + [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, + [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, + }, }, sub_drivers = require("sub_drivers"), lifecycle_handlers = { added = device_added, init = init, + doConfigure = do_configure, }, health_check = false, } defaults.register_for_default_handlers(zigbee_lock_driver, zigbee_lock_driver.supported_capabilities) -local lock = ZigbeeDriver("zigbee-lock", zigbee_lock_driver) -lock:run() +local driver = ZigbeeDriver("zigbee-lock", zigbee_lock_driver) +driver:run() diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua similarity index 79% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua index 1fe9815bb9..a37b71e079 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua @@ -1,4 +1,4 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, ...) @@ -6,8 +6,8 @@ return function(opts, driver, device, ...) local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) if not lock_codes_migrated then - local subdriver = require("using-old-capabilities") + local subdriver = require("legacy-handlers") return true, subdriver end return false -end \ No newline at end of file +end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua similarity index 87% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua index f8fcae368f..b167d7f75f 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua @@ -1,25 +1,9 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- Zigbee Driver utilities -local device_management = require "st.zigbee.device_management" +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- 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" @@ -35,7 +19,7 @@ local UserTypeEnum = LockCluster.types.DrlkUserType local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode local socket = require "cosock.socket" -local lock_utils = require "lock_utils" +local lock_utils = require "legacy-handlers.legacy_lock_utils" local DELAY_LOCK_EVENT = "_delay_lock_event" local MAX_DELAY = 10 @@ -342,32 +326,8 @@ local migrate = function(driver, device, command) 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", +local legacy_capabilities_driver = { + NAME = "Lock Driver Using LockCodes Capability", supported_capabilities = { Lock, LockCodes, @@ -400,14 +360,12 @@ local old_capabilities_driver = { [LockCodes.commands.migrate.NAME] = migrate, }, }, - sub_drivers = { - require("using-old-capabilities.sub_drivers") - }, + sub_drivers = require("legacy-handlers.sub_drivers"), health_check = false, lifecycle_handlers = { - doConfigure = do_configure + init = function(driver, device) lock_utils.populate_state_from_data(device) end, }, - can_handle = require("using-old-capabilities.can_handle") + can_handle = require("legacy-handlers.can_handle") } -return old_capabilities_driver +return legacy_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/legacy_lock_utils.lua similarity index 50% rename from drivers/SmartThings/zigbee-lock/src/lock_utils.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/legacy_lock_utils.lua index d9c06f41f6..6afa0aea3a 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/legacy_lock_utils.lua @@ -1,12 +1,13 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 + local utils = require "st.utils" local capabilities = require "st.capabilities" local json = require "st.json" local LockCodes = capabilities.lockCodes -local lock_utils = { +local legacy_lock_utils = { -- Constants LOCK_CODES = "lockCodes", LOCK_USERS = "lockUsers", @@ -17,57 +18,57 @@ local lock_utils = { CHECKED_CODE_SUPPORT = "checkedCodeSupport", } -lock_utils.get_lock_codes = function(device) - local lc = device:get_field(lock_utils.LOCK_CODES) +legacy_lock_utils.get_lock_codes = function(device) + local lc = device:get_field(legacy_lock_utils.LOCK_CODES) return lc ~= nil and lc or {} end -lock_utils.lock_codes_event = function(device, lock_codes) - device:set_field(lock_utils.LOCK_CODES, lock_codes, { persist = true } ) +legacy_lock_utils.lock_codes_event = function(device, lock_codes) + device:set_field(legacy_lock_utils.LOCK_CODES, lock_codes, { persist = true } ) device:emit_event(capabilities.lockCodes.lockCodes(json.encode(utils.deep_copy(lock_codes)), { visibility = { displayed = false } })) end -function lock_utils.get_code_name(device, code_id) - if (device:get_field(lock_utils.CODE_STATE) ~= nil and device:get_field(lock_utils.CODE_STATE)["setName"..code_id] ~= nil) then +function legacy_lock_utils.get_code_name(device, code_id) + if (device:get_field(legacy_lock_utils.CODE_STATE) ~= nil and device:get_field(legacy_lock_utils.CODE_STATE)["setName"..code_id] ~= nil) then -- this means a code set operation succeeded - return device:get_field(lock_utils.CODE_STATE)["setName"..code_id] - elseif (lock_utils.get_lock_codes(device)[code_id] ~= nil) then - return lock_utils.get_lock_codes(device)[code_id] + return device:get_field(legacy_lock_utils.CODE_STATE)["setName"..code_id] + elseif (legacy_lock_utils.get_lock_codes(device)[code_id] ~= nil) then + return legacy_lock_utils.get_lock_codes(device)[code_id] else return "Code " .. code_id end end -function lock_utils.get_change_type(device, code_id) - if (lock_utils.get_lock_codes(device)[code_id] == nil) then +function legacy_lock_utils.get_change_type(device, code_id) + if (legacy_lock_utils.get_lock_codes(device)[code_id] == nil) then return " set" else return " changed" end end -function lock_utils.reset_code_state(device, code_slot) - local codeState = device:get_field(lock_utils.CODE_STATE) +function legacy_lock_utils.reset_code_state(device, code_slot) + local codeState = device:get_field(legacy_lock_utils.CODE_STATE) if (codeState ~= nil) then codeState["setName".. code_slot] = nil codeState["setCode".. code_slot] = nil - device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) + device:set_field(legacy_lock_utils.CODE_STATE, codeState, { persist = true }) end end -function lock_utils.code_deleted(device, code_slot) - local lock_codes = lock_utils.get_lock_codes(device) +function legacy_lock_utils.code_deleted(device, code_slot) + local lock_codes = legacy_lock_utils.get_lock_codes(device) local event = LockCodes.codeChanged(code_slot.." deleted", { state_change = true }) - event.data = {codeName = lock_utils.get_code_name(device, code_slot)} + event.data = {codeName = legacy_lock_utils.get_code_name(device, code_slot)} lock_codes[code_slot] = nil device:emit_event(event) - lock_utils.reset_code_state(device, code_slot) + legacy_lock_utils.reset_code_state(device, code_slot) return lock_codes end -function lock_utils.populate_state_from_data(device) - if device.data.lockCodes ~= nil and device:get_field(lock_utils.MIGRATION_COMPLETE) ~= true then +function legacy_lock_utils.populate_state_from_data(device) + if device.data.lockCodes ~= nil and device:get_field(legacy_lock_utils.MIGRATION_COMPLETE) ~= true then -- build the lockCodes table local lockCodes = {} local lc_data = json.decode(device.data.lockCodes) @@ -75,14 +76,14 @@ function lock_utils.populate_state_from_data(device) lockCodes[k] = v end -- Populate the devices `lockCodes` field - device:set_field(lock_utils.LOCK_CODES, utils.deep_copy(lockCodes), { persist = true }) + device:set_field(legacy_lock_utils.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(lock_utils.MIGRATION_COMPLETE, true, { persist = true }) + device:set_field(legacy_lock_utils.MIGRATION_COMPLETE, true, { persist = true }) end end -return lock_utils +return legacy_lock_utils diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/can_handle.lua similarity index 60% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/can_handle.lua index 0eac7666c6..34b3a5a280 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/can_handle.lua @@ -1,11 +1,11 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, cmd) - local fingerprints = require("using-old-capabilities.lock-without-codes.fingerprints") + local fingerprints = require("legacy-handlers.lock-without-codes.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("using-old-capabilities.lock-without-codes") + local subdriver = require("legacy-handlers.lock-without-codes") return true, subdriver end end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/init.lua similarity index 96% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/init.lua index 743a2cc6f6..4522580297 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/init.lua @@ -8,6 +8,9 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration + + + local function device_init(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then @@ -70,7 +73,7 @@ local lock_without_codes = { } } }, - can_handle = require("using-old-capabilities.lock-without-codes.can_handle") + can_handle = require("legacy-handlers.lock-without-codes.can_handle") } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua new file mode 100644 index 0000000000..8cbfd6701f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("legacy-handlers.yale"), + lazy_load_if_possible("legacy-handlers.yale-fingerprint-lock"), + lazy_load_if_possible("legacy-handlers.lock-without-codes") +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/can_handle.lua similarity index 59% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/can_handle.lua index a281fc16da..3baa8f368c 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/can_handle.lua @@ -1,11 +1,11 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device) - local fingerprints = require("using-old-capabilities.yale-fingerprint-lock.fingerprints") + local fingerprints = require("yale-fingerprint-lock.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("using-old-capabilities.yale-fingerprint-lock") + local subdriver = require("legacy-handlers.yale-fingerprint-lock") return true, subdriver end end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/init.lua similarity index 85% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/init.lua index 12619c01d5..7e982fd943 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 @@ -21,7 +21,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = require("using-old-capabilities.yale-fingerprint-lock.can_handle") + can_handle = require("legacy-handlers.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/legacy-handlers/yale/can_handle.lua similarity index 61% rename from drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/can_handle.lua index ecefc56b4c..832914f6ea 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/can_handle.lua @@ -1,10 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -return function(opts, driver, device) +local function yale_can_handle(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 + return true, require("legacy-handlers.yale") end return false end + +return yale_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/init.lua similarity index 97% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/init.lua index 4d6b8f8431..a85a9f8798 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/init.lua @@ -15,7 +15,7 @@ local LockCodes = capabilities.lockCodes local UserStatusEnum = LockCluster.types.DrlkUserStatus local UserTypeEnum = LockCluster.types.DrlkUserType -local lock_utils = (require "lock_utils") +local lock_utils = (require "legacy-handlers.legacy_lock_utils") local reload_all_codes = function(driver, device, command) -- starts at first user code index then iterates through all lock codes as they come in @@ -142,8 +142,7 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - sub_drivers = require("using-old-capabilities.yale.sub_drivers"), - can_handle = require("using-old-capabilities.yale.can_handle") + can_handle = require("legacy-handlers.yale.can_handle") } return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua similarity index 55% rename from drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua index f9ff67fcf5..c483b2fe27 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua @@ -1,10 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -return function(opts, driver, device) +local function samsungsds_can_handle(opts, driver, device, ...) if device:get_manufacturer() == "SAMSUNG SDS" then - local subdriver = require("using-new-capabilities.samsungsds") - return true, subdriver + return true, require("samsungsds") end return false end + +return samsungsds_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua similarity index 86% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua rename to drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index 51418f6e03..060b7d013d 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -10,7 +10,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 "lock_utils" +local lock_utils = require "zigbee_lock_utils" local SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND = 0x1F local SAMSUNG_SDS_MFR_CODE = 0x0003 @@ -53,8 +53,18 @@ local function emit_event_if_latest_state_missing(device, component, capability, end end +local function load_device_state(device) + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + lock_utils.reload_tables(device) + else + local legacy_lock_utils = require "legacy-handlers.legacy_lock_utils" + legacy_lock_utils.populate_state_from_data(device) + end +end + local device_added = function(self, device) - lock_utils.populate_state_from_data(device) + load_device_state(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 @@ -71,7 +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.populate_state_from_data(device) + load_device_state(device) end local samsung_sds_driver = { @@ -102,7 +112,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = require("using-old-capabilities.samsungsds.can_handle") + can_handle = require("samsungsds.can_handle") } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua index e7ed53082b..d3491ed664 100644 --- a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua @@ -3,7 +3,9 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("using-old-capabilities"), - lazy_load_if_possible("using-new-capabilities"), + lazy_load_if_possible("legacy-handlers"), + lazy_load_if_possible("samsungsds"), + lazy_load_if_possible("bad-battery-reporter"), + lazy_load_if_possible("yale-fingerprint-lock"), } return sub_drivers 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 d0de6d6824..0fde1e1cd5 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 @@ -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 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 d23423983d..5f6eb6ce5c 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 @@ -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 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 73030a792e..14a183adfe 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,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 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_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 deleted file mode 100644 index 5618b59b30..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/can_handle.lua +++ /dev/null @@ -1,13 +0,0 @@ --- 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/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/can_handle.lua deleted file mode 100644 index 21984e938b..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/can_handle.lua +++ /dev/null @@ -1,17 +0,0 @@ --- 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 deleted file mode 100644 index 8363377bf9..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua +++ /dev/null @@ -1,87 +0,0 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -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 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 = 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/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua deleted file mode 100644 index ed61935e4b..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua +++ /dev/null @@ -1,118 +0,0 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -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 lock_utils = require "new_lock_utils" - -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) - 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 - -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) - lock_utils.reload_tables(device) -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 = 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 deleted file mode 100644 index 8167f51d98..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/sub_drivers.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require "lazy_load_subdriver" -local sub_drivers = { - lazy_load_if_possible("using-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 deleted file mode 100644 index 6f206ed9b5..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/can_handle.lua +++ /dev/null @@ -1,19 +0,0 @@ --- 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/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua deleted file mode 100644 index 2e88e950f1..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua +++ /dev/null @@ -1,171 +0,0 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- Zigbee Spec Utils -local clusters = require "st.zigbee.zcl.clusters" -local LockCluster = clusters.DoorLock - --- Capabilities -local capabilities = require "st.capabilities" - --- Enums -local UserStatusEnum = LockCluster.types.DrlkUserStatus -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 - lock_utils.send_events(device) - end - -- ignore handling the busy state for these commands, they are handled within their own handlers - if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then - lock_utils.clear_busy_state(device, status) - end -end - -local programming_event_handler = function(driver, device, zb_mess) - local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) - local command = device:get_field(lock_utils.COMMAND_NAME) - local emit_events = false - - if 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 = 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 == 0xFFFF) then - -- All credentials deleted - for _, credential in pairs(lock_utils.get_credentials(device)) do - lock_utils.delete_credential(device, credential.credentialIndex) - emit_events = true - end - else - -- One credential deleted - if (lock_utils.get_credential(device, credential_index) ~= nil) then - lock_utils.delete_credential(device, credential_index) - emit_events = true - end - end - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or - zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then - if lock_utils.get_credential(device, credential_index) == nil and command == nil then - local user_index = lock_utils.get_available_user_index(device) - if user_index ~= nil then - lock_utils.create_user(device, nil, "guest", user_index) - lock_utils.add_credential(device, - user_index, - lock_utils.CREDENTIAL_TYPE, - credential_index) - emit_events = true - end - end - end - - if emit_events then - lock_utils.send_events(device) - end -end - -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.sub_drivers") }, - can_handle = require("using-new-capabilities.yale.can_handle") -} - -return yale_door_lock_driver 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 deleted file mode 100644 index 19d92e6f3e..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/sub_drivers.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require "lazy_load_subdriver" -local sub_drivers = { - lazy_load_if_possible("using-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 deleted file mode 100644 index 39fffc666c..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/can_handle.lua +++ /dev/null @@ -1,21 +0,0 @@ --- 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 deleted file mode 100644 index 0a1bcaceb3..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua +++ /dev/null @@ -1,34 +0,0 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -local clusters = require "st.zigbee.zcl.clusters" -local capabilities = require "st.capabilities" - -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 = 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/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua deleted file mode 100644 index c2419a7b01..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/can_handle.lua +++ /dev/null @@ -1,10 +0,0 @@ --- 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/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/sub_drivers.lua deleted file mode 100644 index 1f6149587c..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/sub_drivers.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require "lazy_load_subdriver" -local sub_drivers = { - lazy_load_if_possible("using-old-capabilities.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/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua deleted file mode 100644 index 25a4a6caee..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/can_handle.lua +++ /dev/null @@ -1,10 +0,0 @@ --- 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/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua deleted file mode 100644 index d31cfb314a..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/sub_drivers.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require "lazy_load_subdriver" -local sub_drivers = { - lazy_load_if_possible("using-old-capabilities.yale.yale-bad-battery-reporter"), -} -return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua new file mode 100644 index 0000000000..84a9918622 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local yale_fingerprint_lock_models = 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 return false end + local FINGERPRINTS = require("yale-fingerprint-lock.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yale-fingerprint-lock") + end + end + return false +end + +return yale_fingerprint_lock_models \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua similarity index 53% rename from drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua rename to drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua index d0ab156b65..9965ef181e 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.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 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -20,6 +10,8 @@ local LockUsers = capabilities.lockUsers local YALE_FINGERPRINT_MAX_CODES = 0x1E + + 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)) @@ -34,7 +26,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = require("using-new-capabilities.yale-fingerprint-lock.can_handle") + can_handle = require("yale-fingerprint-lock.can_handle") } return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua similarity index 94% rename from drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua rename to drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua index 9d5848b21b..6000f6a804 100644 --- a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/zigbee_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 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" local INITIAL_INDEX = 1 @@ -45,6 +35,8 @@ local new_lock_utils = { USER_TYPE = "userType" } +new_lock_utils.DELETE_ALL_USERS = 0xFF + -- 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) From c02dbd6122294d99ec6191165d7ddd5ee89a5b62 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Sun, 3 May 2026 16:36:20 -0500 Subject: [PATCH 3/7] fixup! remove unused file --- .../src/using-new-capabilities/init.lua | 570 ------------------ 1 file changed, 570 deletions(-) delete mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua deleted file mode 100644 index 4b3c6a4096..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ /dev/null @@ -1,570 +0,0 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- Zigbee Driver utilities -local device_management = require "st.zigbee.device_management" -local log = require "log" -local utils = require "st.utils" - - --- Zigbee Spec Utils -local clusters = require "st.zigbee.zcl.clusters" -local Alarm = clusters.Alarms -local LockCluster = clusters.DoorLock -local PowerConfiguration = clusters.PowerConfiguration - --- Capabilities -local capabilities = require "st.capabilities" -local Battery = capabilities.battery -local Lock = capabilities.lock -local LockCredentials = capabilities.lockCredentials -local LockUsers = capabilities.lockUsers - --- Enums -local UserStatusEnum = LockCluster.types.DrlkUserStatus -local UserTypeEnum = LockCluster.types.DrlkUserType -local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode - -local socket = require "cosock.socket" -local lock_utils = require "new_lock_utils" - -local DELAY_LOCK_EVENT = "_delay_lock_event" -local MAX_DELAY = 10 - -local reload_all_codes = function(device) - -- starts at first user code index then iterates through all lock codes as they come in - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) == nil) then - device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) == nil) then - device:send(LockCluster.attributes.MinPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) == nil) then - device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) - end - if (device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then - device:send(LockCluster.attributes.NumberOfTotalUsersSupported:read(device)) - end - if (device:get_field(lock_utils.CHECKING_CODE) == nil) then - device:set_field(lock_utils.CHECKING_CODE, 1) - end - - device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) -end - -local init = function(driver, device) - lock_utils.reload_tables(device) - device.thread:call_with_delay(15, function(d) - reload_all_codes(device) - end) -end - -local do_configure = function(self, device) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) - - device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) - device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) - - device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) - device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) - - device.thread:call_with_delay(2, function(d) - reload_all_codes(device) - end) -end - -local add_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_USER, type = lock_utils.LOCK_USERS}) then - return - end - local available_index = lock_utils.get_available_user_index(device) - local status = lock_utils.STATUS_SUCCESS - if available_index == nil then - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - else - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = available_index}) - lock_utils.create_user(device, command.args.userName, command.args.userType, available_index) - end - - if status == lock_utils.STATUS_SUCCESS then - lock_utils.send_events(device, lock_utils.LOCK_USERS) - end - - lock_utils.clear_busy_state(device, status) -end - -local update_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_USER, type = lock_utils.LOCK_USERS}) then - return - end - - local user_name = command.args.userName - local user_type = command.args.userType - local user_index = tonumber(command.args.userIndex) - local current_users = lock_utils.get_users(device) - local status = lock_utils.STATUS_FAILURE - - for _, user in pairs(current_users) do - if user.userIndex == user_index then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index}) - user.userName = user_name - user.userType = user_type - device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) - lock_utils.send_events(device, lock_utils.LOCK_USERS) - status = lock_utils.STATUS_SUCCESS - break - end - end - - lock_utils.clear_busy_state(device, status) -end - -local delete_user_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_USER, type = lock_utils.LOCK_USERS}, command.override_busy_check) then - return - end - local status = lock_utils.STATUS_SUCCESS - local user_index = tonumber(command.args.userIndex) - if lock_utils.get_user(device, user_index) ~= nil then - if command.override_busy_check == nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, { userIndex = user_index }) - end - - local associated_credential = lock_utils.get_credential_by_user_index(device, user_index) - if associated_credential ~= nil then - -- if there is an associated credential with this user then delete the credential - -- this command also handles the user deletion - driver:inject_capability_command(device, { - capability = capabilities.lockCredentials.ID, - command = capabilities.lockCredentials.commands.deleteCredential.NAME, - args = { associated_credential.credentialIndex, "pin" }, - override_busy_check = true - }) - else - lock_utils.delete_user(device, user_index) - lock_utils.send_events(device, lock_utils.LOCK_USERS) - lock_utils.clear_busy_state(device, status, command.override_busy_check) - end - else - status = lock_utils.STATUS_FAILURE - lock_utils.clear_busy_state(device, status, command.override_busy_check) - end -end - -local delete_all_users_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_USERS, type = lock_utils.LOCK_USERS}) then - return - end - local status = lock_utils.STATUS_SUCCESS - local current_users = lock_utils.get_users(device) - - local delay = 0 - for _, user in pairs(current_users) do - device.thread:call_with_delay(delay, function() - driver:inject_capability_command(device, { - capability = capabilities.lockUsers.ID, - command = capabilities.lockUsers.commands.deleteUser.NAME, - args = {user.userIndex}, - override_busy_check = true - }) - end) - delay = delay + 2 - end - - device.thread:call_with_delay(delay + 4, function() - lock_utils.clear_busy_state(device, status) - end) -end - -local add_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.ADD_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local user_index = tonumber(command.args.userIndex) - local user_type = command.args.userType - local credential_type = command.args.credentialType - local credential_data = command.args.credentialData - local status = lock_utils.STATUS_SUCCESS - - local credential_index = lock_utils.get_available_credential_index(device) - if credential_index == nil then - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - elseif user_index ~= 0 and lock_utils.get_credential_by_user_index(device, user_index) then - status = lock_utils.STATUS_OCCUPIED - elseif user_index ~= 0 and lock_utils.get_user(device, user_index) == nil then - status = lock_utils.STATUS_FAILURE - end - - if user_index == 0 then - user_index = lock_utils.get_available_user_index(device) - if user_index ~= nil then - lock_utils.create_user(device, nil, user_type, user_index) - else - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - end - end - - if status == lock_utils.STATUS_SUCCESS then - -- set the pin code and then validate it was successful when the GetPINCode response is received. - -- the credential creation and events will also be handled in that response. - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = user_index, userType = user_type, credentialType = credential_type, credentialIndex = credential_index }) - device:send(LockCluster.server.commands.SetPINCode(device, - credential_index, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - credential_data) - ) - device.thread:call_with_delay(4, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - else - lock_utils.clear_busy_state(device, status) - end -end - -local update_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.UPDATE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local credential_index = tonumber(command.args.credentialIndex) - local credential_data = command.args.credentialData - local credential = lock_utils.get_credential(device, credential_index) - - if credential ~= nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) - device:send(LockCluster.server.commands.SetPINCode(device, - credential_index, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - credential_data) - ) - device.thread:call_with_delay(4, function() - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - else - lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE) - end -end - -local delete_credential_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_CREDENTIAL, type = lock_utils.LOCK_CREDENTIALS}, command.override_busy_check) then - return - end - - local credential_index = tonumber(command.args.credentialIndex) - local credential = lock_utils.get_credential(device, credential_index) - if credential ~= nil then - if command.override_busy_check == nil then - device:set_field(lock_utils.ACTIVE_CREDENTIAL, - { userIndex = credential.userIndex, credentialType = credential.credentialType, credentialIndex = credential.credentialIndex }) - end - - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) - device.thread:call_with_delay(2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - else - lock_utils.clear_busy_state(device, lock_utils.STATUS_FAILURE, command.override_busy_check) - end -end - -local delete_all_credentials_handler = function(driver, device, command) - if lock_utils.busy_check_and_set(device, {name = lock_utils.DELETE_ALL_CREDENTIALS, type = lock_utils.LOCK_CREDENTIALS}) then - return - end - local credentials = lock_utils.get_credentials(device) - local status = lock_utils.STATUS_SUCCESS - local delay = 0 - for _, credential in pairs(credentials) do - local credential_index = tonumber(credential.credentialIndex) - device.thread:call_with_delay(delay, function() - device:send(LockCluster.server.commands.ClearPINCode(device, credential_index)) - end) - device.thread:call_with_delay(delay + 2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) - end) - delay = delay + 2 - end - - device.thread:call_with_delay(delay + 4, function() - lock_utils.clear_busy_state(device, status) - end) -end - -local max_code_length_handler = function(driver, device, value) - device:emit_event(capabilities.lockCredentials.maxPinCodeLen(value.value, { visibility = { displayed = false } })) -end - -local min_code_length_handler = function(driver, device, value) - device:emit_event(capabilities.lockCredentials.minPinCodeLen(value.value, { visibility = { displayed = false } })) -end - -local max_codes_handler = function(driver, device, value) - device:emit_event(capabilities.lockUsers.totalUsersSupported(value.value, {visibility = {displayed = false}})) - device:emit_event(capabilities.lockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) -end - -local get_pin_response_handler = function(driver, device, zb_mess) - local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) - local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) - local command = device:get_field(lock_utils.COMMAND_NAME) - local status = lock_utils.STATUS_SUCCESS - local emit_event = false - - if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then - if command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then - -- create credential if not already present. - if lock_utils.get_credential(device, credential_index) == nil then - lock_utils.add_credential(device, - active_credential.userIndex, - active_credential.credentialType, - credential_index) - emit_event = true - end - elseif command ~= nil and command.name == lock_utils.UPDATE_CREDENTIAL then - -- update credential - local credential = lock_utils.get_credential(device, credential_index) - if credential ~= nil then - lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) - emit_event = true - end - else - -- Called by reloading the codes. Don't add if already in table. - if lock_utils.get_credential(device, credential_index) == nil then - local new_user_index = lock_utils.get_available_user_index(device) - if new_user_index ~= nil then - lock_utils.create_user(device, nil, "guest", new_user_index) - lock_utils.add_credential(device, - new_user_index, - lock_utils.CREDENTIAL_TYPE, - credential_index) - emit_event = true - else - status = lock_utils.STATUS_RESOURCE_EXHAUSTED - end - end - end - elseif zb_mess.body.zcl_body.user_status.value == UserStatusEnum.AVAILABLE and command ~= nil and command.name == lock_utils.ADD_CREDENTIAL then - -- tried to add a code that already is in use. - -- remove the created user if one got made. There is no associated credential. - status = lock_utils.STATUS_DUPLICATE - lock_utils.delete_user(device, active_credential.userIndex) - else - if lock_utils.get_credential(device, credential_index) ~= nil then - -- Credential has been deleted. - lock_utils.delete_credential(device, credential_index) - emit_event = true - end - end - - if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then - -- the credential we're checking has arrived - local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, - capabilities.lockCredentials.pinUsersSupported.NAME) - if (credential_index >= last_slot) then - device:set_field(lock_utils.CHECKING_CODE, nil) - emit_event = true - else - local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 - device:set_field(lock_utils.CHECKING_CODE, checkingCode) - device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) - end - end - - if emit_event then - lock_utils.send_events(device) - end - -- ignore handling the busy state for these commands, they are handled within their own handlers - if command ~= nil and command ~= lock_utils.DELETE_ALL_CREDENTIALS and command ~= lock_utils.DELETE_ALL_USERS then - lock_utils.clear_busy_state(device, status) - end -end - -local programming_event_handler = function(driver, device, zb_mess) - local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) - local command = device:get_field(lock_utils.COMMAND_NAME) - local emit_events = false - - if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then - -- Master code updated - device:emit_event(capabilities.lockCredentials.commandResult( - {commandName = lock_utils.UPDATE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS}, - { state_change = true, visibility = { displayed = true } } - )) - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then - if (zb_mess.body.zcl_body.user_id.value == 0xFF) then - -- All credentials deleted - for _, credential in pairs(lock_utils.get_credentials(device)) do - lock_utils.delete_credential(device, credential.credentialIndex) - emit_events = true - end - else - -- One credential deleted - if (lock_utils.get_credential(device, credential_index) ~= nil) then - lock_utils.delete_credential(device, credential_index) - emit_events = true - end - end - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or - zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then - if lock_utils.get_credential(device, credential_index) == nil and command == nil then - local user_index = lock_utils.get_available_user_index(device) - if user_index ~= nil then - lock_utils.create_user(device, nil, "guest", user_index) - lock_utils.add_credential(device, - user_index, - lock_utils.CREDENTIAL_TYPE, - credential_index) - emit_events = true - end - end - end - - if emit_events then - lock_utils.send_events(device) - end -end - --- REMOVE THIS AFTER DONE WITH TESTING -local migrate = function(driver, device, value) - log.error_with({ hub_logs = true }, "\n--- PK -- CURRENT USERS ---- \n" .. - "\n" ..utils.stringify_table(lock_utils.get_users(device)).."\n" .. - "\n--- PK -- CURRENT CREDENTIALS ---- \n" .. - "\n" ..utils.stringify_table(lock_utils.get_credentials(device)).."\n" .. - "\n --------------------------------- \n") -end - -local lock_operation_event_handler = function(driver, device, zb_rx) - local event_code = zb_rx.body.zcl_body.operation_event_code.value - local source = zb_rx.body.zcl_body.operation_event_source.value - local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" - local METHOD = { - [0] = "keypad", - [1] = "command", - [2] = "manual", - [3] = "rfid", - [4] = "fingerprint", - [5] = "bluetooth" - } - local STATUS = { - [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() - } - local event = STATUS[event_code] - if (event ~= nil) then - event["data"] = {} - if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or - event_code == OperationEventCode.SCHEDULE_LOCK or - event_code == OperationEventCode.SCHEDULE_UNLOCK - ) then - event.data.method = "auto" - else - event.data.method = METHOD[source] - end - if (source == 0 and device:supports_capability_by_id(capabilities.lockUsers.ID)) then --keypad - local code_id = zb_rx.body.zcl_body.user_id.value - local code_name = "Code " .. code_id - local user = lock_utils.get_user(device, code_id) - if user ~= nil then - code_name = user.userName - end - - event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } - end - - -- if this is an event corresponding to a recently-received attribute report, we - -- want to set our delay timer for future lock attribute report events - if device:get_latest_state( - device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), - capabilities.lock.ID, - capabilities.lock.lock.ID) == event.value.value then - local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 - local time_diff = socket.gettime() - preceding_event_time - if time_diff < MAX_DELAY then - device:set_field(DELAY_LOCK_EVENT, time_diff) - end - end - - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) - end -end - - -local new_capabilities_driver = { - NAME = "Lock Driver Using New Capabilities", - supported_capabilities = { - Lock, - LockCredentials, - LockUsers, - Battery, - }, - zigbee_handlers = { - cluster = { - [LockCluster.ID] = { - [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, - [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, - [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler, - } - }, - attr = { - [LockCluster.ID] = { - [LockCluster.attributes.MaxPINCodeLength.ID] = max_code_length_handler, - [LockCluster.attributes.MinPINCodeLength.ID] = min_code_length_handler, - [LockCluster.attributes.NumberOfPINUsersSupported.ID] = max_codes_handler, - } - } - }, - capability_handlers = { - [LockUsers.ID] = { - [LockUsers.commands.addUser.NAME] = add_user_handler, - [LockUsers.commands.updateUser.NAME] = update_user_handler, - [LockUsers.commands.deleteUser.NAME] = delete_user_handler, - [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, - }, - [LockCredentials.ID] = { - [LockCredentials.commands.addCredential.NAME] = add_credential_handler, - [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, - [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, - [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, - }, - - [capabilities.lockCodes.ID] = { -- REMOVE THIS WHEN DONE WITH TESTING - [capabilities.lockCodes.commands.migrate.NAME] = migrate, - }, - }, - sub_drivers = { - require("using-new-capabilities.sub_drivers") - }, - health_check = false, - lifecycle_handlers = { - init = init, - doConfigure = do_configure - }, - can_handle = require("using-new-capabilities.can_handle") -} - -return new_capabilities_driver From 2cfd70682716cad38e2b1fb39f67173fbb465d78 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 4 May 2026 14:31:48 -0500 Subject: [PATCH 4/7] only update on added if state is typed --- drivers/SmartThings/zigbee-lock/src/init.lua | 12 +- .../zigbee-lock/src/samsungsds/init.lua | 8 +- .../test/test_zigbee_lock_code_migration.lua | 250 ------------------ .../test_zigbee_lock_code_slga_migration.lua | 17 +- .../src/test/test_zigbee_samsungsds.lua | 1 + .../test_zigbee_yale-new_capabilities.lua | 245 +++++++---------- 6 files changed, 117 insertions(+), 416 deletions(-) delete mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 1c4c74a29e..0e02b8ec88 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -59,19 +59,13 @@ local alarm_handler = function(driver, device, zb_mess) end end --- This triggers setting the migrated field so new devices use new capabilities from the start. local function 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 + if device:supports_capability_by_id(LockCodes.ID) then + if device._provisioning_state == "TYPED" then -- only run migration for typed devices, as provisioned devices may be in the process of migrating and we don't want to interfere with that. + -- If a device is newly onboarded (typed), we set the migrated field to true so devices use lockCredentials/lockUsers from the start. device:emit_event(LockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) - lock_utils.reload_tables(device) - else - legacy_lock_utils.populate_state_from_data(device) end - else - legacy_lock_utils.populate_state_from_data(device) end driver:inject_capability_command(device, { diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index 060b7d013d..ca9986ad16 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -64,7 +64,13 @@ local function load_device_state(device) end local device_added = function(self, device) - load_device_state(device) + if device:supports_capability_by_id(capabilities.lockCodes.ID) then + if device._provisioning_state == "TYPED" then -- only run migration for typed devices, as provisioned devices may be in the process of migrating and we don't want to interfere with that. + -- If a device is newly onboarded (typed), we set the migrated field to true so devices use lockCredentials/lockUsers from the start. + device:emit_event(capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) + end + end 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 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 deleted file mode 100644 index bc1c2b54ef..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua +++ /dev/null @@ -1,250 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - --- 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 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", - ["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 = {}, - useOldCapabilityForTesting = true - } -) -zigbee_test_utils.prepare_zigbee_env_info() -local function test_init()end - -test.set_test_init_function(test_init) - -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.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", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device added without data should function", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, DoorLock.attributes.LockState:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, Alarm.attributes.AlarmCount:read(mock_device_no_data) }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", nil) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", nil) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", nil) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init after added shouldn't change the datastores", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.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", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- 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, "lockCodes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init with new data should populate fields", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, DoorLock.attributes.LockState:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, Alarm.attributes.AlarmCount:read(mock_device_no_data) }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "lockCodes", nil) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "__state_cache", {}) - -- 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({ ["1"] = "Zach", ["2"] = "Steven" }) - } - } - )) - test.wait_for_events() - 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, "lockCodes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device added data lock codes population, device response produces no events", - 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", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.wait_for_events() - - -- run do_configure step after added and verify no refresh all codes - 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" }) - -- Validate migration reload skipped datastore - test.wait_for_events() - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationReloadSkipped", true) - -- Verify the timer doesn't fire as it wasn't created - test.mock_time.advance_time(4) - test.wait_for_events() - end, - { - min_api_version = 17 - } -) - - -test.run_registered_tests() 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 0fde1e1cd5..2bfb0032ff 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 @@ -7,9 +7,7 @@ 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" @@ -24,30 +22,27 @@ local mock_device = test.mock_device.build_test_zigbee_device( ["1"] = "Zach", ["5"] = "Steven" }), - }, - useOldCapabilityForTesting = true + } } ) zigbee_test_utils.prepare_zigbee_env_info() -local function test_init()end +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(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.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.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, "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) }) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua index 4b5b18a70e..fe76380bba 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua @@ -30,6 +30,7 @@ local SAMSUNG_SDS_MFR_CODE = 0x0003 local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("lock-without-codes.yml"), + provisioning_state = "TYPED", zigbee_endpoints = { [1] = { id = 1, 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 14a183adfe..0f0f03d455 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 @@ -17,9 +17,12 @@ local DoorLockUserStatus = DoorLock.types.DrlkUserStatus local DoorLockUserType = DoorLock.types.DrlkUserType local ProgrammingEventCode = DoorLock.types.ProgramEventCode +test.disable_startup_messages() + local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", zigbee_endpoints = { [1] = { id = 1, manufacturer = "Yale", server_clusters = { 0x0001 } } } @@ -28,6 +31,7 @@ local mock_device = test.mock_device.build_test_zigbee_device({ zigbee_test_utils.prepare_zigbee_env_info() local function test_init_default() + test.disable_startup_messages() 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", @@ -40,18 +44,8 @@ local function test_init_default() 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", - 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) }) - test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) - +local function test_init_lifecycle_event(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init"}) 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", @@ -62,6 +56,7 @@ local function test_init_add_device() 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() end test.set_test_init_function(test_init_default) @@ -118,6 +113,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Adding a credential should succeed and report users, credentials, and command result.", function() + test_init_lifecycle_event(mock_device) 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( @@ -177,17 +173,13 @@ test.register_coroutine_test( ) ) ) - end, - { - test_init = function() - test_init_add_device() - end - } + end ) test.register_coroutine_test( "Updating a credential should succeed and report users, credentials, and command result.", function() + test_init_lifecycle_event(mock_device) -- 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" } } }) @@ -326,86 +318,67 @@ test.register_coroutine_test( ) ) test.wait_for_events() - end, - { - test_init = function() - test_init_add_device() - end - } + end ) -test.register_message_test( +test.register_coroutine_test( "The lock reporting a single code has been set and then deleted should be handled", - { + function() + test_init_lifecycle_event(mock_device) -- 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", + test.socket.zigbee:__queue_receive({ + 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" + ) + }) + 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 } })) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", + ) + 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.wait_for_events() -- 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", + test.socket.zigbee:__queue_receive({ + 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" + ) + }) + test.socket.capability:__expect_send( + 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", + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } })) - } - }, - { test_init = test_init_add_device } + ) + test.wait_for_events() + end ) test.register_message_test( @@ -433,78 +406,65 @@ test.register_message_test( } ) -test.register_message_test( +test.register_coroutine_test( "The lock reporting all codes have been deleted should be handled", - { + function() + test_init_lifecycle_event(mock_device) -- 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", + test.socket.zigbee:__queue_receive({ + 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" + ) + }) + 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 } })) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", + ) + 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.wait_for_events() -- 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", + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 0xFFFF + ) + }) + test.socket.capability:__expect_send( + 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", + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { state_change = true, visibility = { displayed = true } })) - } - }, - { test_init = test_init_add_device } + ) + test.wait_for_events() + end ) 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_init_lifecycle_event(mock_device) test.socket.zigbee:__queue_receive( { mock_device.id, @@ -531,12 +491,7 @@ test.register_coroutine_test( { state_change = true, visibility = { displayed = true } }) ) ) - end, - { - test_init = function() - test_init_add_device() - end - } + end ) test.run_registered_tests() From b36d8a8b49dcd4fbbee0c8bbc100a3e5e6f65647 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 4 May 2026 15:15:53 -0500 Subject: [PATCH 5/7] remove explicit state_change value from capability event --- drivers/SmartThings/zigbee-lock/src/init.lua | 12 ++++++------ .../SmartThings/zigbee-lock/src/samsungsds/init.lua | 12 ++++++------ .../src/test/test_zigbee_yale-new_capabilities.lua | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 0e02b8ec88..6388db425a 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -60,12 +60,12 @@ local alarm_handler = function(driver, device, zb_mess) end local function device_added(driver, device) - if device:supports_capability_by_id(LockCodes.ID) then - if device._provisioning_state == "TYPED" then -- only run migration for typed devices, as provisioned devices may be in the process of migrating and we don't want to interfere with that. - -- If a device is newly onboarded (typed), we set the migrated field to true so devices use lockCredentials/lockUsers from the start. - device:emit_event(LockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) - device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) - end + if device:supports_capability_by_id(capabilities.lockCodes.ID) and device._provisioning_state == "TYPED" then + -- set the migrated field to true so new devices use lockCredentials/lockUsers from the start. + -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, + -- and should be migrated manually by the user. + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) end driver:inject_capability_command(device, { diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index ca9986ad16..48926ff1ef 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -64,12 +64,12 @@ local function load_device_state(device) end local device_added = function(self, device) - if device:supports_capability_by_id(capabilities.lockCodes.ID) then - if device._provisioning_state == "TYPED" then -- only run migration for typed devices, as provisioned devices may be in the process of migrating and we don't want to interfere with that. - -- If a device is newly onboarded (typed), we set the migrated field to true so devices use lockCredentials/lockUsers from the start. - device:emit_event(capabilities.lockCodes.migrated(true, { state_change = true, visibility = { displayed = true } })) - device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) - end + if device:supports_capability_by_id(capabilities.lockCodes.ID) and device._provisioning_state == "TYPED" then + -- set the migrated field to true so new devices use lockCredentials/lockUsers from the start. + -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, + -- and should be migrated manually by the user. + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) end 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)) 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 0f0f03d455..3ac6d09e86 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 @@ -35,7 +35,7 @@ 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 } }))) + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) 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( From ee7c5fcfc5c062420a47ce30386bcdea8d5c5dc5 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 6 May 2026 17:23:20 -0500 Subject: [PATCH 6/7] update test names, add persisted datastore value for slga migration --- drivers/SmartThings/zigbee-lock/src/init.lua | 5 +- .../src/legacy-handlers/can_handle.lua | 7 +- .../zigbee-lock/src/legacy-handlers/init.lua | 3 + .../zigbee-lock/src/samsungsds/init.lua | 5 +- .../zigbee-lock/src/test/test_zigbee_lock.lua | 1386 +++++++---------- .../test_zigbee_lock_code_slga_migration.lua | 2 + .../src/test/test_zigbee_lock_legacy.lua | 942 +++++++++++ .../test_zigbee_lock_new_capabilities.lua | 614 -------- .../test_zigbee_yale-new_capabilities.lua | 497 ------ .../zigbee-lock/src/test/test_zigbee_yale.lua | 736 +++++---- .../src/test/test_zigbee_yale_legacy.lua | 449 ++++++ .../src/yale-fingerprint-lock/can_handle.lua | 9 +- .../zigbee-lock/src/zigbee_lock_utils.lua | 141 +- 13 files changed, 2402 insertions(+), 2394 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 6388db425a..689aa63d56 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -65,6 +65,7 @@ local function device_added(driver, device) -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, -- and should be migrated manually by the user. device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:set_field(lock_utils.SLGA_MIGRATED, true, { persist = true }) -- persist the migration event in the datastore device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) end @@ -148,8 +149,8 @@ local do_configure = function(self, device) 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)) - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) - if lock_codes_migrated then + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) or false + if slga_migrated then device.thread:call_with_delay(2, function(d) reload_all_codes(device) end) else -- Don't send a reload all codes if this is a part of migration diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua index a37b71e079..fb869ffe0d 100644 --- a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua @@ -2,10 +2,9 @@ -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, ...) - local capabilities = require "st.capabilities" - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, - capabilities.lockCodes.migrated.NAME, false) - if not lock_codes_migrated then + local lock_utils = require "zigbee_lock_utils" + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) or false + if not slga_migrated then local subdriver = require("legacy-handlers") return true, subdriver end diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua index b167d7f75f..d66f5e583f 100644 --- a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua @@ -290,6 +290,8 @@ local name_slot = function(driver, device, command) end local migrate = function(driver, device, command) + local post_migration_lock_utils = require "zigbee_lock_utils" + local lock_users = {} local lock_credentials = {} local lock_codes = lock_utils.get_lock_codes(device) @@ -324,6 +326,7 @@ local migrate = function(driver, device, command) 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 } })) + device:set_field(post_migration_lock_utils.SLGA_MIGRATED, true, { persist = true }) -- persist the migration event in the datastore end local legacy_capabilities_driver = { diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index 48926ff1ef..79d4bfa5fa 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -54,8 +54,8 @@ local function emit_event_if_latest_state_missing(device, component, capability, end local function load_device_state(device) - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) - if lock_codes_migrated then + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) or false + if slga_migrated then lock_utils.reload_tables(device) else local legacy_lock_utils = require "legacy-handlers.legacy_lock_utils" @@ -69,6 +69,7 @@ local device_added = function(self, device) -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, -- and should be migrated manually by the user. device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:set_field(lock_utils.SLGA_MIGRATED, true, { persist = true }) -- persist the migration event in the datastore device:emit_event(capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) end emit_event_if_latest_state_missing(device, "main", capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua index 12f8331253..db6e13e8c0 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -- Mock out globals @@ -7,936 +7,610 @@ 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 DoorLockState = DoorLock.attributes.LockState -local OperationEventCode = DoorLock.types.OperationEventCode local DoorLockUserStatus = DoorLock.types.DrlkUserStatus local DoorLockUserType = DoorLock.types.DrlkUserType -local ProgrammingEventCode = DoorLock.types.ProgramEventCode - -local json = require "dkjson" +local lock_utils = require "zigbee_lock_utils" +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() - test.mock_device.add_test_device(mock_device)end - -test.set_test_init_function(test_init) - -local expect_reload_all_codes_messages = function() - test.socket.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.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 0) }) -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, { - min_api_version = 17 + profile = t_utils.get_profile_definition("base-lock.yml"), } ) -test.register_coroutine_test( - "Refresh should read expected attributes", - function() - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.capability:__queue_receive({mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} }}) - - 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, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Lock status reporting should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, - DoorLockState.LOCKED) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked()) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Battery percentage report should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, - 55) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.battery.battery(28)) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Lock operation event reporting should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, - DoorLock.client.commands.OperatingEventNotification.build_test_rx( - mock_device, - 0x02, - OperationEventCode.LOCK, - 0x0000, - "", - 0x0000, - "") } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({ data = { method = "manual" } })) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Pin response reporting should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 0x02, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 set", - { data = { codeName = "Code 2" }, state_change = true })) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["2"] = "Code 2"} ), { visibility = { displayed = false } })) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Sending the lock command should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "lock", component = "main", command = "lock", args = {} } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, DoorLock.server.commands.LockDoor(mock_device) } - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Min lock code length report should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 4) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(4, { visibility = { displayed = false }})) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Max lock code length report should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 4) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(4, { visibility = { displayed = false }})) - } - }, - { - min_api_version = 17 - } -) - -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.lockCodes.maxCodes(16, { visibility = { displayed = false }})) - } - }, - { - min_api_version = 17 - } -) +zigbee_test_utils.prepare_zigbee_env_info() -test.register_coroutine_test( - "Reloading all codes of an unconfigured lock should generate correct attribute checks", - function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) - expect_reload_all_codes_messages() - end, - { - min_api_version = 17 - } -) +local function test_init_new_capabilities() + test_credential_index = 1 + test_credentials = {} + test_users = {} + test.mock_device.add_test_device(mock_device) +end -test.register_message_test( - "Requesting a user code should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = capabilities.lockCodes.ID, command = "requestCode", args = { 1 } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) } - } - }, - { - min_api_version = 17 - } -) +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() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "SLGA_MIGRATED field should be set to true after migration") +end -test.register_coroutine_test( - "Deleting a user code should be handled", - function() - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - 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.lockCodes.codeChanged("1 set", - { data = { codeName = "Code 1" }, state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode( {["1"] = "Code 1"} ), { visibility = { displayed = false }}) - )) - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "deleteCode", args = { 1 } } }) - 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.wait_for_events() - - test.mock_time.advance_time(2) - 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, - "")}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 deleted", - { data = { codeName = "Code 1"}, state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) - )) - end, - { - min_api_version = 17 - } -) +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, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = i }, + { state_change = true, visibility = { displayed = true } } + ) + ) + ) + end +end -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, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) - test.socket.zigbee:__expect_send( - { +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, - 1, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" + test_credential_index, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + credential_data ) - } - ) - test.wait_for_events() - - test.mock_time.advance_time(4) - test.socket.zigbee:__expect_send( - { + } + ) + 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( - { + 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, - 0x01, + test_credential_index, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, - "1234" + credential_data ) - } - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test" }, state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }))) - end, - { - min_api_version = 17 - } -) - -local function init_code_slot(slot_number, name, device) - test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") - test.socket.capability:__queue_receive({ device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { slot_number, "1234", name } } }) - test.socket.zigbee:__expect_send( - { - device.id, - DoorLock.server.commands.SetPINCode(device, - slot_number, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" + } + ) + 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.wait_for_events() - test.mock_time.advance_time(4) - test.socket.zigbee:__expect_send( - { - device.id, - DoorLock.server.commands.GetPINCode(device, slot_number) - } - ) - test.wait_for_events() - test.socket.zigbee:__queue_receive( - { - device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - device, - slot_number, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" + ) + 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.socket.capability:__expect_send(device:generate_test_message("main", - capabilities.lockCodes.codeChanged(slot_number .. " set", { data = { codeName = name }, state_change = 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( - "Setting a user code name should be handled", + "Add User command received and commandResult is success until totalUsersSupported reached", function() - init_code_slot(1, "initialName", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) - test.wait_for_events() - - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) - end, - { - min_api_version = 17 - } + -- 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( - "Setting a user code name via setCode should be handled", - function() - init_code_slot(1, "initialName", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) - test.wait_for_events() + "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() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "", "foo"} } }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) - end, - { - min_api_version = 17 - } -) + -- 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 } } + ) + ) + ) -test.register_coroutine_test( - "Calling updateCodes should send properly spaced commands", - function () - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, - DoorLock.server.commands.SetPINCode(mock_device, - 1, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" - ) - }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, - DoorLock.server.commands.SetPINCode(mock_device, - 2, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "2345" - ) - }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, - DoorLock.server.commands.SetPINCode(mock_device, - 3, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "3456" - ) - }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) - }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, - DoorLock.server.commands.GetPINCode(mock_device, 1) - }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, - DoorLock.server.commands.GetPINCode(mock_device, 2) - }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, - DoorLock.server.commands.GetPINCode(mock_device, 3) - }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ - mock_device.id, - DoorLock.server.commands.GetPINCode(mock_device, 4) - }) - test.wait_for_events() - end, - { - min_api_version = 17 - } + -- 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( - "Setting all user codes should result in a code set event for each", - function () - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) - test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234")}) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 2, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "2345")}) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 2) }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "3456")}) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 3) }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) }) - test.mock_time.advance_time(2) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 4) }) - test.wait_for_events() - end, - { - min_api_version = 17 - } -) + "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() -test.register_message_test( - "Master code programming event should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_device.id, - DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( - mock_device, - 0x00, - ProgrammingEventCode.MASTER_CODE_CHANGED, - 0, - "1234", - DoorLockUserType.MASTER_USER, - DoorLockUserStatus.OCCUPIED_ENABLED, - 0x0000, - "data" - ) + -- 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" }, } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.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 } } + ) + ) ) - } - }, - { - min_api_version = 17 - } -) -test.register_message_test( - "The lock reporting a single code has been set should be handled", - { - { - 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.lockCodes.codeChanged("1 set", { data = { codeName = "Code 1"}, state_change = true })) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } })) - } - }, - { - min_api_version = 17 - } + -- 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( - "The lock reporting a code has been deleted should be handled", + "addCredential command received and commandResult is success", function() - init_code_slot(1, "Code 1", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) - test.socket.zigbee:__queue_receive( - { - 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" - ) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) - end, - { - min_api_version = 17 - } + init_migration() + add_credential(0, "abc123") + end ) test.register_coroutine_test( - "The lock reporting that all codes have been deleted should be handled", + "updateCredential command received and commandResult is success", function() - init_code_slot(1, "Code 1", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) - init_code_slot(2, "Code 2", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1", ["2"] = "Code 2"}), { visibility = { displayed = false } }))) - init_code_slot(3, "Code 3", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1", ["2"] = "Code 2", ["3"] = "Code 3"}), { visibility = { displayed = false } }))) - - test.socket.zigbee:__queue_receive( - { + 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, - DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( - mock_device, - 0x0, - ProgrammingEventCode.PIN_CODE_DELETED, - 0xFF, - "1234", - DoorLockUserType.UNRESTRICTED, - DoorLockUserStatus.AVAILABLE, - 0x0000, - "data" + { + 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.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) - ) - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) - ) - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) - test.wait_for_events() - end, - { - min_api_version = 17 - } + ) + 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( - "The lock reporting unlock via code should include the code info in the report", + "deleteCredential command received and commandResult is success", function() - init_code_slot(1, "Code 1", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) - test.socket.zigbee:__queue_receive( - { + 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.OperatingEventNotification.build_test_rx( + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( mock_device, - 0x00, -- 0 = keypad - OperationEventCode.UNLOCK, - 0x0001, - "1234", - 0x0000, + 0x01, + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, "" ) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) - ) - ) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Lock state attribute reports (after the first) should be delayed if they come before event notifications ", - function() - init_code_slot(1, "Code 1", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) - test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.UNLOCKED)}) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lock.lock.unlocked() - ) - ) - test.mock_time.advance_time(2) - test.socket.zigbee:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.OperatingEventNotification.build_test_rx( - mock_device, - 0x00, -- 0 = keypad - OperationEventCode.UNLOCK, - 0x0001, - "1234", - 0x0000, - "" + }) + 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.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) - ) - ) - test.mock_time.advance_time(2) - test.timer.__create_and_queue_test_time_advance_timer(2.5, "oneshot") - test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.LOCKED)}) - test.socket.zigbee:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.OperatingEventNotification.build_test_rx( - mock_device, - 0x00, -- 0 = keypad - OperationEventCode.LOCK, - 0x0001, - "1234", - 0x0000, - "" + 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.lock.lock.locked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) - ) - ) - test.mock_time.advance_time(2.5) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lock.lock.locked() - ) - ) - end, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Alarm code 0 should generate lock unknown event", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x00, DoorLock.ID) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Alarm code 1 should generate lock unknown event", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x01, DoorLock.ID) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Pin response for unoccupied slot with no existing code should generate unset event", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 0x05, - DoorLockUserStatus.OCCUPIED_DISABLED, - DoorLockUserType.UNRESTRICTED, - "" - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("5 unset", - { data = { codeName = "Code 5" }, state_change = true })) - } - }, - { - min_api_version = 17 - } + 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( - "Pin response for already-set slot should use changed change type", + "deleteAllCredentials command received and commandResult is success", function() - init_code_slot(1, "Code 1", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) - test.wait_for_events() + init_migration() + add_credential(0, "abc123") + add_credential(0, "test123") + add_credential(0, "321test") - test.socket.zigbee:__queue_receive( - { + 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, - DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, - "1234" + DoorLockUserStatus.AVAILABLE, + "" ) - } - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 changed", { data = { codeName = "Code 1" }, state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) - end, - { - min_api_version = 17 - } + }) + 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_lock_code_slga_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua index 2bfb0032ff..af8c8f20ce 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 @@ -9,6 +9,7 @@ local t_utils = require "integration_test.utils" local clusters = require "st.zigbee.zcl.clusters" local DoorLock = clusters.DoorLock local capabilities = require "st.capabilities" +local lock_utils = require "zigbee_lock_utils" local json = require "st.json" @@ -62,6 +63,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "SLGA_MIGRATED field should be set to true after migration") end ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua new file mode 100644 index 0000000000..12f8331253 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua @@ -0,0 +1,942 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- 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 DoorLockState = DoorLock.attributes.LockState +local OperationEventCode = DoorLock.types.OperationEventCode +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local ProgrammingEventCode = DoorLock.types.ProgramEventCode + +local json = require "dkjson" + +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() + test.mock_device.add_test_device(mock_device)end + +test.set_test_init_function(test_init) + +local expect_reload_all_codes_messages = function() + test.socket.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.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 0) }) +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, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Refresh should read expected attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} }}) + + 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, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Lock status reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, + DoorLockState.LOCKED) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Battery percentage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, + 55) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(28)) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Lock operation event reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x02, + OperationEventCode.LOCK, + 0x0000, + "", + 0x0000, + "") } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({ data = { method = "manual" } })) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Pin response reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x02, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 set", + { data = { codeName = "Code 2" }, state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["2"] = "Code 2"} ), { visibility = { displayed = false } })) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Sending the lock command should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "lock", component = "main", command = "lock", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, DoorLock.server.commands.LockDoor(mock_device) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Min lock code length report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 4) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(4, { visibility = { displayed = false }})) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Max lock code length report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 4) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(4, { visibility = { displayed = false }})) + } + }, + { + min_api_version = 17 + } +) + +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.lockCodes.maxCodes(16, { visibility = { displayed = false }})) + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Reloading all codes of an unconfigured lock should generate correct attribute checks", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) + expect_reload_all_codes_messages() + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Requesting a user code should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = capabilities.lockCodes.ID, command = "requestCode", args = { 1 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) } + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Deleting a user code should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + 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.lockCodes.codeChanged("1 set", + { data = { codeName = "Code 1" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode( {["1"] = "Code 1"} ), { visibility = { displayed = false }}) + )) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "deleteCode", args = { 1 } } }) + 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.wait_for_events() + + test.mock_time.advance_time(2) + 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, + "")}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 deleted", + { data = { codeName = "Code 1"}, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code should result in the named code changed event firing", + function() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + 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.lockCodes.codeChanged("1 set", { data = { codeName = "test" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }))) + end, + { + min_api_version = 17 + } +) + +local function init_code_slot(slot_number, name, device) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { slot_number, "1234", name } } }) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.SetPINCode(device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.GetPINCode(device, slot_number) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( + { + device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(device:generate_test_message("main", + capabilities.lockCodes.codeChanged(slot_number .. " set", { data = { codeName = name }, state_change = true })) + ) +end + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code name via setCode should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "", "foo"} } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Calling updateCodes should send properly spaced commands", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 2, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "2345" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 3, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "3456" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 2) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 3) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 4) + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting all user codes should result in a code set event for each", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 2, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "2345")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 2) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "3456")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 3) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 4) }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Master code programming event should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x00, + ProgrammingEventCode.MASTER_CODE_CHANGED, + 0, + "1234", + DoorLockUserType.MASTER_USER, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ) + } + }, + + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + ) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "The lock reporting a single code has been set should be handled", + { + { + channel = "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.lockCodes.codeChanged("1 set", { data = { codeName = "Code 1"}, state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } })) + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "The lock reporting a code has been deleted should be handled", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive( + { + 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" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "The lock reporting that all codes have been deleted should be handled", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + init_code_slot(2, "Code 2", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1", ["2"] = "Code 2"}), { visibility = { displayed = false } }))) + init_code_slot(3, "Code 3", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1", ["2"] = "Code 2", ["3"] = "Code 3"}), { visibility = { displayed = false } }))) + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 0xFF, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + 0x0000, + "data" + ) + } + ) + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code info in the report", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.UNLOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Lock state attribute reports (after the first) should be delayed if they come before event notifications ", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.UNLOCKED)}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked() + ) + ) + test.mock_time.advance_time(2) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.UNLOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + test.mock_time.advance_time(2) + test.timer.__create_and_queue_test_time_advance_timer(2.5, "oneshot") + test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.LOCKED)}) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.LOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + test.mock_time.advance_time(2.5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked() + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Alarm code 0 should generate lock unknown event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x00, DoorLock.ID) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Alarm code 1 should generate lock unknown event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x01, DoorLock.ID) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Pin response for unoccupied slot with no existing code should generate unset event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x05, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("5 unset", + { data = { codeName = "Code 5" }, state_change = true })) + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Pin response for already-set slot should use changed change type", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + 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.lockCodes.codeChanged("1 changed", { data = { codeName = "Code 1" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() 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 deleted file mode 100644 index 5f6eb6ce5c..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua +++ /dev/null @@ -1,614 +0,0 @@ --- Copyright 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - --- 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, - { state_change = true, visibility = { displayed = true } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "addUser", statusCode = "success", userIndex = 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-new_capabilities.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua deleted file mode 100644 index 3ac6d09e86..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-new_capabilities.lua +++ /dev/null @@ -1,497 +0,0 @@ --- Copyright 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - --- 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 PowerConfiguration = clusters.PowerConfiguration -local Alarm = clusters.Alarms - -local DoorLock = clusters.DoorLock -local DoorLockUserStatus = DoorLock.types.DrlkUserStatus -local DoorLockUserType = DoorLock.types.DrlkUserType -local ProgrammingEventCode = DoorLock.types.ProgramEventCode - -test.disable_startup_messages() - - -local mock_device = test.mock_device.build_test_zigbee_device({ - profile = t_utils.get_profile_definition("base-lock.yml"), - _provisioning_state = "TYPED", - zigbee_endpoints = { - [1] = { id = 1, manufacturer = "Yale", server_clusters = { 0x0001 } } - } -}) - -zigbee_test_utils.prepare_zigbee_env_info() - -local function test_init_default() - test.disable_startup_messages() - 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.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) }) - test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) -end - -local function test_init_lifecycle_event(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init"}) - 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() -end - -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, - 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_init_lifecycle_event(mock_device) - 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.register_coroutine_test( - "Updating a credential should succeed and report users, credentials, and command result.", - function() - test_init_lifecycle_event(mock_device) - -- 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.register_coroutine_test( - "The lock reporting a single code has been set and then deleted should be handled", - function() - test_init_lifecycle_event(mock_device) - -- add credential - test.socket.zigbee:__queue_receive({ - 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" - ) - }) - 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.wait_for_events() - - -- delete the credential - test.socket.zigbee:__queue_receive({ - 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" - ) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockUsers.users({}, - { state_change = true, visibility = { displayed = true } })) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCredentials.credentials({}, - { state_change = true, visibility = { displayed = true } })) - ) - test.wait_for_events() - end -) - -test.register_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 = true } })) - } - } -) - -test.register_coroutine_test( - "The lock reporting all codes have been deleted should be handled", - function() - test_init_lifecycle_event(mock_device) - -- add a credential - test.socket.zigbee:__queue_receive({ - 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" - ) - }) - 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.wait_for_events() - - -- delete all credentials - test.socket.zigbee:__queue_receive({ - mock_device.id, - DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( - mock_device, - 0x0, - ProgrammingEventCode.PIN_CODE_DELETED, - 0xFFFF - ) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockUsers.users({}, - { state_change = true, visibility = { displayed = true } })) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCredentials.credentials({}, - { state_change = true, visibility = { displayed = true } })) - ) - test.wait_for_events() - end -) - -test.register_coroutine_test( - "Out of band get pin call should add credential if it doesn't exist (happens during reload all codes).", - function() - test_init_lifecycle_event(mock_device) - 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.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua index 95d496e786..3ac6d09e86 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -- Mock out globals @@ -8,7 +8,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 @@ -16,41 +15,62 @@ local Alarm = clusters.Alarms local DoorLock = clusters.DoorLock local DoorLockUserStatus = DoorLock.types.DrlkUserStatus local DoorLockUserType = DoorLock.types.DrlkUserType +local ProgrammingEventCode = DoorLock.types.ProgramEventCode + +test.disable_startup_messages() + 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}} + _provisioning_state = "TYPED", + 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)end -test.set_test_init_function(test_init) +local function test_init_default() + test.disable_startup_messages() + 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.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) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) +end + +local function test_init_lifecycle_event(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init"}) + 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() +end + +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, - true) }) + 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.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false }}))) + 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( - "Reloading all codes of an unconfigured lock should generate correct attribute checks", - function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) - expect_reload_all_codes_messages() - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Configure should configure all necessary attributes and begin reading codes", function() @@ -60,390 +80,418 @@ test.register_coroutine_test( 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) }) + 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) }) + 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) }) + 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) }) + 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) }) + 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, - { - min_api_version = 17 - } + end ) 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, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) - 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() + "Adding a credential should succeed and report users, credentials, and command result.", + function() + test_init_lifecycle_event(mock_device) + 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.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.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.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = 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.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false }}))) - end, - { - min_api_version = 17 - } + ) + 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( - "Setting the master code should result in the correct user type being used", - function() - test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 0, "1234", "test" } } }) - test.socket.zigbee:__expect_send( - { - mock_device.id, - DoorLock.server.commands.SetPINCode(mock_device, - 0, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.MASTER_USER, - "1234" - ) - } - ) - test.wait_for_events() + "Updating a credential should succeed and report users, credentials, and command result.", + function() + test_init_lifecycle_event(mock_device) + -- 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, 0) - } - ) - 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, - 0x00, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.MASTER_USER, - "1234" - ) - } + 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.lockCodes.codeChanged("0 set", { data = { codeName = "test"}, state_change = 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.lockCodes.lockCodes(json.encode({["0"] = "test"}), { visibility = { displayed = false }}))) - end, - { - min_api_version = 17 - } -) + ) + 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() -test.register_message_test( - "Pin response reporting should be handled when the Lock User status is disabled", - { + -- update the credential + test.socket.capability:__queue_receive({ + mock_device.id, { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - nil, - DoorLockUserStatus.OCCUPIED_DISABLED, - DoorLockUserType.UNRESTRICTED, - "1234" - ) - } + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { "1", "1", "pin", "changedPin123" } }, + }) + test.socket.zigbee:__expect_send( { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("0 unset", - { data = { codeName = "Code 0" }, state_change = true })) - } - }, - { - min_api_version = 17 - } -) - -local function init_code_slot(slot_number, name, device) - test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") - test.socket.capability:__queue_receive({ device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { slot_number, "1234", name } } }) - test.socket.zigbee:__expect_send( - { - device.id, - DoorLock.server.commands.SetPINCode(device, - slot_number, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "changedPin123" ) } - ) - test.wait_for_events() - test.mock_time.advance_time(4) - test.socket.zigbee:__expect_send( + ) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( { - device.id, - DoorLock.server.commands.GetPINCode(device, slot_number) + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) } - ) - test.wait_for_events() - test.socket.zigbee:__queue_receive( + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( { - device.id, + mock_device.id, DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - device, - slot_number, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "abc123" ) } - ) - test.socket.capability:__expect_send(device:generate_test_message("main", - capabilities.lockCodes.codeChanged(slot_number .. " set", { data = { codeName = name }, state_change = true })) - ) -end - -test.register_coroutine_test( - "Setting a user code name should be handled", - function() - init_code_slot(1, "initialName", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) - test.wait_for_events() - - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false }}))) - end, - { - min_api_version = 17 - } + ) + 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( - "Setting a user code and getting an incorrect code in response should indicate failure", + "The lock reporting a single code has been set and then deleted should be handled", function() - test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) - test.socket.zigbee:__expect_send( - { - mock_device.id, - DoorLock.server.commands.SetPINCode(mock_device, - 1, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" - ) - } + test_init_lifecycle_event(mock_device) + -- add credential + test.socket.zigbee:__queue_receive({ + 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" + ) + }) + 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.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.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.wait_for_events() - test.socket.zigbee:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 1, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "5678" - ) - } - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 failed", { state_change = true }))) - end, - { - min_api_version = 17 - } -) -test.register_coroutine_test( - "Setting a user code name via setCode should be handled", - function() - init_code_slot(1, "initialName", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + -- delete the credential + test.socket.zigbee:__queue_receive({ + 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" + ) + }) + 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.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "", "foo"} } }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false }}))) - end, +test.register_message_test( + "The lock reporting master code changed", { - min_api_version = 17 + { + 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 = true } })) + } } ) test.register_coroutine_test( - "Setting a user code for a slot that is empty should indicate failure and unset", - function() - test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) - test.socket.zigbee:__expect_send( - { - mock_device.id, - DoorLock.server.commands.SetPINCode(mock_device, - 1, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" - ) - } + "The lock reporting all codes have been deleted should be handled", + function() + test_init_lifecycle_event(mock_device) + -- add a credential + test.socket.zigbee:__queue_receive({ + 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" ) - 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.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.wait_for_events() - test.socket.zigbee:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 1, - DoorLockUserStatus.OCCUPIED_DISABLED, - DoorLockUserType.UNRESTRICTED, - "" - ) - } + -- delete all credentials + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 0xFFFF ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 failed", { state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 is not set", { state_change = true }))) - end, - { - min_api_version = 17 - } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, + { state_change = true, visibility = { displayed = true } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, + { state_change = true, visibility = { displayed = true } })) + ) + test.wait_for_events() + end ) test.register_coroutine_test( - "Pin response for already-set slot without pending operation should use changed change type", - function() - init_code_slot(1, "initialName", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) - test.wait_for_events() - - test.socket.zigbee:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 1, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" - ) - } + "Out of band get pin call should add credential if it doesn't exist (happens during reload all codes).", + function() + test_init_lifecycle_event(mock_device) + 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.lockCodes.codeChanged("1 changed", { data = { codeName = "initialName" }, state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Pin response for already-set slot that is now empty should delete the code", - function() - init_code_slot(1, "initialName", mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) - test.wait_for_events() - - test.socket.zigbee:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 1, - DoorLockUserStatus.OCCUPIED_DISABLED, - DoorLockUserType.UNRESTRICTED, - "" - ) - } + ) + 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.lockCodes.codeChanged("1 deleted", { data = { codeName = "initialName" }, state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false }}))) - end, - { - min_api_version = 17 - } + ) + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua new file mode 100644 index 0000000000..95d496e786 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua @@ -0,0 +1,449 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- 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)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.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false }}))) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) +end + +test.register_coroutine_test( + "Reloading all codes of an unconfigured lock should generate correct attribute checks", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) + expect_reload_all_codes_messages() + end, + { + min_api_version = 17 + } +) + +test.register_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, + { + min_api_version = 17 + } +) + +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, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + 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.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false }}))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting the master code should result in the correct user type being used", + function() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 0, "1234", "test" } } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 0, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.MASTER_USER, + "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, 0) + } + ) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x00, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.MASTER_USER, + "1234" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "test"}, state_change = true })) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["0"] = "test"}), { visibility = { displayed = false }}))) + end, + { + min_api_version = 17 + } +) + + +test.register_message_test( + "Pin response reporting should be handled when the Lock User status is disabled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + nil, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("0 unset", + { data = { codeName = "Code 0" }, state_change = true })) + } + }, + { + min_api_version = 17 + } +) + +local function init_code_slot(slot_number, name, device) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { slot_number, "1234", name } } }) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.SetPINCode(device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.GetPINCode(device, slot_number) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( + { + device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(device:generate_test_message("main", + capabilities.lockCodes.codeChanged(slot_number .. " set", { data = { codeName = name }, state_change = true })) + ) +end + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false }}))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code and getting an incorrect code in response should indicate failure", + function() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + 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, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "5678" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 failed", { state_change = true }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code name via setCode should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "", "foo"} } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false }}))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code for a slot that is empty should indicate failure and unset", + function() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + 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, + 1, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 failed", { state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 is not set", { state_change = true }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Pin response for already-set slot without pending operation should use changed change type", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 changed", { data = { codeName = "initialName" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Pin response for already-set slot that is now empty should delete the code", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "initialName" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false }}))) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua index 84a9918622..be2197030c 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua @@ -2,10 +2,9 @@ -- Licensed under the Apache License, Version 2.0 local yale_fingerprint_lock_models = 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 return false end + local lock_utils = require "zigbee_lock_utils" + local slga_migrated = device:get_field(lock_utils.SLGA_MIGRATED) or false + if not slga_migrated then return false end local FINGERPRINTS = require("yale-fingerprint-lock.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then @@ -15,4 +14,4 @@ local yale_fingerprint_lock_models = function(opts, driver, device) return false end -return yale_fingerprint_lock_models \ No newline at end of file +return yale_fingerprint_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua index 6000f6a804..7d96477574 100644 --- a/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua @@ -5,7 +5,7 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" local INITIAL_INDEX = 1 -local new_lock_utils = { +local lock_utils = { -- Constants ADD_CREDENTIAL = "addCredential", ADD_USER = "addUser", @@ -32,31 +32,32 @@ local new_lock_utils = { UPDATE_USER = "updateUser", USER_INDEX = "userIndex", USER_NAME = "userName", - USER_TYPE = "userType" + USER_TYPE = "userType", + SLGA_MIGRATED = "slgaMigrated" } -new_lock_utils.DELETE_ALL_USERS = 0xFF +lock_utils.DELETE_ALL_USERS = 0xFF -- check if we are currently busy performing a task. -- if we aren't then set as busy. -new_lock_utils.busy_check_and_set = function (device, command, override_busy_check) +lock_utils.busy_check_and_set = function (device, command, override_busy_check) if override_busy_check then -- the function was called by an injected command. return false end local c_time = os.time() - local busy_state = device:get_field(new_lock_utils.BUSY) or false + local busy_state = device:get_field(lock_utils.BUSY) or false if busy_state == false or c_time - busy_state > 10 then - device:set_field(new_lock_utils.COMMAND_NAME, command) - device:set_field(new_lock_utils.BUSY, c_time) + device:set_field(lock_utils.COMMAND_NAME, command) + device:set_field(lock_utils.BUSY, c_time) return false else local command_result_info = { commandName = command.name, - statusCode = new_lock_utils.STATUS_BUSY + statusCode = lock_utils.STATUS_BUSY } - if command.type == new_lock_utils.LOCK_USERS then + if command.type == lock_utils.LOCK_USERS then device:emit_event(capabilities.lockUsers.commandResult( command_result_info, { state_change = true, visibility = { displayed = true } } )) @@ -69,18 +70,18 @@ new_lock_utils.busy_check_and_set = function (device, command, override_busy_che end end -new_lock_utils.clear_busy_state = function(device, status, override_busy_check) +lock_utils.clear_busy_state = function(device, status, override_busy_check) if override_busy_check then return end - local command = device:get_field(new_lock_utils.COMMAND_NAME) - local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_NAME) + local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) if command ~= nil then local command_result_info = { commandName = command.name, statusCode = status } - if command.type == new_lock_utils.LOCK_USERS then + if command.type == lock_utils.LOCK_USERS then if active_credential ~= nil and active_credential.userIndex ~= nil then command_result_info.userIndex = active_credential.userIndex end @@ -100,35 +101,35 @@ new_lock_utils.clear_busy_state = function(device, status, override_busy_check) end end - device:set_field(new_lock_utils.ACTIVE_CREDENTIAL, nil) - device:set_field(new_lock_utils.COMMAND_NAME, nil) - device:set_field(new_lock_utils.BUSY, false) + device:set_field(lock_utils.ACTIVE_CREDENTIAL, nil) + device:set_field(lock_utils.COMMAND_NAME, nil) + device:set_field(lock_utils.BUSY, false) end -new_lock_utils.reload_tables = function(device) +lock_utils.reload_tables = function(device) local users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.users.NAME, {}) local credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.credentials.NAME, {}) if next(users) ~= nil then - device:set_field(new_lock_utils.LOCK_USERS, users) + device:set_field(lock_utils.LOCK_USERS, users) end if next(credentials) ~= nil then - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials) end - device:set_field(new_lock_utils.TABLES_LOADED, true) + device:set_field(lock_utils.TABLES_LOADED, true) end -new_lock_utils.get_users = function(device) - if not device:get_field(new_lock_utils.TABLES_LOADED) then - new_lock_utils.reload_tables(device) +lock_utils.get_users = function(device) + if not device:get_field(lock_utils.TABLES_LOADED) then + lock_utils.reload_tables(device) end - local users = utils.deep_copy(device:get_field(new_lock_utils.LOCK_USERS)) + local users = utils.deep_copy(device:get_field(lock_utils.LOCK_USERS)) return users ~= nil and users or {} end -new_lock_utils.get_user = function(device, user_index) - for _, user in pairs(new_lock_utils.get_users(device)) do +lock_utils.get_user = function(device, user_index) + for _, user in pairs(lock_utils.get_users(device)) do if user.userIndex == user_index then return user end @@ -137,10 +138,10 @@ new_lock_utils.get_user = function(device, user_index) return nil end -new_lock_utils.get_available_user_index = function(device) +lock_utils.get_available_user_index = function(device) local max = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME, 0) - local current_users = new_lock_utils.get_users(device) + local current_users = lock_utils.get_users(device) local available_index = nil local used_index = {} for _, user in pairs(current_users) do @@ -159,17 +160,17 @@ new_lock_utils.get_available_user_index = function(device) return available_index end -new_lock_utils.get_credentials = function(device) - if not device:get_field(new_lock_utils.TABLES_LOADED) then - new_lock_utils.reload_tables(device) +lock_utils.get_credentials = function(device) + if not device:get_field(lock_utils.TABLES_LOADED) then + lock_utils.reload_tables(device) end - local credentials = utils.deep_copy(device:get_field(new_lock_utils.LOCK_CREDENTIALS)) + local credentials = utils.deep_copy(device:get_field(lock_utils.LOCK_CREDENTIALS)) return credentials ~= nil and credentials or {} end -new_lock_utils.get_credential = function(device, credential_index) - for _, credential in pairs(new_lock_utils.get_credentials(device)) do +lock_utils.get_credential = function(device, credential_index) + for _, credential in pairs(lock_utils.get_credentials(device)) do if credential.credentialIndex == credential_index then return credential end @@ -177,8 +178,8 @@ new_lock_utils.get_credential = function(device, credential_index) return nil end -new_lock_utils.get_credential_by_user_index = function(device, user_index) - for _, credential in pairs(new_lock_utils.get_credentials(device)) do +lock_utils.get_credential_by_user_index = function(device, user_index) + for _, credential in pairs(lock_utils.get_credentials(device)) do if credential.userIndex == user_index then return credential end @@ -187,10 +188,10 @@ new_lock_utils.get_credential_by_user_index = function(device, user_index) return nil end -new_lock_utils.get_available_credential_index = function(device) +lock_utils.get_available_credential_index = function(device) local max = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME, 0) - local current_credentials = new_lock_utils.get_credentials(device) + local current_credentials = lock_utils.get_credentials(device) local available_index = nil local used_index = {} for _, credential in pairs(current_credentials) do @@ -209,53 +210,53 @@ new_lock_utils.get_available_credential_index = function(device) return available_index end -new_lock_utils.create_user = function(device, user_name, user_type, user_index) +lock_utils.create_user = function(device, user_name, user_type, user_index) if user_name == nil then user_name = "Guest" .. user_index end - local current_users = new_lock_utils.get_users(device) + local current_users = lock_utils.get_users(device) table.insert(current_users, { userIndex = user_index, userType = user_type, userName = user_name }) - device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) end -new_lock_utils.delete_user = function(device, user_index) - local current_users = new_lock_utils.get_users(device) - local status_code = new_lock_utils.STATUS_FAILURE +lock_utils.delete_user = function(device, user_index) + local current_users = lock_utils.get_users(device) + local status_code = lock_utils.STATUS_FAILURE for index, user in pairs(current_users) do if user.userIndex == user_index then -- table.remove causes issues if we are removing while iterating. -- instead set the value as nil and let `prep_table` handle removing it. current_users[index] = nil - device:set_field(new_lock_utils.LOCK_USERS, current_users) - status_code = new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_USERS, current_users) + status_code = lock_utils.STATUS_SUCCESS break end end return status_code end -new_lock_utils.add_credential = function(device, user_index, credential_type, credential_index) - local credentials = new_lock_utils.get_credentials(device) +lock_utils.add_credential = function(device, user_index, credential_type, credential_index) + local credentials = lock_utils.get_credentials(device) table.insert(credentials, { userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type }) - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) - return new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + return lock_utils.STATUS_SUCCESS end -new_lock_utils.delete_credential = function(device, credential_index) - local credentials = new_lock_utils.get_credentials(device) - local status_code = new_lock_utils.STATUS_FAILURE +lock_utils.delete_credential = function(device, credential_index) + local credentials = lock_utils.get_credentials(device) + local status_code = lock_utils.STATUS_FAILURE for index, credential in pairs(credentials) do if credential.credentialIndex == credential_index then - new_lock_utils.delete_user(device, credential.userIndex) + lock_utils.delete_user(device, credential.userIndex) -- table.remove causes issues if we are removing while iterating. -- instead set the value as nil and let `prep_table` handle removing it. credentials[index] = nil - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) - status_code = new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials) + status_code = lock_utils.STATUS_SUCCESS break end end @@ -263,16 +264,16 @@ new_lock_utils.delete_credential = function(device, credential_index) return status_code end -new_lock_utils.update_credential = function(device, credential_index, user_index, credential_type) - local credentials = new_lock_utils.get_credentials(device) - local status_code = new_lock_utils.STATUS_FAILURE +lock_utils.update_credential = function(device, credential_index, user_index, credential_type) + local credentials = lock_utils.get_credentials(device) + local status_code = lock_utils.STATUS_FAILURE for _, credential in pairs(credentials) do if credential.credentialIndex == credential_index then credential.credentialType = credential_type credential.userIndex = user_index - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) - status_code = new_lock_utils.STATUS_SUCCESS + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + status_code = lock_utils.STATUS_SUCCESS break end end @@ -280,7 +281,7 @@ new_lock_utils.update_credential = function(device, credential_index, user_index end -- emit_event doesn't like having `nil` values in the table. Remove any if they are present. -new_lock_utils.prep_table = function(data) +lock_utils.prep_table = function(data) local clean_table = {} for _, value in pairs(data) do if value ~= nil then @@ -290,19 +291,19 @@ new_lock_utils.prep_table = function(data) return clean_table end -new_lock_utils.send_events = function(device, type) - if type == nil or type == new_lock_utils.LOCK_USERS then - local current_users = new_lock_utils.prep_table(new_lock_utils.get_users(device)) - device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) +lock_utils.send_events = function(device, type) + if type == nil or type == lock_utils.LOCK_USERS then + local current_users = lock_utils.prep_table(lock_utils.get_users(device)) + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) 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:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) + if type == nil or type == lock_utils.LOCK_CREDENTIALS then + local credentials = lock_utils.prep_table(lock_utils.get_credentials(device)) + device:set_field(lock_utils.LOCK_CREDENTIALS, credentials, { persist = true }) device:emit_event(capabilities.lockCredentials.credentials(credentials, { state_change = true, visibility = { displayed = true } })) end end -return new_lock_utils +return lock_utils From d74f6a534292f0c0502d9a8fc739c3665575cff7 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 8 May 2026 10:54:41 -0500 Subject: [PATCH 7/7] rename COMMAND field since it's not just the name --- drivers/SmartThings/zigbee-lock/src/init.lua | 4 ++-- drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 689aa63d56..41a336a73c 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -405,7 +405,7 @@ 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 command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) local status = lock_utils.STATUS_SUCCESS local emit_event = false @@ -480,7 +480,7 @@ 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 command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) local emit_events = false if credential_index >= 256 then -- Index is incorrectly written, attempt to shift it to get an actual value diff --git a/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua index 7d96477574..8658711a5d 100644 --- a/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/zigbee_lock_utils.lua @@ -10,7 +10,7 @@ local lock_utils = { ADD_CREDENTIAL = "addCredential", ADD_USER = "addUser", BUSY = "busy", - COMMAND_NAME = "commandName", + COMMAND_IN_PROGRESS = "commandInProgress", CREDENTIAL_TYPE = "pin", CHECKING_CODE = "checkingCode", DELETE_ALL_CREDENTIALS = "deleteAllCredentials", @@ -49,7 +49,7 @@ lock_utils.busy_check_and_set = function (device, command, override_busy_check) local busy_state = device:get_field(lock_utils.BUSY) or false if busy_state == false or c_time - busy_state > 10 then - device:set_field(lock_utils.COMMAND_NAME, command) + device:set_field(lock_utils.COMMAND_IN_PROGRESS, command) device:set_field(lock_utils.BUSY, c_time) return false else @@ -74,7 +74,7 @@ lock_utils.clear_busy_state = function(device, status, override_busy_check) if override_busy_check then return end - local command = device:get_field(lock_utils.COMMAND_NAME) + local command = device:get_field(lock_utils.COMMAND_IN_PROGRESS) local active_credential = device:get_field(lock_utils.ACTIVE_CREDENTIAL) if command ~= nil then local command_result_info = { @@ -102,7 +102,7 @@ lock_utils.clear_busy_state = function(device, status, override_busy_check) end device:set_field(lock_utils.ACTIVE_CREDENTIAL, nil) - device:set_field(lock_utils.COMMAND_NAME, nil) + device:set_field(lock_utils.COMMAND_IN_PROGRESS, nil) device:set_field(lock_utils.BUSY, false) end