From 4e7b9734501c0fcc48ee91a6c562bb58a5593de1 Mon Sep 17 00:00:00 2001 From: Pegor Date: Fri, 19 Sep 2025 12:21:53 -0700 Subject: [PATCH 01/11] CHAD-16364: SLGA: Add lockUsers and lockCredentials capabilities to profiles --- drivers/SmartThings/zigbee-lock/profiles/base-lock.yml | 4 ++++ drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml | 4 ++++ drivers/SmartThings/zwave-lock/profiles/base-lock.yml | 4 ++++ 3 files changed, 12 insertions(+) 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/zwave-lock/profiles/base-lock-tamper.yml b/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml index 5fbfb13f3d..e3a25ab57a 100644 --- a/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml +++ b/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml @@ -6,6 +6,10 @@ components: version: 1 - id: lockCodes version: 1 + - id: lockCredentials + version: 1 + - id: lockUsers + version: 1 - id: battery version: 1 - id: tamperAlert diff --git a/drivers/SmartThings/zwave-lock/profiles/base-lock.yml b/drivers/SmartThings/zwave-lock/profiles/base-lock.yml index f4957f9ad0..efb97c8b27 100644 --- a/drivers/SmartThings/zwave-lock/profiles/base-lock.yml +++ b/drivers/SmartThings/zwave-lock/profiles/base-lock.yml @@ -6,6 +6,10 @@ components: version: 1 - id: lockCodes version: 1 + - id: lockCredentials + version: 1 + - id: lockUsers + version: 1 - id: battery version: 1 - id: refresh From 47b412c076bb83c84b3f350e7bb9cf1561cebc8f Mon Sep 17 00:00:00 2001 From: Pegor Date: Tue, 30 Sep 2025 12:31:58 -0700 Subject: [PATCH 02/11] CHAD-16365: SLGA: Add implementation for Zigbee lockCodes:migrate command --- drivers/SmartThings/zigbee-lock/src/init.lua | 38 +++++++++ .../test_zigbee_lock_code_slga_migration.lua | 82 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index ce6894b868..f76eaf51e0 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -28,6 +28,8 @@ local capabilities = require "st.capabilities" local Battery = capabilities.battery local Lock = capabilities.lock local LockCodes = capabilities.lockCodes +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers -- Enums local UserStatusEnum = LockCluster.types.DrlkUserStatus @@ -291,6 +293,41 @@ local name_slot = function(driver, device, command) 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 max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) + + if (code_length ~= nil) then + max_code_len = code_length + min_code_len = code_length + end + + device:emit_event(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(LockCodes.migrated(true, { visibility = { displayed = false } })) +end + local function device_added(driver, device) lock_utils.populate_state_from_data(device) @@ -436,6 +473,7 @@ local zigbee_lock_driver = { [LockCodes.commands.requestCode.NAME] = request_code, [LockCodes.commands.setCode.NAME] = set_code, [LockCodes.commands.nameSlot.NAME] = name_slot, + [LockCodes.commands.migrate.NAME] = migrate, }, [Lock.ID] = { [Lock.commands.lock.NAME] = lock, 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..569dca5236 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_slga_migration.lua @@ -0,0 +1,82 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local clusters = require "st.zigbee.zcl.clusters" +local PowerConfiguration = clusters.PowerConfiguration +local DoorLock = clusters.DoorLock +local Alarm = clusters.Alarms +local capabilities = require "st.capabilities" + +local json = require "st.json" + +local mock_datastore = require "integration_test.mock_env_datastore" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + data = { + lockCodes = json.encode({ + ["1"] = "Zach", + ["5"] = "Steven" + }), + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init()end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Device called 'migrate' command", + function() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) + test.wait_for_events() + -- Validate lockCodes field + mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" }) + -- Validate migration complete flag + mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + + -- Set min/max code length attributes + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) }) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) }) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.wait_for_events() + -- Validate `migrate` command functionality. + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(5, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() + end +) + +test.run_registered_tests() \ No newline at end of file From 0450cac04f72d2a10040802aed503e6faa938265 Mon Sep 17 00:00:00 2001 From: Pegor Date: Tue, 30 Sep 2025 15:29:37 -0700 Subject: [PATCH 03/11] CHAD-16365: SLGA: Add implementation for ZWave lockCodes:migrate command --- drivers/SmartThings/zwave-lock/src/init.lua | 54 ++++++ .../test_zwave_lock_code_slga_migration.lua | 156 ++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index b83b196256..cb44541ebf 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -146,6 +146,55 @@ local function update_codes(driver, device, cmd) end end +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function migrate(driver, device, cmd) + local lock_users = {} + local lock_credentials = {} + local lc_data = json.decode(device.data.lockCodes) + local lock_codes = {} + local ordered_codes = {} + for k, v in pairs(lc_data) do + lock_codes[k] = v + end + + for code in pairs(lock_codes) do + table.insert(ordered_codes, code) + end + + table.sort(ordered_codes) + for index = 1, #ordered_codes do + local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ] + table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name}) + table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) + end + + local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) + + if (min_code_len == nil) then + min_code_len = 4 -- per ZWave spec + end + if (max_code_len == nil) then + max_code_len = 10 -- per ZWave spec + end + if (code_length ~= nil) then + max_code_len = code_length + min_code_len = code_length + end + + device:emit_event(capabilities.lockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) +end + local function time_get_handler(driver, device, cmd) local time = os.date("*t") device:send_to_component( @@ -162,6 +211,8 @@ local driver_template = { supported_capabilities = { capabilities.lock, capabilities.lockCodes, + capabilities.lockUsers, + capabilities.lockCredentials, capabilities.battery, capabilities.tamperAlert }, @@ -173,6 +224,9 @@ local driver_template = { [capabilities.lockCodes.ID] = { [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes }, + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.migrate.NAME] = migrate + }, [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua new file mode 100644 index 0000000000..17629c1912 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua @@ -0,0 +1,156 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) +local t_utils = require "integration_test.utils" +local zw_test_utils = require "integration_test.zwave_test_utils" +local utils = require "st.utils" +local mock_datastore = require "integration_test.mock_env_datastore" +local json = require "dkjson" + +local SCHLAGE_MANUFACTURER_ID = 0x003B +local SCHLAGE_PRODUCT_TYPE = 0x0002 +local SCHLAGE_PRODUCT_ID = 0x0469 + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION } + } + } +} + +local lockCodes = { + ["1"] = "Zach", + ["5"] = "Steven" +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints, + data = { + lockCodes = json.encode(utils.deep_copy(lockCodes)) + } + } +) + +local schlage_mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, + zwave_product_type = SCHLAGE_PRODUCT_TYPE, + zwave_product_id = SCHLAGE_PRODUCT_ID, + data = { + lockCodes = json.encode(utils.deep_copy(lockCodes)) + } + } +) + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +test.register_coroutine_test( + "Device called 'migrate' command", + function() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + DoorLock:OperationGet({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Battery:Get({}) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) + test.wait_for_events() + -- Validate lockCodes field + mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" }) + -- Validate migration complete flag + mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + -- setup codes + test.socket.zwave:__queue_receive({mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.wait_for_events() + -- Validate migrate command + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Schlage-Lock device called 'migrate' command, validate codeLength is being properly set", + function() + test.mock_device.add_test_device(schlage_mock_device) + test.socket.device_lifecycle:__queue_receive({ schlage_mock_device.id, "added" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + schlage_mock_device, + DoorLock:OperationGet({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + schlage_mock_device, + Battery:Get({}) + ) + ) + test.wait_for_events() + -- Validate lockCodes field + mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" }) + -- Validate migration complete flag + mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "migrationComplete", true) + -- setup codes + test.socket.zwave:__queue_receive({schlage_mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) + test.socket.zwave:__queue_receive({schlage_mock_device.id, Configuration:Report({ parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, configuration_value = 6 })}) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6))) + test.wait_for_events() + -- Validate migrate command + test.socket.capability:__queue_receive({ schlage_mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + end +) + +test.run_registered_tests() \ No newline at end of file From 1230bdd43e9ae4f616dadfc94f18208951b4fb54 Mon Sep 17 00:00:00 2001 From: Pegor Date: Wed, 1 Oct 2025 14:36:58 -0700 Subject: [PATCH 04/11] PR discussion fixes --- drivers/SmartThings/zigbee-lock/src/init.lua | 5 ++--- drivers/SmartThings/zwave-lock/src/init.lua | 19 ++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index f76eaf51e0..8fc0a3499c 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -310,10 +310,9 @@ local migrate = function(driver, device, command) end local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) - local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 4) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 8) local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - if (code_length ~= nil) then max_code_len = code_length min_code_len = code_length diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index cb44541ebf..2710ee7a59 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -160,27 +160,20 @@ local function migrate(driver, device, cmd) end for code in pairs(lock_codes) do - table.insert(ordered_codes, code) + table.insert(ordered_codes, code) end table.sort(ordered_codes) for index = 1, #ordered_codes do - local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ] - table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name}) - table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) + local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ] + table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name}) + table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) end local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) - local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 4) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 10) local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - - if (min_code_len == nil) then - min_code_len = 4 -- per ZWave spec - end - if (max_code_len == nil) then - max_code_len = 10 -- per ZWave spec - end if (code_length ~= nil) then max_code_len = code_length min_code_len = code_length From cd64fb91be47c9852496e19e2383805df9f8d70f Mon Sep 17 00:00:00 2001 From: Pegor Date: Wed, 3 Dec 2025 15:01:29 -0800 Subject: [PATCH 05/11] Initial split of drivers using new and old capabilities --- drivers/SmartThings/zigbee-lock/src/init.lua | 353 +------- .../zigbee-lock/src/lock_utils.lua | 6 +- .../test_zigbee_lock_new_capabilities.lua | 796 ++++++++++++++++++ .../test_zigbee_yale-fingerprint-lock.lua | 33 +- .../src/using-new-capabilities/init.lua | 302 +++++++ .../lock-without-codes/init.lua | 0 .../samsungsds/init.lua | 121 +++ .../yale-fingerprint-lock/init.lua | 54 ++ .../src/using-new-capabilities/yale/init.lua | 109 +++ .../yale/yale-bad-battery-reporter/init.lua | 0 .../src/using-old-capabilities/init.lua | 422 ++++++++++ .../lock-without-codes/init.lua | 101 +++ .../samsungsds/init.lua | 5 +- .../yale-fingerprint-lock/init.lua | 0 .../yale/init.lua | 2 +- .../yale/yale-bad-battery-reporter/init.lua | 52 ++ 16 files changed, 2009 insertions(+), 347 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-new-capabilities}/lock-without-codes/init.lua (100%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-new-capabilities}/yale/yale-bad-battery-reporter/init.lua (100%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/samsungsds/init.lua (97%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale-fingerprint-lock/init.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{ => using-old-capabilities}/yale/init.lua (98%) create mode 100644 drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 8fc0a3499c..06732d3480 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -42,21 +42,16 @@ local lock_utils = require "lock_utils" local DELAY_LOCK_EVENT = "_delay_lock_event" local MAX_DELAY = 10 -local reload_all_codes = function(driver, device, command) - -- starts at first user code index then iterates through all lock codes as they come in - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) == nil) then - device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) == nil) then - device:send(LockCluster.attributes.MinPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) == nil) then - device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) +local function lazy_load_if_possible(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + + -- version 9 will include the lazy loading functions + if version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) end - if (device:get_field(lock_utils.CHECKING_CODE) == nil) then device:set_field(lock_utils.CHECKING_CODE, 0) end - device:emit_event(LockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) - device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) end local refresh = function(driver, device, cmd) @@ -73,30 +68,6 @@ local refresh = function(driver, device, cmd) end end -local do_configure = function(self, device) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) - - device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) - device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) - - device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) - device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) - - -- Don't send a reload all codes if this is a part of migration - if device.data.lockCodes == nil or device:get_field(lock_utils.MIGRATION_RELOAD_SKIPPED) == true then - device.thread:call_with_delay(2, function(d) - self:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - }) - end) - else - device:set_field(lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) - end -end - local alarm_handler = function(driver, device, zb_mess) local ALARM_REPORT = { [0] = Lock.lock.unknown(), @@ -108,225 +79,6 @@ local alarm_handler = function(driver, device, zb_mess) end end -local get_pin_response_handler = function(driver, device, zb_mess) - local event = LockCodes.codeChanged("", { state_change = true }) - local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) - event.data = {codeName = lock_utils.get_code_name(device, code_slot)} - if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then - -- Code slot is occupied - event.value = code_slot .. lock_utils.get_change_type(device, code_slot) - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = event.data.codeName - device:emit_event(event) - lock_utils.lock_codes_event(device, lock_codes) - lock_utils.reset_code_state(device, code_slot) - else - -- Code slot is unoccupied - if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then - -- Code has been deleted - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) - else - -- Code is unset - event.value = code_slot .. " unset" - device:emit_event(event) - end - end - - code_slot = tonumber(code_slot) - if (code_slot == device:get_field(lock_utils.CHECKING_CODE)) then - -- the code we're checking has arrived - local last_slot = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - 1 - if (code_slot >= last_slot) then - device:emit_event(LockCodes.scanCodes("Complete", { visibility = { displayed = false } })) - device:set_field(lock_utils.CHECKING_CODE, nil) - else - local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 - device:set_field(lock_utils.CHECKING_CODE, checkingCode) - device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) - end - end -end - -local programming_event_handler = function(driver, device, zb_mess) - local event = LockCodes.codeChanged("", { state_change = true }) - local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) - event.data = {} - if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then - -- Master code changed - event.value = "0 set" - event.data = {codeName = "Master Code"} - device:emit_event(event) - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then - if (zb_mess.body.zcl_body.user_id.value == 0xFF) then - -- All codes deleted - for cs, _ in pairs(lock_utils.get_lock_codes(device)) do - lock_utils.code_deleted(device, cs) - end - lock_utils.lock_codes_event(device, {}) - else - -- One code deleted - if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) - end - end - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or - zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then - -- Code added or changed - local change_type = lock_utils.get_change_type(device, code_slot) - local code_name = lock_utils.get_code_name(device, code_slot) - event.value = code_slot .. change_type - event.data = {codeName = code_name} - device:emit_event(event) - if (change_type == " set") then - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = code_name - lock_utils.lock_codes_event(device, lock_codes) - end - end -end - -local handle_max_codes = function(driver, device, value) - if value.value ~= 0 then - -- Here's where we'll end up if we queried a lock whose profile does not have lock codes, - -- but it gave us a non-zero number of pin users, so we want to switch the profile - if not device:supports_capability_by_id(LockCodes.ID) then - device:try_update_metadata({profile = "base-lock"}) -- switch to a lock with codes - lock_utils.populate_state_from_data(device) -- if this was a migrated device, try to migrate the lock codes - if not device:get_field(lock_utils.MIGRATION_COMPLETE) then -- this means we didn't find any pre-migration lock codes - -- so we'll load them manually - driver:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - }) - end - end - device:emit_event(LockCodes.maxCodes(value.value, { visibility = { displayed = false } })) - end -end - -local handle_max_code_length = function(driver, device, value) - device:emit_event(LockCodes.maxCodeLength(value.value, { visibility = { displayed = false } })) -end - -local handle_min_code_length = function(driver, device, value) - device:emit_event(LockCodes.minCodeLength(value.value, { visibility = { displayed = false } })) -end - -local update_codes = function(driver, device, command) - local delay = 0 - -- args.codes is json - for name, code in pairs(command.args.codes) do - -- these seem to come in the format "code[slot#]: code" - local code_slot = tonumber(string.gsub(name, "code", ""), 10) - if (code_slot ~= nil) then - if (code ~= nil and (code ~= "0" and code ~= "")) then - device.thread:call_with_delay(delay, function () - device:send(LockCluster.server.commands.SetPINCode(device, - code_slot, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - code)) - end) - delay = delay + 2 - else - device.thread:call_with_delay(delay, function () - device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) - end) - delay = delay + 2 - end - device.thread:call_with_delay(delay, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, code_slot)) - end) - delay = delay + 2 - 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 max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 4) - local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 8) - local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - if (code_length ~= nil) then - max_code_len = code_length - min_code_len = code_length - end - - device:emit_event(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(LockCodes.migrated(true, { visibility = { displayed = false } })) -end - local function device_added(driver, device) lock_utils.populate_state_from_data(device) @@ -367,69 +119,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 @@ -449,31 +138,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, - [LockCodes.commands.migrate.NAME] = migrate, - }, [Lock.ID] = { [Lock.commands.lock.NAME] = lock, [Lock.commands.unlock.NAME] = unlock, @@ -483,13 +155,10 @@ local zigbee_lock_driver = { } }, sub_drivers = { - require("samsungsds"), - require("yale"), - require("yale-fingerprint-lock"), - require("lock-without-codes") + lazy_load_if_possible("using-old-capabilities"), + lazy_load_if_possible("using-new-capabilities"), }, lifecycle_handlers = { - doConfigure = do_configure, added = device_added, init = init, }, diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index 0a36a9685e..eed6391588 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -24,7 +24,11 @@ local lock_utils = { CODE_STATE = "codeState", MIGRATION_COMPLETE = "migrationComplete", MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped", - CHECKED_CODE_SUPPORT = "checkedCodeSupport" + CHECKED_CODE_SUPPORT = "checkedCodeSupport", + COMMAND_NAME = "commandName", + USER_NAME = "userName", + USER_INDEX = "userIndex", + USER_TYPE = "userType" } lock_utils.get_lock_codes = function(device) 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..6e56a34b0c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_new_capabilities.lua @@ -0,0 +1,796 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local 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) + 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.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.lockCodes.migrated(true, { visibility = { displayed = false } }))) +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 +) + +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 +) + +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()) + } + } +) + +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)) + } + } +) + +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" } })) + } + } +) + +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 } })) + } + } +) + +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) } + } + } +) + +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 }})) + } + } +) + +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 }})) + } + } +) + +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 }})) + } + } +) + +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 +) + +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) } + } + } +) + +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 +) + +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 +) + +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 +) + +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 +) + +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 +) + +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 +) + +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 }) + ) + } + } +) + +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 } })) + } + } +) + +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 +) + +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 +) + +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 +) + +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 +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua index 2255c063a3..830f3acb1a 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua @@ -27,7 +27,17 @@ 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.lockCodes.migrated(true, { visibility = { displayed = false } }))) +end test.register_message_test( "Max user code number report should be handled", @@ -43,7 +53,26 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(30)) } - } + }, + {test_init = test_init } +) + +test.register_message_test( + "Max user code number report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, + 16) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(30)) + } + }, + {test_init = test_init_new_capabilities } ) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua new file mode 100644 index 0000000000..5fae1468bf --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -0,0 +1,302 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + + + +-- Zigbee Driver utilities +local defaults = require "st.zigbee.defaults" +local device_management = require "st.zigbee.device_management" +local ZigbeeDriver = require "st.zigbee" + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +-- Capabilities +local capabilities = require "st.capabilities" +local Battery = capabilities.battery +local Lock = capabilities.lock +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local lock_utils = require "lock_utils" + +local socket = require "cosock.socket" +local lock_utils = require "lock_utils" + +local DELAY_LOCK_EVENT = "_delay_lock_event" +local MAX_DELAY = 10 + +local INITIAL_CREDENTIAL_INDEX = 1 -- only used to obtain the next available index. + +local add_user_handler = function(driver, device, command) + local cmdName = "addUser" + local userName = command.args.userName + local userType = command.args.userType + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) + device:set_field(lock_utils.USER_NAME, userName, {persist = true}) + device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) + + -- Send a request to server to get an index. + local ep = device:component_to_endpoint(command.component) + device:send(LockCluster.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) +end + +local update_user_handler = function(driver, device, command) +-- TODO -- +print("---- PK TODO --- ") +end + +local delete_user_handler = function(driver, device, command) +-- TODO +end + +local delete_all_users_handler = function(driver, device, command) +-- TODO +end + +local add_credential_handler = function(driver, device, command) +-- TODO +end + +local update_credential_handler = function(driver, device, command) +-- TODO +end + +local delete_credential_handler = function(driver, device, command) +-- TODO +end + +local delete_all_credentials_handler = function(driver, device, command) +-- TODO +end + +local max_code_length_handler = function(driver, device, value) + device:emit_event(LockCredentials.maxPinCodeLen(value.value, {visibility = {displayed = false}})) +end + +local min_code_length_handler = function(driver, device, value) + device:emit_event(LockCredentials.minPinCodeLen(value.value, {visibility = {displayed = false}})) +end + +local max_codes_handler = function(driver, device, value) + device:emit_event(LockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) +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 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 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, + }, + }, + sub_drivers = { + require("using-new-capabilities.samsungsds"), + require("using-new-capabilities.yale-fingerprint-lock"), + require("using-new-capabilities.yale"), + require("using-new-capabilities.lock-without-codes") + }, + can_handle = function(opts, driver, device, ...) + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + print("--- PK NEW CAPABILITIES --") + local subdriver = require("using-new-capabilities") + return true, subdriver + end + return false + end +} + +return new_capabilities_driver \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-new-capabilities/lock-without-codes/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua new file mode 100644 index 0000000000..2d3ef0ec97 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua @@ -0,0 +1,121 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local device_management = require "st.zigbee.device_management" +local clusters = require "st.zigbee.zcl.clusters" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local capabilities = require "st.capabilities" +local cluster_base = require "st.zigbee.cluster_base" +local PowerConfiguration = clusters.PowerConfiguration +local DoorLock = clusters.DoorLock +local Lock = capabilities.lock + +local SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND = 0x1F +local SAMSUNG_SDS_MFR_CODE = 0x0003 + +local function handle_lock_state(driver, device, value, zb_rx) + if value.value == DoorLock.attributes.LockState.LOCKED then + device:emit_event(Lock.lock.locked()) + elseif value.value == DoorLock.attributes.LockState.UNLOCKED then + device:emit_event(Lock.lock.unlocked()) + end +end + +local function mfg_lock_door_handler(driver, device, zb_rx) + local cmd = zb_rx.body.zcl_body.body_bytes:byte(1) + if cmd == 0x00 then + device:emit_event(Lock.lock.unlocked()) + end +end + +local function unlock_cmd_handler(driver, device, command) + device:send(cluster_base.build_manufacturer_specific_command( + device, + DoorLock.ID, + SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND, + SAMSUNG_SDS_MFR_CODE, + "\x10\x04\x31\x32\x33\x35")) +end + +local function lock_cmd_handler(driver, device, command) + -- do nothing in lock command handler +end + +local refresh = function(driver, device, cmd) + -- do nothing in refresh capability handler +end + +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +local device_added = function(self, device) + emit_event_if_latest_state_missing(device, "main", capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) + device:emit_event(capabilities.battery.battery(100)) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, DoorLock.ID, self.environment_info.hub_zigbee_eui)) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(DoorLock.attributes.LockState:configure_reporting(device, 0, 3600, 0)) +end + +local battery_init = battery_defaults.build_linear_voltage_init(4.0, 6.0) + +local device_init = function(driver, device, event) + battery_init(driver, device, event) + device:remove_monitored_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) + device:remove_configured_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) +end + +local samsung_sds_driver = { + NAME = "SAMSUNG SDS Lock Driver", + zigbee_handlers = { + cluster = { + [DoorLock.ID] = { + [SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND] = mfg_lock_door_handler + } + }, + attr = { + [DoorLock.ID] = { + [DoorLock.attributes.LockState.ID] = handle_lock_state + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh + }, + [capabilities.lock.ID] = { + [capabilities.lock.commands.unlock.NAME] = unlock_cmd_handler, + [capabilities.lock.commands.lock.NAME] = lock_cmd_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + added = device_added, + init = device_init + }, + can_handle = function(opts, driver, device, ...) + if device:get_manufacturer() == "SAMSUNG SDS" and lock_codes_migrated then + local subdriver = require("using-new-capabilities.samsungsds") + return true, subdriver + end + return false + end +} + +return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua new file mode 100644 index 0000000000..b336cb1bc5 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale-fingerprint-lock/init.lua @@ -0,0 +1,54 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local LockCluster = clusters.DoorLock +local LockCredentials = capabilities.lockCredentials + +local YALE_FINGERPRINT_MAX_CODES = 0x1E + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +local yale_fingerprint_lock_models = function(opts, driver, device) + for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local handle_max_codes = function(driver, device, value) + device:emit_event(LockCredentials.maxPinCodeLen(YALE_FINGERPRINT_MAX_CODES)) +end + +local yale_fingerprint_lock_driver = { + NAME = "YALE Fingerprint Lock", + zigbee_handlers = { + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes + } + } + }, + can_handle = yale_fingerprint_lock_models +} + +return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua new file mode 100644 index 0000000000..d00f9ed73f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/init.lua @@ -0,0 +1,109 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local LockCluster = clusters.DoorLock + +-- Capabilities +local capabilities = require "st.capabilities" +local LockCredentials = capabilities.lockCredentials + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType + +local lock_utils = (require "lock_utils") + +local get_pin_response_handler = function(driver, device, zb_mess) + local event = LockCredentials.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + local localCode = device:get_field(lock_utils.CODE_STATE) + if localCode ~= nil then localCode = localCode["setCode"..code_slot] end + local code_name = lock_utils.get_code_name(device, code_slot) + local lock_codes = lock_utils.get_lock_codes(device) + event.data = {codeName = code_name} + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + -- Code slot is occupied + if (localCode ~= nil) then + local serverCode = zb_mess.body.zcl_body.code.value + if (localCode == serverCode) then + event.value = code_slot .. lock_utils.get_change_type(device, code_slot) + device:emit_event(event) + lock_codes[code_slot] = code_name + lock_utils.lock_codes_event(device, lock_codes) + else + event.value = code_slot .. " failed" + event.data = nil + device:emit_event(event) + end + lock_utils.reset_code_state(device, code_slot) + else + event.value = code_slot .. lock_utils.get_change_type(device, code_slot) + device:emit_event(event) + lock_codes[code_slot] = code_name + lock_utils.lock_codes_event(device, lock_codes) + end + else + -- Code slot is unoccupied + if (localCode ~= nil) then + -- Code slot found empty during creation of a user code + event.value = code_slot .. " failed" + event.data = nil + device:emit_event(event) + event.value = code_slot .. " is not set" + device:emit_event(event) + lock_utils.reset_code_state(device, code_slot) + elseif (lock_codes[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 defaultMaxCodes = 8 + if (code_slot >= defaultMaxCodes) 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 yale_door_lock_driver = { + NAME = "Yale Door Lock", + zigbee_handlers = { + cluster = { + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + } + } + }, + + sub_drivers = { require("using-new-capabilities.yale.yale-bad-battery-reporter") }, + can_handle = function(opts, driver, device, ...) + return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" + end +} + +return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-new-capabilities/yale/yale-bad-battery-reporter/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua new file mode 100644 index 0000000000..158bb643d0 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -0,0 +1,422 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Zigbee Driver utilities +local defaults = require "st.zigbee.defaults" +local device_management = require "st.zigbee.device_management" +local ZigbeeDriver = require "st.zigbee" + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +-- Capabilities +local capabilities = require "st.capabilities" +local Battery = capabilities.battery +local Lock = capabilities.lock +local LockCodes = capabilities.lockCodes +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local socket = require "cosock.socket" +local lock_utils = require "lock_utils" + +local DELAY_LOCK_EVENT = "_delay_lock_event" +local MAX_DELAY = 10 + +local reload_all_codes = function(driver, device, command) + -- starts at first user code index then iterates through all lock codes as they come in + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + end + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then device:set_field(lock_utils.CHECKING_CODE, 0) end + device:emit_event(LockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +end + +local get_pin_response_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = {codeName = lock_utils.get_code_name(device, code_slot)} + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + -- Code slot is occupied + event.value = code_slot .. lock_utils.get_change_type(device, code_slot) + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = event.data.codeName + device:emit_event(event) + lock_utils.lock_codes_event(device, lock_codes) + lock_utils.reset_code_state(device, code_slot) + else + -- Code slot is unoccupied + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + -- Code has been deleted + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + else + -- Code is unset + event.value = code_slot .. " unset" + device:emit_event(event) + end + end + + code_slot = tonumber(code_slot) + if (code_slot == device:get_field(lock_utils.CHECKING_CODE)) then + -- the code we're checking has arrived + local last_slot = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - 1 + if (code_slot >= last_slot) then + device:emit_event(LockCodes.scanCodes("Complete", { visibility = { displayed = false } })) + device:set_field(lock_utils.CHECKING_CODE, nil) + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) + end + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = {} + if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then + -- Master code changed + event.value = "0 set" + event.data = {codeName = "Master Code"} + device:emit_event(event) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (zb_mess.body.zcl_body.user_id.value == 0xFF) then + -- All codes deleted + for cs, _ in pairs(lock_utils.get_lock_codes(device)) do + lock_utils.code_deleted(device, cs) + end + lock_utils.lock_codes_event(device, {}) + else + -- One code deleted + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + -- Code added or changed + local change_type = lock_utils.get_change_type(device, code_slot) + local code_name = lock_utils.get_code_name(device, code_slot) + event.value = code_slot .. change_type + event.data = {codeName = code_name} + device:emit_event(event) + if (change_type == " set") then + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = code_name + lock_utils.lock_codes_event(device, lock_codes) + end + end +end + +local handle_max_codes = function(driver, device, value) + if value.value ~= 0 then + -- Here's where we'll end up if we queried a lock whose profile does not have lock codes, + -- but it gave us a non-zero number of pin users, so we want to switch the profile + if not device:supports_capability_by_id(LockCodes.ID) then + device:try_update_metadata({profile = "base-lock"}) -- switch to a lock with codes + lock_utils.populate_state_from_data(device) -- if this was a migrated device, try to migrate the lock codes + if not device:get_field(lock_utils.MIGRATION_COMPLETE) then -- this means we didn't find any pre-migration lock codes + -- so we'll load them manually + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end + end + device:emit_event(LockCodes.maxCodes(value.value, { visibility = { displayed = false } })) + end +end + +local handle_max_code_length = function(driver, device, value) + device:emit_event(LockCodes.maxCodeLength(value.value, { visibility = { displayed = false } })) +end + +local handle_min_code_length = function(driver, device, value) + device:emit_event(LockCodes.minCodeLength(value.value, { visibility = { displayed = false } })) +end + +local lock_operation_event_handler = function(driver, device, zb_rx) + local event_code = zb_rx.body.zcl_body.operation_event_code.value + local source = zb_rx.body.zcl_body.operation_event_source.value + local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" + local METHOD = { + [0] = "keypad", + [1] = "command", + [2] = "manual", + [3] = "rfid", + [4] = "fingerprint", + [5] = "bluetooth" + } + local STATUS = { + [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local event = STATUS[event_code] + if (event ~= nil) then + event["data"] = {} + if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or + event_code == OperationEventCode.SCHEDULE_LOCK or + event_code == OperationEventCode.SCHEDULE_UNLOCK + ) then + event.data.method = "auto" + else + event.data.method = METHOD[source] + end + if (source == 0 and device:supports_capability_by_id(capabilities.lockCodes.ID)) then --keypad + local code_id = zb_rx.body.zcl_body.user_id.value + local code_name = "Code "..code_id + local lock_codes = device:get_field("lockCodes") + if (lock_codes ~= nil and + lock_codes[code_id] ~= nil) then + code_name = lock_codes[code_id] + end + event.data = {method = METHOD[0], codeId = code_id .. "", codeName = code_name} + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), + capabilities.lock.ID, + capabilities.lock.lock.ID) == event.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) + end +end + +local update_codes = function(driver, device, command) + local delay = 0 + -- args.codes is json + for name, code in pairs(command.args.codes) do + -- these seem to come in the format "code[slot#]: code" + local code_slot = tonumber(string.gsub(name, "code", ""), 10) + if (code_slot ~= nil) then + if (code ~= nil and (code ~= "0" and code ~= "")) then + device.thread:call_with_delay(delay, function () + device:send(LockCluster.server.commands.SetPINCode(device, + code_slot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + code)) + end) + delay = delay + 2 + else + device.thread:call_with_delay(delay, function () + device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) + end) + delay = delay + 2 + end + device.thread:call_with_delay(delay, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, code_slot)) + end) + delay = delay + 2 + end + end +end + +local delete_code = function(driver, device, command) + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, command.args.codeSlot)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) +end + +local request_code = function(driver, device, command) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) +end + +local set_code = function(driver, device, command) + if (command.args.codePIN == "") then + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = {command.args.codeSlot, command.args.codeName} + }) + else + device:send(LockCluster.server.commands.SetPINCode(device, + command.args.codeSlot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + command.args.codePIN) + ) + if (command.args.codeName ~= nil) then + -- wait for confirmation from the lock to commit this to memory + -- Groovy driver has a lot more info passed here as a description string, may need to be investigated + local codeState = device:get_field(lock_utils.CODE_STATE) or {} + codeState["setName"..command.args.codeSlot] = command.args.codeName + device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) + end + + device.thread:call_with_delay(4, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) + end +end + +local name_slot = function(driver, device, command) + local code_slot = tostring(command.args.codeSlot) + local lock_codes = lock_utils.get_lock_codes(device) + if (lock_codes[code_slot] ~= nil) then + lock_codes[code_slot] = command.args.codeName + device:emit_event(LockCodes.codeChanged(code_slot .. " renamed", { state_change = true })) + lock_utils.lock_codes_event(device, lock_codes) + end +end + +local migrate = function(driver, device, command) + local lock_users = {} + local lock_credentials = {} + local lock_codes = lock_utils.get_lock_codes(device) + local ordered_codes = {} + + for code in pairs(lock_codes) do + table.insert(ordered_codes, code) + end + + table.sort(ordered_codes) + for index, code_slot in ipairs(ordered_codes) do + table.insert(lock_users, {userIndex = index, userType = "guest", userName = lock_codes[code_slot]}) + table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) + end + + local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 4) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 8) + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME, 0) + if (code_length ~= nil) then + max_code_len = code_length + min_code_len = code_length + end + + device:emit_event(LockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) + device:emit_event(LockUsers.users(lock_users, { visibility = { displayed = false } })) + device:emit_event(LockCodes.migrated(true, { visibility = { displayed = false } })) +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) + + device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) + device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) + + device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) + device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) + + -- Don't send a reload all codes if this is a part of migration + if device.data.lockCodes == nil or device:get_field(lock_utils.MIGRATION_RELOAD_SKIPPED) == true then + device.thread:call_with_delay(2, function(d) + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end) + else + device:set_field(lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) + end +end + +local old_capabilities_driver = { + NAME = "Lock Driver Using Old Capabilities", + supported_capabilities = { + Lock, + LockCodes, + Battery, + }, + zigbee_handlers = { + cluster = { + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, + [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler + } + }, + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.MaxPINCodeLength.ID] = handle_max_code_length, + [LockCluster.attributes.MinPINCodeLength.ID] = handle_min_code_length, + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes, + } + } + }, + capability_handlers = { + [LockCodes.ID] = { + [LockCodes.commands.updateCodes.NAME] = update_codes, + [LockCodes.commands.deleteCode.NAME] = delete_code, + [LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [LockCodes.commands.requestCode.NAME] = request_code, + [LockCodes.commands.setCode.NAME] = set_code, + [LockCodes.commands.nameSlot.NAME] = name_slot, + [LockCodes.commands.migrate.NAME] = migrate, + }, + }, + sub_drivers = { + require("using-old-capabilities.samsungsds"), + require("using-old-capabilities.yale"), + require("using-old-capabilities.yale-fingerprint-lock"), + require("using-old-capabilities.lock-without-codes") + }, + health_check = false, + lifecycle_handlers = { + doConfigure = do_configure + }, + can_handle = function(opts, driver, device, ...) + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) + if not lock_codes_migrated then + local subdriver = require("using-old-capabilities") + return true, subdriver + end + return false + end +} + +return old_capabilities_driver \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua new file mode 100644 index 0000000000..7272991459 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/lock-without-codes/init.lua @@ -0,0 +1,101 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local configurationMap = require "configurations" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local DoorLock = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +local function can_handle_lock_without_codes(opts, driver, device) + for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function device_init(driver, device) + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) + end + end +end + +local function handle_lock(driver, device, cmd) + device:send(DoorLock.commands.LockDoor(device)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) +end + +local function handle_unlock(driver, device, cmd) + device:send(DoorLock.commands.UnlockDoor(device)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) +end + +local function do_refresh(driver, device) + device:refresh() +end + +local function do_configure(driver, device) + device:configure() +end + +local function handle_lock_door(driver, device, zb_rx) + local function query_device() + device:send(DoorLock.attributes.LockState:read(device)) + end + device.thread:call_with_delay(5, query_device) +end + +local lock_without_codes = { + NAME = "Zigbee Lock Without Codes", + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure + }, + capability_handlers = { + [capabilities.lock.ID] = { + [capabilities.lock.commands.lock.NAME] = handle_lock, + [capabilities.lock.commands.unlock.NAME] = handle_unlock + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + cluster = { + [DoorLock.ID] = { + [DoorLock.commands.LockDoorResponse.ID] = handle_lock_door, + [DoorLock.commands.UnlockDoorResponse.ID] = handle_lock_door, + } + }, + attr = { + [DoorLock.ID] = { + [DoorLock.attributes.NumberOfPINUsersSupported.ID] = function() end -- just to make sure we don't switch profiles + } + } + }, + can_handle = can_handle_lock_without_codes +} + +return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua similarity index 97% rename from drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua index b529dd3fd1..214a963e02 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua @@ -113,7 +113,10 @@ local samsung_sds_driver = { init = device_init }, can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SAMSUNG SDS" + if device:get_manufacturer() == "SAMSUNG SDS" then + return true + end + return false end } diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale-fingerprint-lock/init.lua diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua similarity index 98% rename from drivers/SmartThings/zigbee-lock/src/yale/init.lua rename to drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua index 73e984036e..a38e6be361 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/init.lua @@ -152,7 +152,7 @@ local yale_door_lock_driver = { } }, - sub_drivers = { require("yale.yale-bad-battery-reporter") }, + sub_drivers = { require("using-old-capabilities.yale.yale-bad-battery-reporter") }, can_handle = function(opts, driver, device, ...) return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua new file mode 100644 index 0000000000..59fdbf228b --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/yale/yale-bad-battery-reporter/init.lua @@ -0,0 +1,52 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +local is_bad_yale_lock_models = function(opts, driver, device) + for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local battery_report_handler = function(driver, device, value) + device:emit_event(capabilities.battery.battery(value.value)) +end + +local bad_yale_driver = { + NAME = "YALE BAD Lock Driver", + zigbee_handlers = { + attr = { + [clusters.PowerConfiguration.ID] = { + [clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_report_handler + } + } + }, + can_handle = is_bad_yale_lock_models +} + +return bad_yale_driver From 7b0f7220237ab9a58c7a48675d2d0b9afe2e1763 Mon Sep 17 00:00:00 2001 From: Pegor Date: Wed, 10 Dec 2025 15:14:47 -0800 Subject: [PATCH 06/11] Add lockCredentials and lockUsers commands --- .../zigbee-lock/src/lock_utils.lua | 4 - .../zigbee-lock/src/new_lock_utils.lua | 184 ++++ .../test_zigbee_lock_code_slga_migration.lua | 1 + .../test_zigbee_lock_new_capabilities.lua | 961 +++++------------- .../test_zigbee_yale-fingerprint-lock.lua | 1 + .../src/using-new-capabilities/init.lua | 314 ++++-- .../src/using-old-capabilities/init.lua | 1 + 7 files changed, 667 insertions(+), 799 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index eed6391588..901c29f030 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -25,10 +25,6 @@ local lock_utils = { MIGRATION_COMPLETE = "migrationComplete", MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped", CHECKED_CODE_SUPPORT = "checkedCodeSupport", - COMMAND_NAME = "commandName", - USER_NAME = "userName", - USER_INDEX = "userIndex", - USER_TYPE = "userType" } 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..c097219584 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -0,0 +1,184 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +local utils = require "st.utils" +local capabilities = require "st.capabilities" +local json = require "st.json" +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers +local INITIAL_INDEX = 1 + +local new_lock_utils = { + -- Constants + ADD_CREDENTIAL = "addCredential", + ADD_USER = "addUser", + COMMAND_NAME = "commandName", + CREDENTIAL_TYPE = "pin", + DELETE_ALL_CREDENTIALS = "deleteAllCredentials", + DELETE_ALL_USERS = "deleteAllUsers", + DELETE_CREDENTIAL = "deleteCredential", + DELETE_USER = "deleteUser", + LOCK_CREDENTIALS = "lockCredentials", + LOCK_USERS = "lockUsers", + PENDING_CREDENTIAL = "pendingCredential", + STATUS_BUSY = "busy", + STATUS_DUPLICATE = "duplicate", + STATUS_FAILURE = "failure", + STATUS_INVALID_COMMAND = "invalidCommand", + STATUS_OCCUPIED = "occupied", + STATUS_RESOURCE_EXHAUSTED = "resourceExhausted", + STATUS_SUCCESS = "success", + UPDATE_CREDENTIAL = "updateCredential", + UPDATE_USER = "updateUser", + USER_INDEX = "userIndex", + USER_NAME = "userName", + USER_TYPE = "userType" +} + +new_lock_utils.get_users = function(device) + local users = device:get_field(new_lock_utils.LOCK_USERS) + return users ~= nil and users or {} +end + +new_lock_utils.get_user = function(device, user_index) + for _, user in ipairs(new_lock_utils.get_users(device)) do + if user.userIndex == user_index then + return user + end + end + + return nil +end + +new_lock_utils.get_credentials = function(device) + local credentials = device:get_field(new_lock_utils.LOCK_CREDENTIALS) + return credentials ~= nil and credentials or {} +end + +new_lock_utils.get_available_index = function(current_data, max) + local available_index = nil + local used_index = {} + + for i, _ in ipairs(current_data) do + used_index[i] = true + end + + if current_data ~= {} then + for index = 1, max do + if used_index[index] == nil then + available_index = index + break + end + end + else + available_index = INITIAL_INDEX + end + + return available_index +end + +new_lock_utils.create_user = function(device, user_name, user_type, user_index) + local status_code = new_lock_utils.STATUS_SUCCESS + local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME, 0) + local current_users = new_lock_utils.get_users(device) + local available_index = new_lock_utils.get_available_index(current_users, max_users) + + if max_users == 0 or available_index == nil then + -- Can't add any users - update commandResult statusCode + status_code = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + else + -- use the passed in index if it's set + if user_index ~= nil then + available_index = user_index + end + table.insert(current_users, {userIndex = available_index, userType = user_type, userName = user_name}) + device:set_field(new_lock_utils.LOCK_USERS, current_users) + end + + return status_code +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 ipairs(current_users) do + if user.userIndex == user_index then + table.remove(current_users, index) + device:set_field(new_lock_utils.LOCK_USERS, current_users) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + + return status_code +end + +new_lock_utils.add_credential = function(device, user_index, user_type, credential_type, credential_index) + -- need to also create a user if one does not exist at the user index. + if new_lock_utils.get_user(device, user_index) == nil then + local user_name = "USER_" .. user_index + local status = new_lock_utils.create_user(device, user_name, user_type, user_index) + if status ~= new_lock_utils.STATUS_SUCCESS then + return status + end + end + + local credentials = new_lock_utils.get_credentials(device) + table.insert(credentials, {userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type}) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + return new_lock_utils.STATUS_SUCCESS +end + +new_lock_utils.delete_credential = function(device, credential_index) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for index, credential in ipairs(credentials) do + if credential.userIndex == credential_index then + table.remove(credentials, index) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + + return status_code +end + +new_lock_utils.get_credential = function(device, credential_index) + for _, credential in ipairs(new_lock_utils.get_credentials(device)) do + if credential.credentialIndex == credential_index then + return credential + end + end + return nil +end + +new_lock_utils.update_credential = function(device, credential_index, user_index, credential_type) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for _, credential in ipairs(credentials) do + if credential.credentialIndex == credential_index then + credential.credentialType = credential_type + credential.userIndex = user_index + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + return status_code +end + +return new_lock_utils \ No newline at end of file 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 569dca5236..d5d970c010 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 @@ -74,6 +74,7 @@ test.register_coroutine_test( 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 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 6e56a34b0c..451ef5e217 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,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -23,22 +23,45 @@ 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 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") } + { + profile = t_utils.get_profile_definition("base-lock.yml"), + } +) + +local mock_device_with_users = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + data = { + lockCodes = json.encode({ + ["1"] = "Zach", + ["3"] = "Steven" + }), + } + } ) + zigbee_test_utils.prepare_zigbee_env_info() -local function test_init() - test.mock_device.add_test_device(mock_device) + +local function test_init_new_capabilities() + 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 } }))) @@ -46,751 +69,271 @@ local function test_init() 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 -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 -) - -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 -) - -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()) - } - } -) - -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)) - } - } -) - -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" } })) - } - } -) - -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 } })) - } - } -) - -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) } - } - } -) - -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 }})) - } - } -) - -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 }})) - } - } -) - -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 }})) - } - } -) - -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 -) - -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) } - } - } -) - -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 -) - -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( - { +local function add_default_users() + local user_list = {} + for i = 1, 4 do + test.socket.capability:__queue_receive({ mock_device.id, - DoorLock.client.commands.GetPINCodeResponse.build_test_rx( - mock_device, - 0x01, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "1234" + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = {"TestUser" .. i, "guest"} + }, + }) + -- add to the user list that is now expected + table.insert(user_list, {userIndex = i, userType = "guest", userName = "TestUser" .. i}) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + user_list, + {visibility={displayed=false}} + ) ) - } - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test" }, state_change = true }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }))) - end -) - -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( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + {commandName="addUser", statusCode="success"}, + {state_change=true, visibility={displayed=false}} + ) + ) ) - } - ) - test.socket.capability:__expect_send(device:generate_test_message("main", - capabilities.lockCodes.codeChanged(slot_number .. " set", { data = { codeName = name }, state_change = true })) - ) + end 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 -) +test.set_test_init_function(test_init_new_capabilities) test.register_coroutine_test( - "Setting a user code name via setCode 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() + -- make sure we have migrated and are using the new capabilities + init_migration() + -- create initial max 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 -) - -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) + -- 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.wait_for_events() + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + {commandName="addUser", statusCode="resourceExhausted"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) 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 -) + "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.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 = "updateUser", + args = {"2", "ChangeUserName", "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( + { + {userIndex=1, userName="TestUser1", userType="guest"}, + {userIndex=2, userName="ChangeUserName", userType="guest"}, + {userIndex=3, userName="TestUser3", userType="guest"}, + {userIndex=4, userName="TestUser4", userType="guest"} + }, + {visibility={displayed=false}} + ) ) - } - } -) - -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 } })) - } - } -) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + {commandName="updateUser", statusCode="success"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) -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" + -- 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=false}} ) - } - ) - 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 + ) + ) + end ) 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 } }))) + "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.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" + -- success + 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.users( + { + {userIndex=1, userName="TestUser1", userType="guest"}, + {userIndex=2, userName="TestUser2", userType="guest"}, + {userIndex=4, userName="TestUser4", userType="guest"} + }, + {visibility={displayed=false}} ) - } - ) - - 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 -) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + {commandName="deleteUser", statusCode="success"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) -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, - "" + -- 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=false}} ) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) - ) - ) - end + ) + ) + end ) + test.register_coroutine_test( - "Lock state attribute reports (after the first) should be delayed if they come before event notifications ", + "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.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( - { + init_migration() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, - DoorLock.client.commands.OperatingEventNotification.build_test_rx( - mock_device, - 0x00, -- 0 = keypad - OperationEventCode.UNLOCK, - 0x0001, - "1234", - 0x0000, - "" + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = {"2", "guest", "pin", "abc123"} + }, + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "abc123" ) - } + } ) - 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(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 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.wait_for_events() + 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, - "" - ) - } + { + 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.lock.lock.locked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) - ) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { + {userIndex = 2, credentialIndex = 1, credentialType = "pin"} + }, + {visibility={displayed=false}} + ) + ) ) - test.mock_time.advance_time(2.5) + test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lock.lock.locked() - ) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + {commandName="addCredential", statusCode="success"}, + {state_change=true, visibility={displayed=false}} + ) + ) ) end ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file 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 830f3acb1a..ddd89ff598 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 @@ -36,6 +36,7 @@ local function test_init_new_capabilities() 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 } }))) end diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 5fae1468bf..7138613650 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -37,59 +37,214 @@ local UserStatusEnum = LockCluster.types.DrlkUserStatus local UserTypeEnum = LockCluster.types.DrlkUserType local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode -local lock_utils = require "lock_utils" - local socket = require "cosock.socket" -local lock_utils = require "lock_utils" +local lock_utils = require "new_lock_utils" local DELAY_LOCK_EVENT = "_delay_lock_event" local MAX_DELAY = 10 -local INITIAL_CREDENTIAL_INDEX = 1 -- only used to obtain the next available index. +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.lockCodes.minPinCodeLen.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCodes.pinUsersSupported.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, 1) + end + + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +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) - local cmdName = "addUser" - local userName = command.args.userName - local userType = command.args.userType - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) - device:set_field(lock_utils.USER_NAME, userName, {persist = true}) - device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) - - -- Send a request to server to get an index. - local ep = device:component_to_endpoint(command.component) - device:send(LockCluster.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) + local user_name = command.args.userName + local user_type = command.args.userType + local status = lock_utils.create_user(device, user_name, user_type, nil) + local command_result_info = { + commandName = lock_utils.ADD_USER, + statusCode = status + } + + if status == lock_utils.STATUS_SUCCESS then + local current_users = lock_utils.get_users(device) + device:emit_event(capabilities.lockUsers.users(current_users, {visibility = {displayed = false}})) + end + + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) end local update_user_handler = function(driver, device, command) --- TODO -- -print("---- PK TODO --- ") + 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 command_result_info = { + commandName = lock_utils.UPDATE_USER, + statusCode = lock_utils.STATUS_FAILURE + } + + for _, user in ipairs(current_users) do + if user.userIndex == user_index then + -- update user info and the commandResult + user.userName = user_name + user.userType = user_type + device:set_field(lock_utils.LOCK_USERS, current_users) + device:emit_event(capabilities.lockUsers.users(current_users, {visibility = {displayed = false}})) + command_result_info.statusCode = lock_utils.STATUS_SUCCESS + break + end + end + + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + end local delete_user_handler = function(driver, device, command) --- TODO + local user_index = tonumber(command.args.userIndex) + local status_code = lock_utils.delete_user(device, user_index) + local command_result_info = { + commandName = lock_utils.DELETE_USER, + statusCode = status_code + } + + if status_code == lock_utils.STATUS_SUCCESS then + local current_users = lock_utils.get_users(device) + device:emit_event(capabilities.lockUsers.users(current_users, {visibility = {displayed = false}})) + end + + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) end local delete_all_users_handler = function(driver, device, command) --- TODO + local current_users = lock_utils.get_users(device) + local command_result_info = { + commandName = lock_utils.DELETE_ALL_USERS, + statusCode = lock_utils.STATUS_SUCCESS + } + current_users = {} -- clear the table + device:set_field(lock_utils.LOCK_USERS, current_users) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) end local add_credential_handler = function(driver, device, command) --- TODO + device:set_field(lock_utils.COMMAND_NAME, lock_utils.ADD_CREDENTIAL) + 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 credential_index = nil + local status_code = lock_utils.STATUS_SUCCESS + local max_credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME, 0) + + credential_index = lock_utils.get_available_index(lock_utils.get_credentials(device), max_credentials) + if credential_index ~= nil then + device:set_field(lock_utils.PENDING_CREDENTIAL, {userIndex = user_index, userType = user_type, credentialType = credential_type}) + else + status_code = lock_utils.STATUS_RESOURCE_EXHAUSTED + end + if status_code == 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: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 + local command_result_info = { + commandName = lock_utils.ADD_CREDENTIAL, + statusCode = status_code + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.COMMAND_NAME, nil) + end end local update_credential_handler = function(driver, device, command) --- TODO + device:set_field(lock_utils.COMMAND_NAME, lock_utils.UPDATE_CREDENTIAL) + + local credential_index = command.args.credentialIndex + local credential_data = command.args.credentialData + local command_result_info = { + commandName = lock_utils.UPDATE_CREDENTIAL, + statusCode = lock_utils.STATUS_FAILURE + } + if lock_utils.get_credential(device, credential_index) ~= nil then + device:send(LockCluster.server.commands.SetPINCode(device, + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) + ) + command_result_info.statusCode = lock_utils.STATUS_SUCCESS + device.thread:call_with_delay(4, function () + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) + end) + end + + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) end local delete_credential_handler = function(driver, device, command) --- TODO + device:set_field(lock_utils.COMMAND_NAME, lock_utils.DELETE_CREDENTIAL) + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, command.args.credentialIndex)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.credentialIndex)) + end) end local delete_all_credentials_handler = function(driver, device, command) --- TODO + device:set_field(lock_utils.COMMAND_NAME, lock_utils.DELETE_ALL_CREDENTIALS) + local credentials = lock_utils.get_credentials(device) + local delay = 0 + for _, credential in ipairs(credentials) do + device.thread:call_with_delay(delay, function () + device:send(LockCluster.server.commands.ClearPINCode(device, credential.credentialIndex)) + end) + device.thread:call_with_delay(delay + 2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential.credentialIndex)) + end) + delay = delay + 2 + end end local max_code_length_handler = function(driver, device, value) @@ -105,35 +260,46 @@ local max_codes_handler = function(driver, device, value) 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)) + local credential_index = zb_mess.body.zcl_body.user_id.value + local pending_credential = device:get_field(lock_utils.PENDING_CREDENTIAL) + local command = device:get_field(lock_utils.COMMAND_NAME) + local command_result_info = { + commandName = command, + statusCode = lock_utils.STATUS_SUCCESS + } + + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + if pending_credential ~= nil then + -- create credential + lock_utils.add_credential(device, pending_credential.userIndex, + pending_credential.userType, + pending_credential.credentialType, + credential_index) else - -- Code is unset - event.value = code_slot .. " unset" - device:emit_event(event) + -- 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) + end + end + + local credentials = lock_utils.get_credentials(device) + device:emit_event(capabilities.lockCredentials.credentials(credentials, {visibility = {displayed = false}})) + else + if lock_utils.get_credential(device, credential_index) ~= nil then + -- Credential has been deleted. + lock_utils.delete_credential(device, credential_index) + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), {visibility = {displayed = false}})) + else + -- unset? 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 } })) + credential_index = tonumber(credential_index) + 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) else local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 @@ -141,44 +307,17 @@ local get_pin_response_handler = function(driver, device, zb_mess) device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) end end + + if command ~= nil then + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.COMMAND_NAME, nil) + 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 + -- TODO end local lock_operation_event_handler = function(driver, device, zb_rx) @@ -288,10 +427,13 @@ local new_capabilities_driver = { require("using-new-capabilities.yale"), require("using-new-capabilities.lock-without-codes") }, + health_check = false, + lifecycle_handlers = { + doConfigure = do_configure + }, can_handle = function(opts, driver, device, ...) local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) if lock_codes_migrated then - print("--- PK NEW CAPABILITIES --") local subdriver = require("using-new-capabilities") return true, subdriver end diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua index 158bb643d0..fe70163ef4 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -338,6 +338,7 @@ local migrate = function(driver, device, command) 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 From ff149aac96fb8c9ec0c1a0fbbc4f61e6b62e9d68 Mon Sep 17 00:00:00 2001 From: Pegor Date: Thu, 11 Dec 2025 14:43:44 -0800 Subject: [PATCH 07/11] formatting --- .../zigbee-lock/src/new_lock_utils.lua | 54 +-- .../test_zigbee_lock_new_capabilities.lua | 442 +++++++++--------- .../src/using-new-capabilities/init.lua | 232 ++++----- .../samsungsds/init.lua | 6 +- .../src/using-old-capabilities/init.lua | 161 +++---- .../samsungsds/init.lua | 5 +- 6 files changed, 458 insertions(+), 442 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua index c097219584..2d193be336 100644 --- a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -18,7 +18,7 @@ local LockCredentials = capabilities.lockCredentials local LockUsers = capabilities.lockUsers local INITIAL_INDEX = 1 -local new_lock_utils = { +local new_lock_utils = { -- Constants ADD_CREDENTIAL = "addCredential", ADD_USER = "addUser", @@ -70,7 +70,7 @@ new_lock_utils.get_available_index = function(current_data, max) local used_index = {} for i, _ in ipairs(current_data) do - used_index[i] = true + used_index[i] = true end if current_data ~= {} then @@ -89,7 +89,8 @@ end new_lock_utils.create_user = function(device, user_name, user_type, user_index) local status_code = new_lock_utils.STATUS_SUCCESS - local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME, 0) + local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, + capabilities.lockUsers.totalUsersSupported.NAME, 0) local current_users = new_lock_utils.get_users(device) local available_index = new_lock_utils.get_available_index(current_users, max_users) @@ -101,7 +102,7 @@ new_lock_utils.create_user = function(device, user_name, user_type, user_index) if user_index ~= nil then available_index = user_index end - table.insert(current_users, {userIndex = available_index, userType = user_type, userName = user_name}) + table.insert(current_users, { userIndex = available_index, userType = user_type, userName = user_name }) device:set_field(new_lock_utils.LOCK_USERS, current_users) end @@ -113,12 +114,12 @@ new_lock_utils.delete_user = function(device, user_index) local status_code = new_lock_utils.STATUS_FAILURE for index, user in ipairs(current_users) do - if user.userIndex == user_index then - table.remove(current_users, index) - device:set_field(new_lock_utils.LOCK_USERS, current_users) - status_code = new_lock_utils.STATUS_SUCCESS - break - end + if user.userIndex == user_index then + table.remove(current_users, index) + device:set_field(new_lock_utils.LOCK_USERS, current_users) + status_code = new_lock_utils.STATUS_SUCCESS + break + end end return status_code @@ -135,22 +136,23 @@ new_lock_utils.add_credential = function(device, user_index, user_type, credenti end local credentials = new_lock_utils.get_credentials(device) - table.insert(credentials, {userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type}) + table.insert(credentials, + { userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type }) device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) return new_lock_utils.STATUS_SUCCESS end -new_lock_utils.delete_credential = function(device, credential_index) +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 ipairs(credentials) do - if credential.userIndex == credential_index then - table.remove(credentials, index) - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) - status_code = new_lock_utils.STATUS_SUCCESS - break - end + if credential.userIndex == credential_index then + table.remove(credentials, index) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end end return status_code @@ -170,15 +172,15 @@ new_lock_utils.update_credential = function(device, credential_index, user_index local status_code = new_lock_utils.STATUS_FAILURE for _, credential in ipairs(credentials) do - if credential.credentialIndex == credential_index then - credential.credentialType = credential_type - credential.userIndex = user_index - device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) - status_code = new_lock_utils.STATUS_SUCCESS - break - end + if credential.credentialIndex == credential_index then + credential.credentialType = credential_type + credential.userIndex = user_index + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end end return status_code end -return new_lock_utils \ No newline at end of file +return new_lock_utils 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 451ef5e217..4948e6bf11 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 @@ -32,46 +32,60 @@ 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"), + profile = t_utils.get_profile_definition("base-lock.yml"), } ) local mock_device_with_users = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("base-lock.yml"), - data = { - lockCodes = json.encode({ - ["1"] = "Zach", - ["3"] = "Steven" - }), - } + profile = t_utils.get_profile_definition("base-lock.yml"), + data = { + lockCodes = json.encode({ + ["1"] = "Zach", + ["3"] = "Steven" + }), + } } ) zigbee_test_utils.prepare_zigbee_env_info() local function test_init_new_capabilities() - test.mock_device.add_test_device(mock_device) + 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() +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() @@ -82,18 +96,18 @@ local function add_default_users() { capability = capabilities.lockUsers.ID, command = "addUser", - args = {"TestUser" .. i, "guest"} + args = { "TestUser" .. i, "guest" } }, }) -- add to the user list that is now expected - table.insert(user_list, {userIndex = i, userType = "guest", userName = "TestUser" .. i}) + table.insert(user_list, { userIndex = i, userType = "guest", userName = "TestUser" .. i }) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockUsers.users( - user_list, - {visibility={displayed=false}} + user_list, + { visibility = { displayed = false } } ) ) ) @@ -101,8 +115,8 @@ local function add_default_users() mock_device:generate_test_message( "main", capabilities.lockUsers.commandResult( - {commandName="addUser", statusCode="success"}, - {state_change=true, visibility={displayed=false}} + { commandName = "addUser", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } ) ) ) @@ -112,228 +126,228 @@ 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() + "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=false}} + -- 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 = false } } + ) ) ) - ) - end + 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() + "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"} - }, - }) - - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( + -- success + test.socket.capability:__queue_receive({ + mock_device.id, { - {userIndex=1, userName="TestUser1", userType="guest"}, - {userIndex=2, userName="ChangeUserName", userType="guest"}, - {userIndex=3, userName="TestUser3", userType="guest"}, - {userIndex=4, userName="TestUser4", userType="guest"} + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = { "2", "ChangeUserName", "guest" } }, - {visibility={displayed=false}} + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "TestUser1", userType = "guest" }, + { userIndex = 2, userName = "ChangeUserName", userType = "guest" }, + { userIndex = 3, userName = "TestUser3", userType = "guest" }, + { userIndex = 4, userName = "TestUser4", userType = "guest" } + }, + { visibility = { displayed = false } } + ) ) ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - {commandName="updateUser", statusCode="success"}, - {state_change=true, visibility={displayed=false}} + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) ) ) - ) - -- 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=false}} + -- 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 = false } } + ) ) ) - ) - end + 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() + "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"} - }, - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( + -- success + test.socket.capability:__queue_receive({ + mock_device.id, { - {userIndex=1, userName="TestUser1", userType="guest"}, - {userIndex=2, userName="TestUser2", userType="guest"}, - {userIndex=4, userName="TestUser4", userType="guest"} + capability = capabilities.lockUsers.ID, + command = "deleteUser", + args = { "3" } }, - {visibility={displayed=false}} + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "TestUser1", userType = "guest" }, + { userIndex = 2, userName = "TestUser2", userType = "guest" }, + { userIndex = 4, userName = "TestUser4", userType = "guest" } + }, + { visibility = { displayed = false } } + ) ) ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - {commandName="deleteUser", statusCode="success"}, - {state_change=true, visibility={displayed=false}} + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) ) ) - ) - -- 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=false}} + -- 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 = false } } + ) ) ) - ) - end + end ) test.register_coroutine_test( - "addCredential command received and commandResult is success", - function() - init_migration() - 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 = {"2", "guest", "pin", "abc123"} - }, - }) - test.socket.zigbee:__expect_send( - { - mock_device.id, - DoorLock.server.commands.SetPINCode(mock_device, - 1, - DoorLockUserStatus.OCCUPIED_ENABLED, - DoorLockUserType.UNRESTRICTED, - "abc123" + "addCredential command received and commandResult is success", + function() + init_migration() + 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 = { "2", "guest", "pin", "abc123" } + }, + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "abc123" + ) + } ) - } - ) - - 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.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.lockCredentials.credentials( + test.socket.zigbee:__queue_receive( { - {userIndex = 2, credentialIndex = 1, credentialType = "pin"} - }, - {visibility={displayed=false}} + 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.lockCredentials.credentials( + { + { userIndex = 2, credentialIndex = 1, credentialType = "pin" } + }, + { visibility = { displayed = false } } + ) ) ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - {commandName="addCredential", statusCode="success"}, - {state_change=true, visibility={displayed=false}} + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) ) ) - ) - end + end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 7138613650..e76fa5cafb 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -55,8 +55,8 @@ local reload_all_codes = function(device) if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCodes.pinUsersSupported.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, 1) + 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))) @@ -85,14 +85,14 @@ local add_user_handler = function(driver, device, command) commandName = lock_utils.ADD_USER, statusCode = status } - + if status == lock_utils.STATUS_SUCCESS then local current_users = lock_utils.get_users(device) - device:emit_event(capabilities.lockUsers.users(current_users, {visibility = {displayed = false}})) + device:emit_event(capabilities.lockUsers.users(current_users, { visibility = { displayed = false } })) end device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} + command_result_info, { state_change = true, visibility = { displayed = false } } )) end @@ -108,21 +108,20 @@ local update_user_handler = function(driver, device, command) } for _, user in ipairs(current_users) do - if user.userIndex == user_index then - -- update user info and the commandResult - user.userName = user_name - user.userType = user_type - device:set_field(lock_utils.LOCK_USERS, current_users) - device:emit_event(capabilities.lockUsers.users(current_users, {visibility = {displayed = false}})) - command_result_info.statusCode = lock_utils.STATUS_SUCCESS - break - end + if user.userIndex == user_index then + -- update user info and the commandResult + user.userName = user_name + user.userType = user_type + device:set_field(lock_utils.LOCK_USERS, current_users) + device:emit_event(capabilities.lockUsers.users(current_users, { visibility = { displayed = false } })) + command_result_info.statusCode = lock_utils.STATUS_SUCCESS + break + end end device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} + command_result_info, { state_change = true, visibility = { displayed = false } } )) - end local delete_user_handler = function(driver, device, command) @@ -135,11 +134,11 @@ local delete_user_handler = function(driver, device, command) if status_code == lock_utils.STATUS_SUCCESS then local current_users = lock_utils.get_users(device) - device:emit_event(capabilities.lockUsers.users(current_users, {visibility = {displayed = false}})) + device:emit_event(capabilities.lockUsers.users(current_users, { visibility = { displayed = false } })) end device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} + command_result_info, { state_change = true, visibility = { displayed = false } } )) end @@ -152,7 +151,7 @@ local delete_all_users_handler = function(driver, device, command) current_users = {} -- clear the table device:set_field(lock_utils.LOCK_USERS, current_users) device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} + command_result_info, { state_change = true, visibility = { displayed = false } } )) end @@ -164,11 +163,13 @@ local add_credential_handler = function(driver, device, command) local credential_data = command.args.credentialData local credential_index = nil local status_code = lock_utils.STATUS_SUCCESS - local max_credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME, 0) + local max_credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, + capabilities.lockCredentials.pinUsersSupported.NAME, 0) credential_index = lock_utils.get_available_index(lock_utils.get_credentials(device), max_credentials) if credential_index ~= nil then - device:set_field(lock_utils.PENDING_CREDENTIAL, {userIndex = user_index, userType = user_type, credentialType = credential_type}) + device:set_field(lock_utils.PENDING_CREDENTIAL, + { userIndex = user_index, userType = user_type, credentialType = credential_type }) else status_code = lock_utils.STATUS_RESOURCE_EXHAUSTED end @@ -176,10 +177,10 @@ local add_credential_handler = function(driver, device, command) -- 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:send(LockCluster.server.commands.SetPINCode(device, - credential_index, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - credential_data) + 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)) @@ -190,7 +191,7 @@ local add_credential_handler = function(driver, device, command) statusCode = status_code } device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} + command_result_info, { state_change = true, visibility = { displayed = false } } )) device:set_field(lock_utils.COMMAND_NAME, nil) end @@ -198,7 +199,7 @@ end local update_credential_handler = function(driver, device, command) device:set_field(lock_utils.COMMAND_NAME, lock_utils.UPDATE_CREDENTIAL) - + local credential_index = command.args.credentialIndex local credential_data = command.args.credentialData local command_result_info = { @@ -207,19 +208,19 @@ local update_credential_handler = function(driver, device, command) } if lock_utils.get_credential(device, credential_index) ~= nil then device:send(LockCluster.server.commands.SetPINCode(device, - credential_index, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - credential_data) + credential_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + credential_data) ) command_result_info.statusCode = lock_utils.STATUS_SUCCESS - device.thread:call_with_delay(4, function () + device.thread:call_with_delay(4, function() device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) end) end device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} + command_result_info, { state_change = true, visibility = { displayed = false } } )) end @@ -237,26 +238,26 @@ local delete_all_credentials_handler = function(driver, device, command) local credentials = lock_utils.get_credentials(device) local delay = 0 for _, credential in ipairs(credentials) do - device.thread:call_with_delay(delay, function () - device:send(LockCluster.server.commands.ClearPINCode(device, credential.credentialIndex)) - end) - device.thread:call_with_delay(delay + 2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, credential.credentialIndex)) - end) - delay = delay + 2 + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.ClearPINCode(device, credential.credentialIndex)) + end) + device.thread:call_with_delay(delay + 2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, credential.credentialIndex)) + end) + delay = delay + 2 end end local max_code_length_handler = function(driver, device, value) - device:emit_event(LockCredentials.maxPinCodeLen(value.value, {visibility = {displayed = false}})) + device:emit_event(LockCredentials.maxPinCodeLen(value.value, { visibility = { displayed = false } })) end local min_code_length_handler = function(driver, device, value) - device:emit_event(LockCredentials.minPinCodeLen(value.value, {visibility = {displayed = false}})) + device:emit_event(LockCredentials.minPinCodeLen(value.value, { visibility = { displayed = false } })) end local max_codes_handler = function(driver, device, value) - device:emit_event(LockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) + device:emit_event(LockCredentials.pinUsersSupported(value.value, { visibility = { displayed = false } })) end local get_pin_response_handler = function(driver, device, zb_mess) @@ -268,15 +269,15 @@ local get_pin_response_handler = function(driver, device, zb_mess) statusCode = lock_utils.STATUS_SUCCESS } - if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then if pending_credential ~= nil then -- create credential lock_utils.add_credential(device, pending_credential.userIndex, - pending_credential.userType, - pending_credential.credentialType, - credential_index) + pending_credential.userType, + pending_credential.credentialType, + credential_index) else - -- update credential + -- 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) @@ -284,13 +285,14 @@ local get_pin_response_handler = function(driver, device, zb_mess) end local credentials = lock_utils.get_credentials(device) - device:emit_event(capabilities.lockCredentials.credentials(credentials, {visibility = {displayed = false}})) + device:emit_event(capabilities.lockCredentials.credentials(credentials, { visibility = { displayed = false } })) else if lock_utils.get_credential(device, credential_index) ~= nil then -- Credential has been deleted. lock_utils.delete_credential(device, credential_index) - device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), {visibility = {displayed = false}})) - else + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { visibility = { displayed = false } })) + else -- unset? end end @@ -298,7 +300,8 @@ local get_pin_response_handler = function(driver, device, zb_mess) credential_index = tonumber(credential_index) if (credential_index == device:get_field(lock_utils.CHECKING_CODE)) then -- the credential we're checking has arrived - local last_slot = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) + local last_slot = 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) else @@ -310,7 +313,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) if command ~= nil then device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} + command_result_info, { state_change = true, visibility = { displayed = false } } )) device:set_field(lock_utils.COMMAND_NAME, nil) end @@ -348,30 +351,30 @@ local lock_operation_event_handler = function(driver, device, zb_rx) 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_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 code_name = "Code " .. code_id local lock_codes = device:get_field("lockCodes") if (lock_codes ~= nil and - lock_codes[code_id] ~= nil) then + lock_codes[code_id] ~= nil) then code_name = lock_codes[code_id] end - event.data = {method = METHOD[0], codeId = code_id .. "", codeName = code_name} + 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 + 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 @@ -384,61 +387,62 @@ local lock_operation_event_handler = function(driver, device, zb_rx) 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, - } + 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, } }, - 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, - }, - }, - sub_drivers = { - require("using-new-capabilities.samsungsds"), - require("using-new-capabilities.yale-fingerprint-lock"), - require("using-new-capabilities.yale"), - require("using-new-capabilities.lock-without-codes") + 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, }, - health_check = false, - lifecycle_handlers = { - doConfigure = do_configure + [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, }, - can_handle = function(opts, driver, device, ...) - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) - if lock_codes_migrated then - local subdriver = require("using-new-capabilities") - return true, subdriver - end - return false + }, + sub_drivers = { + require("using-new-capabilities.samsungsds"), + require("using-new-capabilities.yale-fingerprint-lock"), + require("using-new-capabilities.yale"), + require("using-new-capabilities.lock-without-codes") + }, + health_check = false, + lifecycle_handlers = { + doConfigure = do_configure + }, + can_handle = function(opts, driver, device, ...) + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + local subdriver = require("using-new-capabilities") + return true, subdriver end + return false + end } -return new_capabilities_driver \ No newline at end of file +return new_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua index 2d3ef0ec97..b42f3b0f14 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/samsungsds/init.lua @@ -110,11 +110,7 @@ local samsung_sds_driver = { init = device_init }, can_handle = function(opts, driver, device, ...) - if device:get_manufacturer() == "SAMSUNG SDS" and lock_codes_migrated then - local subdriver = require("using-new-capabilities.samsungsds") - return true, subdriver - end - return false + return device:get_manufacturer() == "SAMSUNG SDS" end } diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua index fe70163ef4..a235b469d7 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/init.lua @@ -62,7 +62,7 @@ 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)} + 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) @@ -105,7 +105,7 @@ local programming_event_handler = function(driver, device, zb_mess) 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"} + 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 @@ -121,12 +121,12 @@ local programming_event_handler = function(driver, device, zb_mess) 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 + 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} + event.data = { codeName = code_name } device:emit_event(event) if (change_type == " set") then local lock_codes = lock_utils.get_lock_codes(device) @@ -141,7 +141,7 @@ local handle_max_codes = function(driver, device, value) -- 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 + 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 @@ -192,30 +192,30 @@ local lock_operation_event_handler = function(driver, device, zb_rx) 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_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 code_name = "Code " .. code_id local lock_codes = device:get_field("lockCodes") if (lock_codes ~= nil and - lock_codes[code_id] ~= nil) then + lock_codes[code_id] ~= nil) then code_name = lock_codes[code_id] end - event.data = {method = METHOD[0], codeId = code_id .. "", codeName = code_name} + 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 + 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 @@ -235,16 +235,16 @@ local update_codes = function(driver, device, command) 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.thread:call_with_delay(delay, function() device:send(LockCluster.server.commands.SetPINCode(device, - code_slot, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - code)) + code_slot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + code)) end) delay = delay + 2 else - device.thread:call_with_delay(delay, function () + device.thread:call_with_delay(delay, function() device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) end) delay = delay + 2 @@ -274,20 +274,20 @@ local set_code = function(driver, device, command) driver:inject_capability_command(device, { capability = capabilities.lockCodes.ID, command = capabilities.lockCodes.commands.nameSlot.NAME, - args = {command.args.codeSlot, command.args.codeName} + 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) + 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 + codeState["setName" .. command.args.codeSlot] = command.args.codeName device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) end @@ -319,13 +319,15 @@ local migrate = function(driver, device, command) 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"}) + 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 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 @@ -336,7 +338,7 @@ local migrate = function(driver, device, command) 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(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 } })) @@ -367,57 +369,58 @@ local do_configure = function(self, device) 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, - } + 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 } }, - capability_handlers = { - [LockCodes.ID] = { - [LockCodes.commands.updateCodes.NAME] = update_codes, - [LockCodes.commands.deleteCode.NAME] = delete_code, - [LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, - [LockCodes.commands.requestCode.NAME] = request_code, - [LockCodes.commands.setCode.NAME] = set_code, - [LockCodes.commands.nameSlot.NAME] = name_slot, - [LockCodes.commands.migrate.NAME] = migrate, - }, - }, - sub_drivers = { - require("using-old-capabilities.samsungsds"), - require("using-old-capabilities.yale"), - require("using-old-capabilities.yale-fingerprint-lock"), - require("using-old-capabilities.lock-without-codes") - }, - health_check = false, - lifecycle_handlers = { - doConfigure = do_configure + 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, }, - can_handle = function(opts, driver, device, ...) - local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME, false) - if not lock_codes_migrated then - local subdriver = require("using-old-capabilities") - return true, subdriver - end - return false + }, + sub_drivers = { + require("using-old-capabilities.samsungsds"), + require("using-old-capabilities.yale"), + require("using-old-capabilities.yale-fingerprint-lock"), + require("using-old-capabilities.lock-without-codes") + }, + health_check = false, + lifecycle_handlers = { + doConfigure = do_configure + }, + can_handle = function(opts, driver, device, ...) + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if not lock_codes_migrated then + local subdriver = require("using-old-capabilities") + return true, subdriver end + return false + end } -return old_capabilities_driver \ No newline at end of file +return old_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua index 214a963e02..b529dd3fd1 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-old-capabilities/samsungsds/init.lua @@ -113,10 +113,7 @@ local samsung_sds_driver = { init = device_init }, can_handle = function(opts, driver, device, ...) - if device:get_manufacturer() == "SAMSUNG SDS" then - return true - end - return false + return device:get_manufacturer() == "SAMSUNG SDS" end } From ec5e138ca3961579b0c5c456478bce24618ea59d Mon Sep 17 00:00:00 2001 From: Pegor Date: Fri, 12 Dec 2025 14:01:13 -0800 Subject: [PATCH 08/11] add more tests and fix some logic --- .../zigbee-lock/src/new_lock_utils.lua | 20 +- .../test_zigbee_lock_new_capabilities.lua | 280 ++++++++++++++++-- .../src/using-new-capabilities/init.lua | 113 ++++--- 3 files changed, 348 insertions(+), 65 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua index 2d193be336..812b968d10 100644 --- a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -109,12 +109,22 @@ new_lock_utils.create_user = function(device, user_name, user_type, user_index) return status_code end -new_lock_utils.delete_user = function(device, user_index) +new_lock_utils.delete_user = function(device, user_index, deleted_by_credential_deletion) local current_users = new_lock_utils.get_users(device) local status_code = new_lock_utils.STATUS_FAILURE for index, user in ipairs(current_users) do if user.userIndex == user_index then + -- also delete associated credential if this isn't being call by a credential deletion. + if not deleted_by_credential_deletion then + -- find associated credential. + for _, credential in ipairs(new_lock_utils.get_credentials(device)) do + if credential.userIndex == user_index then + new_lock_utils.delete_credential(device, credential.credentialIndex, true) + break + end + end + end table.remove(current_users, index) device:set_field(new_lock_utils.LOCK_USERS, current_users) status_code = new_lock_utils.STATUS_SUCCESS @@ -142,12 +152,16 @@ new_lock_utils.add_credential = function(device, user_index, user_type, credenti return new_lock_utils.STATUS_SUCCESS end -new_lock_utils.delete_credential = function(device, credential_index) +new_lock_utils.delete_credential = function(device, credential_index, deleted_by_user_deletion) local credentials = new_lock_utils.get_credentials(device) local status_code = new_lock_utils.STATUS_FAILURE for index, credential in ipairs(credentials) do - if credential.userIndex == credential_index then + if credential.credentialIndex == credential_index then + -- also delete associated user if this isn't being called by a user deletion. + if not deleted_by_user_deletion then + new_lock_utils.delete_user(device, credential.userIndex, true) + end table.remove(credentials, index) device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) status_code = new_lock_utils.STATUS_SUCCESS 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 4948e6bf11..26fe09a04e 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 @@ -18,39 +18,25 @@ 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 DoorLockUserStatus = DoorLock.types.DrlkUserStatus local DoorLockUserType = DoorLock.types.DrlkUserType -local json = require "st.json" - -local mock_datastore = require "integration_test.mock_env_datastore" - +local test_credential_index = 1 +local test_credentials = {} local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("base-lock.yml"), } ) -local mock_device_with_users = test.mock_device.build_test_zigbee_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - data = { - lockCodes = json.encode({ - ["1"] = "Zach", - ["3"] = "Steven" - }), - } - } -) - zigbee_test_utils.prepare_zigbee_env_info() local function test_init_new_capabilities() + test_credential_index = 1 + test_credentials = {} test.mock_device.add_test_device(mock_device) end @@ -123,6 +109,67 @@ local function add_default_users() 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 = tonumber(user_index), credentialIndex = test_credential_index, credentialType = "pin" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials(test_credentials, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + 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( @@ -283,13 +330,43 @@ test.register_coroutine_test( "addCredential command received and commandResult is success", function() init_migration() - test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + add_credential("2", "abc123") + end +) + +test.register_coroutine_test( + "updateCredential command received and commandResult is success", + function() + init_migration() + add_credential("2", "abc123") + + -- try to update the wrong credentialIndex first and expect a failure + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { "2", "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 = false } } + ) + ) + ) + test.wait_for_events() + + -- try to update the right credential test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, - command = "addCredential", - args = { "2", "guest", "pin", "abc123" } + command = "updateCredential", + args = { "2", "1", "pin", "changedPin123" } }, }) test.socket.zigbee:__expect_send( @@ -299,11 +376,11 @@ test.register_coroutine_test( 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, - "abc123" + "changedPin123" ) } ) - + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") test.mock_time.advance_time(4) test.socket.zigbee:__expect_send( { @@ -312,8 +389,7 @@ test.register_coroutine_test( } ) test.wait_for_events() - - test.socket.zigbee:__queue_receive( + test.socket.zigbee:__queue_receive( { mock_device.id, DoorLock.client.commands.GetPINCodeResponse.build_test_rx( @@ -325,7 +401,6 @@ test.register_coroutine_test( ) } ) - test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -337,16 +412,167 @@ test.register_coroutine_test( ) ) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential command received and commandResult is success", + function() + init_migration() + add_credential("2", "abc123") + add_credential("1", "test123") + add_credential("3", "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 = false } } + ) + ) + ) + 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.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" } + }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllCredentials command received and commandResult is success", + function() + init_migration() + add_credential("2", "abc123") + add_credential("1", "test123") + add_credential("3", "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.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" } + }, + { visibility = { displayed = false } } + ) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success" }, + { commandName = "deleteAllCredentials", statusCode = "success" }, { state_change = true, visibility = { displayed = false } } ) ) ) + test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index e76fa5cafb..bed4321a81 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -12,8 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. - - -- Zigbee Driver utilities local defaults = require "st.zigbee.defaults" local device_management = require "st.zigbee.device_management" @@ -49,10 +47,10 @@ local reload_all_codes = function(device) 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.lockCodes.minPinCodeLen.NAME) == nil) then + 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.lockCodes.pinUsersSupported.NAME) == nil) then + 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_field(lock_utils.CHECKING_CODE) == nil) then @@ -126,7 +124,7 @@ end local delete_user_handler = function(driver, device, command) local user_index = tonumber(command.args.userIndex) - local status_code = lock_utils.delete_user(device, user_index) + local status_code = lock_utils.delete_user(device, user_index, false) local command_result_info = { commandName = lock_utils.DELETE_USER, statusCode = status_code @@ -199,13 +197,9 @@ end local update_credential_handler = function(driver, device, command) device:set_field(lock_utils.COMMAND_NAME, lock_utils.UPDATE_CREDENTIAL) - - local credential_index = command.args.credentialIndex + local credential_index = tonumber(command.args.credentialIndex) local credential_data = command.args.credentialData - local command_result_info = { - commandName = lock_utils.UPDATE_CREDENTIAL, - statusCode = lock_utils.STATUS_FAILURE - } + if lock_utils.get_credential(device, credential_index) ~= nil then device:send(LockCluster.server.commands.SetPINCode(device, credential_index, @@ -213,24 +207,39 @@ local update_credential_handler = function(driver, device, command) UserTypeEnum.UNRESTRICTED, credential_data) ) - command_result_info.statusCode = lock_utils.STATUS_SUCCESS device.thread:call_with_delay(4, function() device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) end) + else + local command_result_info = { + commandName = lock_utils.UPDATE_CREDENTIAL, + statusCode = lock_utils.STATUS_FAILURE + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, { state_change = true, visibility = { displayed = false } } + )) end - - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, { state_change = true, visibility = { displayed = false } } - )) end local delete_credential_handler = function(driver, device, command) - device:set_field(lock_utils.COMMAND_NAME, lock_utils.DELETE_CREDENTIAL) - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - device:send(LockCluster.server.commands.ClearPINCode(device, command.args.credentialIndex)) - device.thread:call_with_delay(2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, command.args.credentialIndex)) - end) + local credential_index = tonumber(command.args.credentialIndex) + if lock_utils.get_credential(device, credential_index) ~= nil then + device:set_field(lock_utils.COMMAND_NAME, lock_utils.DELETE_CREDENTIAL) + 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 + local command_result_info = { + commandName = lock_utils.DELETE_CREDENTIAL, + statusCode = lock_utils.STATUS_FAILURE + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, { state_change = true, visibility = { displayed = false } } + )) + device:set_field(lock_utils.COMMAND_NAME, nil) + end end local delete_all_credentials_handler = function(driver, device, command) @@ -238,11 +247,12 @@ local delete_all_credentials_handler = function(driver, device, command) local credentials = lock_utils.get_credentials(device) local delay = 0 for _, credential in ipairs(credentials) do + local credential_index = tonumber(credential.credentialIndex) device.thread:call_with_delay(delay, function() - device:send(LockCluster.server.commands.ClearPINCode(device, credential.credentialIndex)) + 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.credentialIndex)) + device:send(LockCluster.server.commands.GetPINCode(device, credential_index)) end) delay = delay + 2 end @@ -261,7 +271,7 @@ local max_codes_handler = function(driver, device, value) end local get_pin_response_handler = function(driver, device, zb_mess) - local credential_index = zb_mess.body.zcl_body.user_id.value + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) local pending_credential = device:get_field(lock_utils.PENDING_CREDENTIAL) local command = device:get_field(lock_utils.COMMAND_NAME) local command_result_info = { @@ -270,13 +280,13 @@ local get_pin_response_handler = function(driver, device, zb_mess) } if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then - if pending_credential ~= nil then + if command == lock_utils.ADD_CREDENTIAL then -- create credential lock_utils.add_credential(device, pending_credential.userIndex, pending_credential.userType, pending_credential.credentialType, credential_index) - else + elseif command == lock_utils.UPDATE_CREDENTIAL then -- update credential local credential = lock_utils.get_credential(device, credential_index) if credential ~= nil then @@ -289,7 +299,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) else if lock_utils.get_credential(device, credential_index) ~= nil then -- Credential has been deleted. - lock_utils.delete_credential(device, credential_index) + lock_utils.delete_credential(device, credential_index, false) device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), { visibility = { displayed = false } })) else @@ -297,7 +307,6 @@ local get_pin_response_handler = function(driver, device, zb_mess) end end - credential_index = tonumber(credential_index) 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, @@ -320,7 +329,41 @@ local get_pin_response_handler = function(driver, device, zb_mess) end local programming_event_handler = function(driver, device, zb_mess) - -- TODO + local credential_index = tostring(zb_mess.body.zcl_body.user_id.value) + if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then + -- Master code changed + -- todo? + 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, false) + end + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { visibility = { displayed = false } })) + else + -- One credential deleted + if (lock_utils.get_credential(credential_index) ~= nil) then + lock_utils.delete_credential(device, credential_index, false) + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { visibility = { displayed = false } })) + 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 then + local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, + capabilities.lockUsers.totalUsersSupported.NAME, 0) + + local user_index = lock_utils.get_available_index(lock_utils.get_users, max_users) + lock_utils.add_credential(device, user_index, + "guest", + lock_utils.CREDENTIAL_TYPE, + credential_index) + device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), + { visibility = { displayed = false } })) + end + end end local lock_operation_event_handler = function(driver, device, zb_rx) @@ -358,14 +401,14 @@ local lock_operation_event_handler = function(driver, device, zb_rx) else event.data.method = METHOD[source] end - if (source == 0 and device:supports_capability_by_id(capabilities.lockCodes.ID)) then --keypad + 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 lock_codes = device:get_field("lockCodes") - if (lock_codes ~= nil and - lock_codes[code_id] ~= nil) then - code_name = lock_codes[code_id] + local user = device: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 From e1dc5478ce9cc50be919e90f39deb3043ce0a0ce Mon Sep 17 00:00:00 2001 From: Pegor Date: Fri, 12 Dec 2025 14:39:37 -0800 Subject: [PATCH 09/11] user persist store for users --- .../zigbee-lock/src/new_lock_utils.lua | 23 +++++++++---- .../test_zigbee_lock_new_capabilities.lua | 32 ++++++++----------- .../src/using-new-capabilities/init.lua | 10 +++--- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua index 812b968d10..c1ebb4ec60 100644 --- a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -65,7 +65,16 @@ new_lock_utils.get_credentials = function(device) return credentials ~= nil and credentials or {} end -new_lock_utils.get_available_index = function(current_data, max) +new_lock_utils.get_available_user_index = function(current_data, max) + for index = 1, max do + if current_data["user"..index] == nil then + return index + end + end + return nil +end + +new_lock_utils.get_available_credential_index = function(current_data, max) local available_index = nil local used_index = {} @@ -92,7 +101,7 @@ new_lock_utils.create_user = function(device, user_name, user_type, user_index) local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME, 0) local current_users = new_lock_utils.get_users(device) - local available_index = new_lock_utils.get_available_index(current_users, max_users) + local available_index = new_lock_utils.get_available_user_index(current_users, max_users) if max_users == 0 or available_index == nil then -- Can't add any users - update commandResult statusCode @@ -102,8 +111,9 @@ new_lock_utils.create_user = function(device, user_name, user_type, user_index) if user_index ~= nil then available_index = user_index end - table.insert(current_users, { userIndex = available_index, userType = user_type, userName = user_name }) - device:set_field(new_lock_utils.LOCK_USERS, current_users) + + current_users["user"..available_index] = { userIndex = available_index, userType = user_type, userName = user_name } + device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) end return status_code @@ -113,7 +123,7 @@ new_lock_utils.delete_user = function(device, user_index, deleted_by_credential_ local current_users = new_lock_utils.get_users(device) local status_code = new_lock_utils.STATUS_FAILURE - for index, user in ipairs(current_users) do + for index, user in pairs(current_users) do if user.userIndex == user_index then -- also delete associated credential if this isn't being call by a credential deletion. if not deleted_by_credential_deletion then @@ -125,7 +135,8 @@ new_lock_utils.delete_user = function(device, user_index, deleted_by_credential_ end end end - table.remove(current_users, index) + -- table.remove(current_users, index) + current_users[index] = nil device:set_field(new_lock_utils.LOCK_USERS, current_users) status_code = new_lock_utils.STATUS_SUCCESS break 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 26fe09a04e..f01e497565 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 @@ -86,7 +86,7 @@ local function add_default_users() }, }) -- add to the user list that is now expected - table.insert(user_list, { userIndex = i, userType = "guest", userName = "TestUser" .. i }) + user_list["user"..i] = {userIndex = i, userType = "guest", userName = "TestUser" .. i } test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -219,18 +219,15 @@ test.register_coroutine_test( }, }) + local users = {} + users["user" .. 1] = { userIndex = 1, userName = "TestUser1", userType = "guest" } + users["user" .. 2] = { userIndex = 2, userName = "ChangeUserName", userType = "guest" } + users["user" .. 3] = { userIndex = 3, userName = "TestUser3", userType = "guest" } + users["user" .. 4] = { userIndex = 4, userName = "TestUser4", userType = "guest" } test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users( - { - { userIndex = 1, userName = "TestUser1", userType = "guest" }, - { userIndex = 2, userName = "ChangeUserName", userType = "guest" }, - { userIndex = 3, userName = "TestUser3", userType = "guest" }, - { userIndex = 4, userName = "TestUser4", userType = "guest" } - }, - { visibility = { displayed = false } } - ) + capabilities.lockUsers.users(users, { visibility = { displayed = false } }) ) ) test.socket.capability:__expect_send( @@ -281,17 +278,16 @@ test.register_coroutine_test( args = { "3" } }, }) + + local users = {} + users["user" .. 1] = { userIndex = 1, userName = "TestUser1", userType = "guest" } + users["user" .. 2] = { userIndex = 2, userName = "TestUser2", userType = "guest" } + users["user" .. 4] = { userIndex = 4, userName = "TestUser4", userType = "guest" } + test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users( - { - { userIndex = 1, userName = "TestUser1", userType = "guest" }, - { userIndex = 2, userName = "TestUser2", userType = "guest" }, - { userIndex = 4, userName = "TestUser4", userType = "guest" } - }, - { visibility = { displayed = false } } - ) + capabilities.lockUsers.users(users, { visibility = { displayed = false } }) ) ) test.socket.capability:__expect_send( diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index bed4321a81..50b9094ce6 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -105,12 +105,12 @@ local update_user_handler = function(driver, device, command) statusCode = lock_utils.STATUS_FAILURE } - for _, user in ipairs(current_users) do + for _, user in pairs(current_users) do if user.userIndex == user_index then -- update user info and the commandResult user.userName = user_name user.userType = user_type - device:set_field(lock_utils.LOCK_USERS, current_users) + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) device:emit_event(capabilities.lockUsers.users(current_users, { visibility = { displayed = false } })) command_result_info.statusCode = lock_utils.STATUS_SUCCESS break @@ -147,7 +147,7 @@ local delete_all_users_handler = function(driver, device, command) statusCode = lock_utils.STATUS_SUCCESS } current_users = {} -- clear the table - device:set_field(lock_utils.LOCK_USERS, current_users) + device:set_field(lock_utils.LOCK_USERS, current_users, { persist = true }) device:emit_event(capabilities.lockUsers.commandResult( command_result_info, { state_change = true, visibility = { displayed = false } } )) @@ -164,7 +164,7 @@ local add_credential_handler = function(driver, device, command) local max_credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME, 0) - credential_index = lock_utils.get_available_index(lock_utils.get_credentials(device), max_credentials) + credential_index = lock_utils.get_available_credential_index(lock_utils.get_credentials(device), max_credentials) if credential_index ~= nil then device:set_field(lock_utils.PENDING_CREDENTIAL, { userIndex = user_index, userType = user_type, credentialType = credential_type }) @@ -355,7 +355,7 @@ local programming_event_handler = function(driver, device, zb_mess) local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME, 0) - local user_index = lock_utils.get_available_index(lock_utils.get_users, max_users) + local user_index = lock_utils.get_available_user_index(lock_utils.get_users, max_users) lock_utils.add_credential(device, user_index, "guest", lock_utils.CREDENTIAL_TYPE, From 64c2bdf2cdbd0d839ea7710360f9da6674a16bcd Mon Sep 17 00:00:00 2001 From: Pegor Date: Tue, 16 Dec 2025 12:17:29 -0800 Subject: [PATCH 10/11] Fixup --- .../zigbee-lock/src/new_lock_utils.lua | 35 +++++++++++-------- .../src/using-new-capabilities/init.lua | 27 +++++++------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua index c1ebb4ec60..e02487be90 100644 --- a/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua @@ -24,6 +24,7 @@ local new_lock_utils = { ADD_USER = "addUser", COMMAND_NAME = "commandName", CREDENTIAL_TYPE = "pin", + CHECKING_CODE = "checkingCode", DELETE_ALL_CREDENTIALS = "deleteAllCredentials", DELETE_ALL_USERS = "deleteAllUsers", DELETE_CREDENTIAL = "deleteCredential", @@ -60,15 +61,29 @@ new_lock_utils.get_user = function(device, user_index) return nil end +new_lock_utils.get_available_user_index = function(current_data, max) + if current_data == nil and max ~= 0 then + return INITIAL_INDEX + elseif current_data ~= nil then + for index = 1, max do + if current_data["user" .. index] == nil then + return index + end + end + end + + return nil +end + new_lock_utils.get_credentials = function(device) local credentials = device:get_field(new_lock_utils.LOCK_CREDENTIALS) return credentials ~= nil and credentials or {} end -new_lock_utils.get_available_user_index = function(current_data, max) - for index = 1, max do - if current_data["user"..index] == nil then - return index +new_lock_utils.get_credential = function(device, credential_index) + for _, credential in ipairs(new_lock_utils.get_credentials(device)) do + if credential.credentialIndex == credential_index then + return credential end end return nil @@ -111,7 +126,6 @@ new_lock_utils.create_user = function(device, user_name, user_type, user_index) if user_index ~= nil then available_index = user_index end - current_users["user"..available_index] = { userIndex = available_index, userType = user_type, userName = user_name } device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) end @@ -167,7 +181,7 @@ new_lock_utils.delete_credential = function(device, credential_index, deleted_by local credentials = new_lock_utils.get_credentials(device) local status_code = new_lock_utils.STATUS_FAILURE - for index, credential in ipairs(credentials) do + for index, credential in pairs(credentials) do if credential.credentialIndex == credential_index then -- also delete associated user if this isn't being called by a user deletion. if not deleted_by_user_deletion then @@ -183,15 +197,6 @@ new_lock_utils.delete_credential = function(device, credential_index, deleted_by return status_code end -new_lock_utils.get_credential = function(device, credential_index) - for _, credential in ipairs(new_lock_utils.get_credentials(device)) do - if credential.credentialIndex == credential_index then - return credential - end - end - return nil -end - new_lock_utils.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 diff --git a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua index 50b9094ce6..17eeab86ee 100644 --- a/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/using-new-capabilities/init.lua @@ -60,7 +60,7 @@ local reload_all_codes = function(device) device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) end -local do_configure = function(self, device) +local init = 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)) @@ -70,9 +70,9 @@ 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)) - device.thread:call_with_delay(2, function(d) - reload_all_codes(device) - end) + -- device.thread:call_with_delay(2, function(d) + -- reload_all_codes(device) + -- end) end local add_user_handler = function(driver, device, command) @@ -292,6 +292,8 @@ local get_pin_response_handler = function(driver, device, zb_mess) if credential ~= nil then lock_utils.update_credential(device, credential.credentialIndex, credential.userIndex, credential.credentialType) end + else + -- something end local credentials = lock_utils.get_credentials(device) @@ -303,7 +305,7 @@ local get_pin_response_handler = function(driver, device, zb_mess) device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), { visibility = { displayed = false } })) else - -- unset? + -- unset end end @@ -329,12 +331,13 @@ local get_pin_response_handler = function(driver, device, zb_mess) end local programming_event_handler = function(driver, device, zb_mess) - local credential_index = tostring(zb_mess.body.zcl_body.user_id.value) + local credential_index = tonumber(zb_mess.body.zcl_body.user_id.value) if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then -- Master code changed -- todo? + -- event updateCredential 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 + 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, false) @@ -343,7 +346,7 @@ local programming_event_handler = function(driver, device, zb_mess) { visibility = { displayed = false } })) else -- One credential deleted - if (lock_utils.get_credential(credential_index) ~= nil) then + if (lock_utils.get_credential(device, credential_index) ~= nil) then lock_utils.delete_credential(device, credential_index, false) device:emit_event(capabilities.lockCredentials.credentials(lock_utils.get_credentials(device), { visibility = { displayed = false } })) @@ -355,7 +358,7 @@ local programming_event_handler = function(driver, device, zb_mess) local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME, 0) - local user_index = lock_utils.get_available_user_index(lock_utils.get_users, max_users) + local user_index = lock_utils.get_available_user_index(lock_utils.get_users(device), max_users) lock_utils.add_credential(device, user_index, "guest", lock_utils.CREDENTIAL_TYPE, @@ -404,7 +407,7 @@ local lock_operation_event_handler = function(driver, device, zb_rx) 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 = device:get_user(device, code_id) + local user = lock_utils.get_user(device, code_id) if user ~= nil then code_name = user.userName end @@ -470,12 +473,12 @@ local new_capabilities_driver = { sub_drivers = { require("using-new-capabilities.samsungsds"), require("using-new-capabilities.yale-fingerprint-lock"), - require("using-new-capabilities.yale"), + -- require("using-new-capabilities.yale"), require("using-new-capabilities.lock-without-codes") }, health_check = false, lifecycle_handlers = { - doConfigure = do_configure + init = init }, can_handle = function(opts, driver, device, ...) local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, From 9824628fb8ac12ceea801a7ba705d65569467ff2 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Sun, 21 Dec 2025 17:22:25 -0800 Subject: [PATCH 11/11] SLGA: Z-Wave lock update --- .../zwave-lock/src/apiv6_bugfix/init.lua | 26 - drivers/SmartThings/zwave-lock/src/init.lua | 189 +--- .../zwave-lock/src/test/test_zwave_lock.lua | 10 +- .../test/test_zwave_lock_code_migration.lua | 246 ----- .../test_zwave_lock_code_slga_migration.lua | 97 +- .../test/test_zwave_lock_new_capabilities.lua | 982 ++++++++++++++++++ .../src/using-new-capabilities/init.lua | 532 ++++++++++ .../keywe-lock/init.lua | 86 ++ .../using-new-capabilities/new_lock_utils.lua | 227 ++++ .../samsung-lock/init.lua | 111 ++ .../schlage-lock/init.lua | 193 ++++ .../zwave-alarm-v1-lock/init.lua | 165 +++ .../src/using-old-capabilities/can_handle.lua | 10 + .../src/using-old-capabilities/init.lua | 175 ++++ .../keywe-lock/init.lua | 86 ++ .../samsung-lock/init.lua | 111 ++ .../schlage-lock/init.lua | 193 ++++ .../zwave-alarm-v1-lock/init.lua | 165 +++ 18 files changed, 3097 insertions(+), 507 deletions(-) delete mode 100644 drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua deleted file mode 100644 index 0204b7b2d5..0000000000 --- a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,26 +0,0 @@ -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = can_handle -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 2710ee7a59..653f99e3f3 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -19,176 +19,28 @@ local cc = require "st.zwave.CommandClass" local ZwaveDriver = require "st.zwave.driver" --- @type st.zwave.defaults local defaults = require "st.zwave.defaults" ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) ---- @type st.zwave.CommandClass.UserCode -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) ---- @type st.zwave.CommandClass.Battery -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.CommandClass.Time -local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) -local constants = require "st.zwave.constants" -local utils = require "st.utils" -local json = require "st.json" -local SCAN_CODES_CHECK_INTERVAL = 30 -local MIGRATION_COMPLETE = "migrationComplete" -local MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped" - -local function periodic_codes_state_verification(driver, device) - local scan_codes_state = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.scanCodes.NAME) - if scan_codes_state == "Scanning" then - driver:inject_capability_command(device, - { capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - } - ) - device.thread:call_with_delay( - SCAN_CODES_CHECK_INTERVAL, - function(d) - periodic_codes_state_verification(driver, device) - end - ) - end -end - -local function populate_state_from_data(device) - if device.data.lockCodes ~= nil and device:get_field(MIGRATION_COMPLETE) ~= true then - -- build the lockCodes table - local lockCodes = {} - local lc_data = json.decode(device.data.lockCodes) - for k, v in pairs(lc_data) do - lockCodes[k] = v - end - -- Populate the devices `lockCodes` field - device:set_field(constants.LOCK_CODES, utils.deep_copy(lockCodes), { persist = true }) - -- Populate the devices state history cache - device.state_cache["main"] = device.state_cache["main"] or {} - device.state_cache["main"][capabilities.lockCodes.ID] = device.state_cache["main"][capabilities.lockCodes.ID] or {} - device.state_cache["main"][capabilities.lockCodes.ID][capabilities.lockCodes.lockCodes.NAME] = {value = json.encode(utils.deep_copy(lockCodes))} - - device:set_field(MIGRATION_COMPLETE, true, { persist = true }) - end -end - ---- Builds up initial state for the device ---- ---- @param self st.zwave.Driver ---- @param device st.zwave.Device -local function added_handler(self, device) - populate_state_from_data(device) - if device.data.lockCodes == nil or device:get_field(MIGRATION_RELOAD_SKIPPED) == true then - if (device:supports_capability(capabilities.lockCodes)) then - self:inject_capability_command(device, - { capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} }) - device.thread:call_with_delay( - SCAN_CODES_CHECK_INTERVAL, - function(d) - periodic_codes_state_verification(self, device) - end - ) - end +local lazy_load_if_possible = function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) else - device:set_field(MIGRATION_RELOAD_SKIPPED, true, { persist = true }) - end - device:send(DoorLock:OperationGet({})) - device:send(Battery:Get({})) - if (device:supports_capability(capabilities.tamperAlert)) then - device:emit_event(capabilities.tamperAlert.tamper.clear()) + return require(sub_driver_name) end end -local init_handler = function(driver, device, event) - populate_state_from_data(device) - -- temp fix before this can be changed from being persisted in memory - device:set_field(constants.CODE_STATE, nil, { persist = true }) -end - local do_refresh = function(self, device) + local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) device:send(DoorLock:OperationGet({})) device:send(Battery:Get({})) end ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @param cmd table -local function update_codes(driver, device, cmd) - local delay = 0 - -- args.codes is json - for name, code in pairs(cmd.args.codes) do - -- these seem to come in the format "code[slot#]: code" - local code_slot = tonumber(string.gsub(name, "code", ""), 10) - if (code_slot ~= nil) then - if (code ~= nil and (code ~= "0" and code ~= "")) then - -- code changed - device.thread:call_with_delay(delay, function () - device:send(UserCode:Set({ - user_identifier = code_slot, - user_code = code, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) - end) - delay = delay + 2.2 - else - -- code deleted - device.thread:call_with_delay(delay, function () - device:send(UserCode:Set({user_identifier = code_slot, user_id_status = UserCode.user_id_status.AVAILABLE})) - end) - delay = delay + 2.2 - device.thread:call_with_delay(delay, function () - device:send(UserCode:Get({user_identifier = code_slot})) - end) - delay = delay + 2.2 - end - end - end -end - ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @param cmd table -local function migrate(driver, device, cmd) - local lock_users = {} - local lock_credentials = {} - local lc_data = json.decode(device.data.lockCodes) - local lock_codes = {} - local ordered_codes = {} - for k, v in pairs(lc_data) do - lock_codes[k] = v - end - - for code in pairs(lock_codes) do - table.insert(ordered_codes, code) - end - - table.sort(ordered_codes) - for index = 1, #ordered_codes do - local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ] - table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name}) - table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) - end - - local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 4) - local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 10) - local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - if (code_length ~= nil) then - max_code_len = code_length - min_code_len = code_length - end - - device:emit_event(capabilities.lockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) -end - local function time_get_handler(driver, device, cmd) + local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) local time = os.date("*t") device:send_to_component( Time:Report({ @@ -209,32 +61,19 @@ local driver_template = { capabilities.battery, capabilities.tamperAlert }, - lifecycle_handlers = { - added = added_handler, - init = init_handler, - }, capability_handlers = { - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes - }, - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.migrate.NAME] = migrate - }, [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, zwave_handlers = { [cc.TIME] = { - [Time.GET] = time_get_handler -- used by DanaLock + [0x01] = time_get_handler -- used by DanaLock } }, sub_drivers = { - require("zwave-alarm-v1-lock"), - require("schlage-lock"), - require("samsung-lock"), - require("keywe-lock"), - require("apiv6_bugfix"), + require("using-new-capabilities"), + require("using-old-capabilities"), } } diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index 52144295b3..3ee38c115b 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -43,10 +43,10 @@ local zwave_lock_endpoints = { } local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints - } + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints + } ) local function test_init() @@ -459,7 +459,7 @@ test.register_coroutine_test( Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, - event_parameter = "" + event_parameter = "\x01" }) } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua deleted file mode 100644 index e4a9b50758..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua +++ /dev/null @@ -1,246 +0,0 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- Mock out globals -local test = require "integration_test" -local capabilities = require "st.capabilities" -local zw = require "st.zwave" ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) ---- @type st.zwave.CommandClass.Battery -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.CommandClass.UserCode -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) -local t_utils = require "integration_test.utils" -local zw_test_utils = require "integration_test.zwave_test_utils" -local utils = require "st.utils" - -local mock_datastore = require "integration_test.mock_env_datastore" - -local json = require "dkjson" - -local zwave_lock_endpoints = { - { - command_classes = { - { value = zw.BATTERY }, - { value = zw.DOOR_LOCK }, - { value = zw.USER_CODE }, - { value = zw.NOTIFICATION } - } - } -} - -local lockCodes = { - ["1"] = "Zach", - ["2"] = "Steven" -} - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints, - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } - } -) - -local mock_device_no_data = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - data = {} - } -) - -local expect_reload_all_codes_messages = function(dev, lc) - test.socket.capability:__expect_send(dev:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode(lc), { visibility = { displayed = false } }) - )) - test.socket.zwave:__expect_send( UserCode:UsersNumberGet({}):build_test_tx(dev.id) ) - test.socket.capability:__expect_send(dev:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) - test.socket.zwave:__expect_send( UserCode:Get({ user_identifier = 1 }):build_test_tx(dev.id) ) -end - -test.register_coroutine_test( - "Device added data lock codes population", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end -) - -test.register_coroutine_test( - "Device added without data should function", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - expect_reload_all_codes_messages(mock_device_no_data,{}) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device_no_data:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", nil) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode({})) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", nil) - end -) - -test.register_coroutine_test( - "Device init after added shouldn't change the datastores", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end -) - -test.register_coroutine_test( - "Device init after added with no data should update the datastores", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - -- This should happen as the data is empty at this point - expect_reload_all_codes_messages(mock_device_no_data, {}) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device_no_data:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", nil) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode({})) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", nil) - test.socket.device_lifecycle():__queue_receive(mock_device_no_data:generate_info_changed( - { - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } - } - )) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", true) - end -) - - -test.register_coroutine_test( - "Device added data lock codes population, should not reload all codes", - function() - test.timer.__create_and_queue_test_time_advance_timer(31, "oneshot") - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.wait_for_events() - test.mock_time.advance_time(35) - -- Nothing should happen - end -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua index 17629c1912..d446289fc0 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_slga_migration.lua @@ -26,8 +26,9 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version local t_utils = require "integration_test.utils" local zw_test_utils = require "integration_test.zwave_test_utils" local utils = require "st.utils" -local mock_datastore = require "integration_test.mock_env_datastore" local json = require "dkjson" +--- @type st.zwave.constants +local constants = require "st.zwave.constants" local SCHLAGE_MANUFACTURER_ID = 0x003B local SCHLAGE_PRODUCT_TYPE = 0x0002 @@ -44,19 +45,11 @@ local zwave_lock_endpoints = { } } -local lockCodes = { - ["1"] = "Zach", - ["5"] = "Steven" -} - local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints, - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } - } + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints, + } ) local schlage_mock_device = test.mock_device.build_test_zwave_device( @@ -66,37 +59,31 @@ local schlage_mock_device = test.mock_device.build_test_zwave_device( zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, zwave_product_id = SCHLAGE_PRODUCT_ID, - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } } ) local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} +local function init_code_slot(slot_number, name, device) + local lock_codes = device.persistent_store[constants.LOCK_CODES] + if lock_codes == nil then + lock_codes = {} + device.persistent_store[constants.LOCK_CODES] = lock_codes + end + lock_codes[tostring(slot_number)] = name +end + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(schlage_mock_device) +end +test.set_test_init_function(test_init) + test.register_coroutine_test( "Device called 'migrate' command", function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" }) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + init_code_slot(1, "Zach", mock_device) + init_code_slot(5, "Steven", mock_device) -- setup codes test.socket.zwave:__queue_receive({mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) @@ -108,33 +95,32 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) end ) +test.register_coroutine_test( + "Migrate new device", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(8, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + end +) + test.register_coroutine_test( "Schlage-Lock device called 'migrate' command, validate codeLength is being properly set", function() - test.mock_device.add_test_device(schlage_mock_device) - test.socket.device_lifecycle:__queue_receive({ schlage_mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - schlage_mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - schlage_mock_device, - Battery:Get({}) - ) - ) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" }) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "migrationComplete", true) + init_code_slot(1, "Zach", schlage_mock_device) + init_code_slot(5, "Steven", schlage_mock_device) -- setup codes test.socket.zwave:__queue_receive({schlage_mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) test.socket.zwave:__queue_receive({schlage_mock_device.id, Configuration:Report({ parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, configuration_value = 6 })}) @@ -148,6 +134,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) end diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua new file mode 100644 index 0000000000..6edacbb690 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_new_capabilities.lua @@ -0,0 +1,982 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local json = require "dkjson" +--- @type st.zwave.constants +local constants = require "st.zwave.constants" +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.Alarm +local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) +local t_utils = require "integration_test.utils" +local zw_test_utils = require "integration_test.zwave_test_utils" + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = zw.BATTERY}, + {value = zw.DOOR_LOCK}, + {value = zw.USER_CODE}, + {value = zw.NOTIFICATION} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints + } +) + +-- start with a migrated blank device +local function test_init() + test.mock_device.add_test_device(mock_device) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(8, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(8, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) +end + +test.set_test_init_function(test_init) + + +test.register_coroutine_test( + "Add user should succeed", + function () + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 2", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Add credential should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 2, "guest", "pin", "3456" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + {userIndex = 1, userType = "guest", userName = "Code 1" }, + {userIndex = 2, userType = "guest", userName = "Code 2" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + user_code = "3456", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" } + }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Add credential for existing user should succeed", + function () + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Update user should succeed", + function () + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 1", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "addUser", + args = { "TestUser 2", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "TestUser 1" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = {1, "new name", "guest" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "new name" }, {userIndex = 2, userType = "guest", userName = "TestUser 2" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete user should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "deleteUser", + args = { 1 } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") + test.wait_for_events() + + test.mock_time.advance_time(4.2) + test.socket.zwave:__expect_send(UserCode:Get( {user_identifier = 1}):build_test_tx(mock_device.id)) + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Update credential should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "updateCredential", + args = { 1, 1, "pin", "3456" } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "3456", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete credential should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "deleteCredential", + args = { 1, "pin" } + }, + }) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end +) + +test.register_coroutine_test( + "Delete all users should succeed", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 2, "guest", "pin", "3456" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + {userIndex = 1, userType = "guest", userName = "Code 1" }, + {userIndex = 2, userType = "guest", userName = "Code 2" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + user_code = "3456", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" } + }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockUsers.ID, + command = "deleteAllUsers", + args = {} + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(0.5, "oneshot") + test.mock_time.advance_time(0) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.mock_time.advance_time(0.5) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 2, + user_id_status = UserCode.user_id_status.AVAILABLE + }):build_test_tx(mock_device.id) + ) + end +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code number", + function() + test.socket.capability:__queue_receive({mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = { 1, "guest", "pin", "1234" } + }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest", userName = "Code 1" }}, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive( + { + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, + event_parameter = "\x01" + }) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", userIndex = 1 } }) + ) + ) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua new file mode 100644 index 0000000000..fc171b493a --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/init.lua @@ -0,0 +1,532 @@ +local capabilities = require "st.capabilities" +local LockUsers = capabilities.lockUsers +local LockCredentials = capabilities.lockCredentials +local lock_utils = require "using-new-capabilities.new_lock_utils" +local utils = require "st.utils" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control + +-- Helper methods + +local add_or_update = function(device, method, credential_index, user_index) + -- if so, add the credential to the list + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + credentials[credential_index] = { userIndex = user_index, credentialIndex = credential_index, credentialType = "pin"} + -- emit credential event + device:emit_event(LockCredentials.credentials(credentials, { visibility = { displayed = false } })) + -- emit command success + device:emit_event(LockCredentials.commandResult( + { commandName = method, statusCode = lock_utils.STATUS_SUCCESS, credentialIndex = credential_index}, { state_change = true, visibility = { displayed = false } } + )) + -- set the ongoing operation field to nil + device:set_field(method..credential_index, nil) +end + +-- returns the index of the lowest unset index less than the max +local next_empty_index = function(table, max) + local index = 1 + for i = 1, max + 1 do + if table[i] == nil then + index = i + break + end + end + return index +end + +-- Lifecycle handlers +local added_handler = function(driver, device) + -- read user/credential metadata + -- reload all codes +end + +-- Lock Users commands + +local add_user_handler = function(driver, device, cmd) + local user_name = cmd.args.userName + local user_type = cmd.args.userType + -- get the table of current users + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + -- check that we can add a new user + local max_users = device:get_latest_state("main", LockUsers.ID, LockUsers.totalUsersSupported.NAME, 8) + if utils.table_size(users) == max_users then + -- we cannot create a new user (unlikely!) + end + -- find the index to add the user at + local index = next_empty_index(users, max_users) + -- insert the user into the table + users[index] = {userIndex = index, userName = user_name, userType = user_type} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) +end + +local update_user_handler = function(driver, device, cmd) + local index = cmd.args.userIndex + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + -- does the user index already exist? + -- if not, update the user (offset user index by 1) + if users[index] ~= nil then + -- insert the user into the table + users[index] = {userIndex = index, userName = cmd.args.userName, userType = cmd.args.userType} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.UPDATE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + end +end + +local delete_user_handler = function(driver, device, cmd) + local index = cmd.args.userIndex + -- make sure the user exists + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + if users[index] ~= nil then + -- see if the user is associated with a lock code + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + for _, credential in pairs(credentials) do + if credential.userIndex == index then + -- if so, delete that code + device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) + -- save state for receipt of delete + device:set_field("_delete_credential"..credential.credentialIndex, index) + -- make sure delete went through + device.thread:call_with_delay(4.2, function(d) device:send(UserCode:Get({user_identifier = credential.credentialIndex})) end) + return -- if the user has a credential, we need confirmation that the code was deleted before proceeding + end + end + -- delete user from the list + users[index] = nil + -- emit users event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit user delete success + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + end +end + +local delete_all_users_handler = function(driver, device, cmd) + -- TODO: Z-Wave User Code v2 includes mass sets/gets that could be leveraged to make this simpler + -- delete every user + -- send users event + device:emit_event(LockUsers.users({}, { visibility = { displayed = false}})) + -- send success event + device:emit_event(LockUsers.commandResult({ commandName = lock_utils.DELETE_ALL_USERS, statusCode = lock_utils.STATUS_SUCCESS }, { state_change = true, visibility = { displayed = false}})) + + + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + -- delete every credential + local delay = 0 + for _, credential in pairs(credentials) do + device.thread:call_with_delay(delay, function(d) + device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) + end) + -- include a delay between deletes + delay = delay + .5 + end + -- send credentials event + device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false}})) + -- send success event (this would be tedious to check for every code, so assume they all went through) + device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { state_change = true, visibility = { displayed = false}})) +end + +--- Lock Credentials Commands + +local add_credential_handler = function(driver, device, cmd) + local index = cmd.args.userIndex + local user_type = cmd.args.userType + local credential_type = cmd.args.credentialType -- if this is not "pin", send an error + local data = cmd.args.credentialData + -- get the table of current credentials + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + -- does the user index already exist? + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + -- if not, create a new user (offset user index by 1) + if users[index] == nil then + -- insert the user into the table + users[index] = {userIndex = index, userName = "Code "..index, userType = user_type} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + end + -- find the index to add the credential at + local max_credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.pinUsersSupported.NAME, 8) + local credential_index = next_empty_index(credentials, max_credentials) + -- save some state so we can complete the transaction on message receipt + device:set_field(lock_utils.ADD_CREDENTIAL..credential_index, index) + -- send the credential creation message + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) +end + +local update_credential_handler = function(driver, device, cmd) + -- validate args + local user_index = cmd.args.userIndex + local credential_index = cmd.args.credentialIndex + local credential_type = cmd.args.credentialType + local data = cmd.args.credentialData + -- make sure credential already exists + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + if credentials[credential_index] ~= nil and users[user_index] ~= nil then + -- store state to track update + device:set_field(lock_utils.UPDATE_CREDENTIAL..credential_index, user_index) + -- send command to update code + device:send(UserCode:Set({ + user_identifier = credential_index, + user_code = data, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + else + -- failure + end +end + +local delete_credential_handler = function(driver, device, cmd) + -- find the user associated with this credential + local user_index = cmd.args.credentialIndex + -- run delete user with that credential + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteUser.NAME, + args = { user_index } + }) +end + +local delete_all_credentials_handler = function(driver, device, cmd) + -- check to see if we have users that do not have a code associated + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + local user_index_marked_for_individual_deletion = {} + + for _, credential in pairs(credentials) do + users[credential.userIndex] = nil + user_index_marked_for_individual_deletion[credential.userIndex] = true + end + -- if we don't, this is equivalent to delete_all_users + if utils.table_size(users) == 0 then + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteAllUsers.NAME, + args = { } + }) + return + end + -- if we do, delete all users other than those + for i, _ in pairs(user_index_marked_for_individual_deletion) do + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = i }, { state_change = true, visibility = { displayed = false } } + )) + end + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- and then delete all codes + local delay = 0 + for _, credential in pairs(credentials) do + device.thread.call_with_delay(delay, function(d) + device:send(UserCode:Set({user_identifier = credential.credentialIndex, user_id_status = UserCode.user_id_status.AVAILABLE})) + end) + -- include a delay between deletes + delay = delay + .5 + end + -- send credentials event (these will be sent before all the actual deletes have been sent) + device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false}})) + -- send success event (this would be tedious to check for every code, so assume they all went through) + device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { visibility = { displayed = false}})) +end + + + +-- Z-Wave Message Handlers + +local user_code_report_handler = function(driver, device, cmd) + local code_id = cmd.args.user_identifier + local user_id_status = cmd.args.user_id_status + + -- is this a report about an occupied credential index? + if (user_id_status == UserCode.user_id_status.ENABLED_GRANT_ACCESS or + (user_id_status == UserCode.user_id_status.STATUS_NOT_AVAILABLE and cmd.args.user_code)) then + -- are we in the middle of a user code set for this index? + local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..code_id) + local user_index_update = device:get_field(lock_utils.UPDATE_CREDENTIAL..code_id) + if user_index_add ~= nil then + add_or_update(device, lock_utils.ADD_CREDENTIAL, code_id, user_index_add) + elseif user_index_update ~= nil then + add_or_update(device, lock_utils.UPDATE_CREDENTIAL, code_id, user_index_update) + end + elseif user_id_status == UserCode.user_id_status.AVAILABLE then + -- are we in the middle of a user code delete? + local user_index = device:get_field("_delete_credential"..code_id) + if user_index ~= nil then + -- if so, delete the credential + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + credentials[code_id] = nil + -- emit credential event + device:emit_event(LockCredentials.credentials(credentials, { visibility = { displayed = false } })) + -- emit command success + device:emit_event(LockCredentials.commandResult( + { commandName = lock_utils.DELETE_CREDENTIAL, statusCode = lock_utils.STATUS_SUCCESS, credentialIndex = code_id }, { state_change = true, visibility = { displayed = false } } + )) + -- delete the user + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + users[user_index] = nil + -- emit users event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit command success + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index }, { state_change = true, visibility = { displayed = false } } + )) + -- clear state + device:set_field("_delete_credential"..code_id, nil) + end + end +end + +local notification_report_handler = function(driver, device, cmd) + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event = cmd.args.event + local credentials = device:get_latest_state("main", LockCredentials.ID, LockCredentials.credentials.NAME, {}) + local users = device:get_latest_state("main", LockUsers.ID, LockUsers.users.NAME, {}) + if (event == access_control_event.ALL_USER_CODES_DELETED) then + -- this is unexpected, but we got this out of band, so... + -- check to see if we have users that do not have a code associated + local user_index_marked_for_individual_deletion = {} + for _, credential in pairs(credentials) do + users[credential.userIndex] = nil + user_index_marked_for_individual_deletion[credential.userIndex] = true + end + -- if we don't, this is equivalent to delete_all_users + if utils.table_size(users) == 0 then + driver:inject_capability_command(device, { + capability = capabilities.lockUsers.ID, + command = capabilities.lockUsers.commands.deleteAllUsers.NAME, + args = { } + }) + return + end + -- if we do, delete all users other than those + for i, _ in pairs(user_index_marked_for_individual_deletion) do + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = i }, { state_change = true, visibility = { displayed = false } } + )) + end + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit empty credentials + device:emit_event(LockCredentials.credentials({}, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.commandResult({ commandName = lock_utils.DELETE_ALL_CREDENTIALS, statusCode = lock_utils.STATUS_SUCCESS }, { visibility = { displayed = false}})) + elseif (event == access_control_event.SINGLE_USER_CODE_DELETED) then + local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) + -- find the user index assigned to this code to delete it as well + local credential = credentials[credential_index] + if credential ~= nil then + -- we may want to check if these match + local stored_user_index = device:get_field("_delete_credential"..credential_index) + local user_index = credential.userIndex + local user = users[user_index] + if user ~= nil then + users[user_index] = nil + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index }, { state_change = true, visibility = { displayed = false } } + )) + device:set_field("_delete_credential"..credential_index, nil) + end + else + -- something bad happened + end + elseif (event == access_control_event.NEW_USER_CODE_ADDED) then + local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) + -- determine if this is due to a command or an out-of-band update + local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..credential_index) + local user_index_update = device:get_field(lock_utils.UPDATE_CREDENTIAL..credential_index) + if user_index_add ~= nil then + add_or_update(device, lock_utils.ADD_CREDENTIAL, credential_index, user_index_add) + elseif user_index_update ~= nil then + add_or_update(device, lock_utils.UPDATE_CREDENTIAL, credential_index, user_index_update) + else + -- out-of-band update + -- create a user for this code index + local max_users = device:get_latest_state("main", LockUsers.ID, LockUsers.totalUsersSupported.NAME, 8) + -- find the index to add the user at + local index = next_empty_index(users, max_users) + -- insert the user into the table + users[index] = {userIndex = index, userName = "Code "..index, userType = "guest"} + -- emit the users table event + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + -- emit the command result event + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.ADD_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = index }, { state_change = true, visibility = { displayed = false } } + )) + -- add the credential + add_or_update(device, lock_utils.ADD_CREDENTIAL, credential_index, index) + end + elseif (event == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE) then + local credential_index = lock_utils.get_code_id_from_notification_event(cmd.args.event_parameter, cmd.args.v1_alarm_level) + -- this is a create code failure + -- double check we have a stored add command + local user_index_add = device:get_field(lock_utils.ADD_CREDENTIAL..credential_index) + -- clear that state + device:set_field(lock_utils.ADD_CREDENTIAL..credential_index, nil) + -- emit a credential add failure + device:emit_event(LockCredentials.commandResult( + { commandName = lock_utils.ADD_CREDENTIAL, statusCode = "duplicate", credentialIndex = credential_index }, { state_change = true, visibility = { displayed = false } } + )) + -- if we have a stored add command, we should delete the associated user, I think + if users[user_index_add] ~= nil then + users[user_index_add] = nil + device:emit_event(LockUsers.commandResult( + { commandName = lock_utils.DELETE_USER, statusCode = lock_utils.STATUS_SUCCESS, userIndex = user_index_add }, { state_change = true, visibility = { displayed = false } } + )) + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + end + elseif (event == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION) then + -- master code changed + -- we might want to fire a credential updated success here? + + + -- these are all the lock operation events + elseif (event >= access_control_event.MANUAL_LOCK_OPERATION and event <= access_control_event.LOCK_JAMMED) then + local event_to_send + + local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + COMMAND = "command", + AUTO = "auto" + } + + local DELAY_LOCK_EVENT = "_delay_lock_event" + local DELAY_LOCK_EVENT_TIMER = "_delay_lock_event_timer" + local MAX_DELAY = 10 + + if ((event >= access_control_event.MANUAL_LOCK_OPERATION and + event <= access_control_event.KEYPAD_UNLOCK_OPERATION) or + event == access_control_event.AUTO_LOCK_LOCKED_OPERATION) then + -- even event codes are unlocks, odd event codes are locks + local events = {[0] = capabilities.lock.lock.unlocked(), [1] = capabilities.lock.lock.locked()} + event_to_send = events[event & 1] + elseif (event >= access_control_event.MANUAL_NOT_FULLY_LOCKED_OPERATION and + event <= access_control_event.LOCK_JAMMED) then + event_to_send = capabilities.lock.lock.unknown() + end + + if (event_to_send ~= nil) then + local method_map = { + [access_control_event.MANUAL_UNLOCK_OPERATION] = METHOD.MANUAL, + [access_control_event.MANUAL_LOCK_OPERATION] = METHOD.MANUAL, + [access_control_event.MANUAL_NOT_FULLY_LOCKED_OPERATION] = METHOD.MANUAL, + [access_control_event.RF_LOCK_OPERATION] = METHOD.COMMAND, + [access_control_event.RF_UNLOCK_OPERATION] = METHOD.COMMAND, + [access_control_event.RF_NOT_FULLY_LOCKED_OPERATION] = METHOD.COMMAND, + [access_control_event.KEYPAD_LOCK_OPERATION] = METHOD.KEYPAD, + [access_control_event.KEYPAD_UNLOCK_OPERATION] = METHOD.KEYPAD, + [access_control_event.AUTO_LOCK_LOCKED_OPERATION] = METHOD.AUTO, + [access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION] = METHOD.AUTO + } + + event_to_send["data"] = {method = method_map[event]} + + -- SPECIAL CASES: + if (event == access_control_event.MANUAL_UNLOCK_OPERATION and cmd.args.event_parameter == 2) then + -- functionality from DTH, some locks can distinguish being manually locked via keypad + event_to_send.data.method = METHOD.KEYPAD + elseif (event == access_control_event.KEYPAD_LOCK_OPERATION or event == access_control_event.KEYPAD_UNLOCK_OPERATION) then + local code_id = cmd.args.v1_alarm_level + if cmd.args.event_parameter ~= nil and string.len(cmd.args.event_parameter) ~= 0 then + local event_params = { cmd.args.event_parameter:byte(1, -1) } + code_id = (#event_params == 1) and event_params[1] or event_params[3] + end + local user_id + if (credentials ~= nil and + credentials[code_id] ~= nil) then + user_id = credentials[code_id].userIndex + end + if user_id ~= nil then event_to_send["data"] = { userIndex = user_id, method = event_to_send["data"].method } end + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + "main", + capabilities.lock.ID, + capabilities.lock.lock.ID) == event_to_send.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local socket = require "socket" + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + local timer = device:get_field(DELAY_LOCK_EVENT_TIMER) + if timer ~= nil then + device.thread:cancel_timer(timer) + device:set_field(DELAY_LOCK_EVENT_TIMER, nil) + end + + device:emit_event(event_to_send) + end + end + end +end + +local users_number_report_handler = function(driver, device, cmd) + -- these are the same for Z-Wave + device:emit_event(LockUsers.totalUsersSupported(cmd.args.supported_users, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.pinUsersSupported(cmd.args.supported_users, { visibility = { displayed = false } })) +end + +local zwave_lock = { + lifecycle_handlers = { + added = added_handler, + }, + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + }, + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler, + [UserCode.USERS_NUMBER_REPORT] = users_number_report_handler, + } + }, + capability_handlers = { + [LockUsers.ID] = { + [LockUsers.commands.addUser.NAME] = add_user_handler, + [LockUsers.commands.updateUser.NAME] = update_user_handler, + [LockUsers.commands.deleteUser.NAME] = delete_user_handler, + [LockUsers.commands.deleteAllUsers.NAME] = delete_all_users_handler, + }, + [LockCredentials.ID] = { + [LockCredentials.commands.addCredential.NAME] = add_credential_handler, + [LockCredentials.commands.updateCredential.NAME] = update_credential_handler, + [LockCredentials.commands.deleteCredential.NAME] = delete_credential_handler, + [LockCredentials.commands.deleteAllCredentials.NAME] = delete_all_credentials_handler, + } + }, + NAME = "Using new capabilities", + can_handle = function(opts, driver, device, ...) + if not device:supports_capability_by_id(LockUsers.ID) then return false end + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if lock_codes_migrated then + local subdriver = require("using-new-capabilities") + return true, subdriver + end + return false + end +} + +return zwave_lock \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua new file mode 100644 index 0000000000..d39aa45d1c --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/keywe-lock/init.lua @@ -0,0 +1,86 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Association = (require "st.zwave.CommandClass.Association")({version=2}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control + +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local TamperDefaults = require "st.zwave.defaults.tamperAlert" + +local KEYWE_MFR = 0x037B +local TAMPER_CLEAR_DELAY = 10 + +local function can_handle_keywe_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == KEYWE_MFR +end + +local function clear_tamper_if_needed(device) + local current_tamper_state = device:get_latest_state("main", capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME) + if current_tamper_state == "detected" then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_OPEN then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_CLOSED then + event = capabilities.lock.lock.locked() + end + if event ~= nil then + event["data"] = {method = "manual"} + end + end + + if event ~= nil then + device:emit_event(event) + else + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + device.thread:call_with_delay( + TAMPER_CLEAR_DELAY, + function(d) + clear_tamper_if_needed(device) + end + ) + end +end + +local function do_configure(self, device) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local keywe_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Keywe Lock", + can_handle = can_handle_keywe_lock, +} + +return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua new file mode 100644 index 0000000000..43a22cc713 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/new_lock_utils.lua @@ -0,0 +1,227 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +local utils = require "st.utils" +local capabilities = require "st.capabilities" +local json = require "st.json" +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers +local INITIAL_INDEX = 1 + +local new_lock_utils = { + -- Constants + ADD_CREDENTIAL = "addCredential", + ADD_USER = "addUser", + COMMAND_NAME = "commandName", + CREDENTIAL_TYPE = "pin", + CHECKING_CODE = "checkingCode", + DELETE_ALL_CREDENTIALS = "deleteAllCredentials", + DELETE_ALL_USERS = "deleteAllUsers", + DELETE_CREDENTIAL = "deleteCredential", + DELETE_USER = "deleteUser", + LOCK_CREDENTIALS = "lockCredentials", + LOCK_USERS = "lockUsers", + PENDING_CREDENTIAL = "pendingCredential", + STATUS_BUSY = "busy", + STATUS_DUPLICATE = "duplicate", + STATUS_FAILURE = "failure", + STATUS_INVALID_COMMAND = "invalidCommand", + STATUS_OCCUPIED = "occupied", + STATUS_RESOURCE_EXHAUSTED = "resourceExhausted", + STATUS_SUCCESS = "success", + UPDATE_CREDENTIAL = "updateCredential", + UPDATE_USER = "updateUser", + USER_INDEX = "userIndex", + USER_NAME = "userName", + USER_TYPE = "userType" +} + +new_lock_utils.get_users = function(device) + local users = device:get_field(new_lock_utils.LOCK_USERS) + return users ~= nil and users or {} +end + +new_lock_utils.get_user = function(device, user_index) + for _, user in ipairs(new_lock_utils.get_users(device)) do + if user.userIndex == user_index then + return user + end + end + + return nil +end + +new_lock_utils.get_available_user_index = function(current_data, max) + if current_data == nil and max ~= 0 then + return INITIAL_INDEX + elseif current_data ~= nil then + for index = 1, max do + if current_data["user" .. index] == nil then + return index + end + end + end + + return nil +end + +new_lock_utils.get_credentials = function(device) + local credentials = device:get_field(new_lock_utils.LOCK_CREDENTIALS) + return credentials ~= nil and credentials or {} +end + +new_lock_utils.get_credential = function(device, credential_index) + for _, credential in ipairs(new_lock_utils.get_credentials(device)) do + if credential.credentialIndex == credential_index then + return credential + end + end + return nil +end + +new_lock_utils.get_available_credential_index = function(current_data, max) + local available_index = nil + local used_index = {} + + for i, _ in ipairs(current_data) do + used_index[i] = true + end + + if current_data ~= {} then + for index = 1, max do + if used_index[index] == nil then + available_index = index + break + end + end + else + available_index = INITIAL_INDEX + end + + return available_index +end + +new_lock_utils.create_user = function(device, user_name, user_type, user_index) + local status_code = new_lock_utils.STATUS_SUCCESS + local max_users = device:get_latest_state("main", capabilities.lockUsers.ID, + capabilities.lockUsers.totalUsersSupported.NAME, 0) + local current_users = new_lock_utils.get_users(device) + local available_index = new_lock_utils.get_available_user_index(current_users, max_users) + + if max_users == 0 or available_index == nil then + -- Can't add any users - update commandResult statusCode + status_code = new_lock_utils.STATUS_RESOURCE_EXHAUSTED + else + -- use the passed in index if it's set + if user_index ~= nil then + available_index = user_index + end + current_users["user"..available_index] = { userIndex = available_index, userType = user_type, userName = user_name } + device:set_field(new_lock_utils.LOCK_USERS, current_users, { persist = true }) + end + + return status_code +end + +new_lock_utils.delete_user = function(device, user_index, deleted_by_credential_deletion) + local current_users = new_lock_utils.get_users(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for index, user in pairs(current_users) do + if user.userIndex == user_index then + -- also delete associated credential if this isn't being call by a credential deletion. + if not deleted_by_credential_deletion then + -- find associated credential. + for _, credential in ipairs(new_lock_utils.get_credentials(device)) do + if credential.userIndex == user_index then + new_lock_utils.delete_credential(device, credential.credentialIndex, true) + break + end + end + end + -- table.remove(current_users, index) + current_users[index] = nil + device:set_field(new_lock_utils.LOCK_USERS, current_users) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + + return status_code +end + +new_lock_utils.add_credential = function(device, user_index, user_type, credential_type, credential_index) + -- need to also create a user if one does not exist at the user index. + if new_lock_utils.get_user(device, user_index) == nil then + local user_name = "USER_" .. user_index + local status = new_lock_utils.create_user(device, user_name, user_type, user_index) + if status ~= new_lock_utils.STATUS_SUCCESS then + return status + end + end + + local credentials = new_lock_utils.get_credentials(device) + table.insert(credentials, + { userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type }) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + return new_lock_utils.STATUS_SUCCESS +end + +new_lock_utils.delete_credential = function(device, credential_index, deleted_by_user_deletion) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for index, credential in pairs(credentials) do + if credential.credentialIndex == credential_index then + -- also delete associated user if this isn't being called by a user deletion. + if not deleted_by_user_deletion then + new_lock_utils.delete_user(device, credential.userIndex, true) + end + table.remove(credentials, index) + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + + return status_code +end + +new_lock_utils.update_credential = function(device, credential_index, user_index, credential_type) + local credentials = new_lock_utils.get_credentials(device) + local status_code = new_lock_utils.STATUS_FAILURE + + for _, credential in ipairs(credentials) do + if credential.credentialIndex == credential_index then + credential.credentialType = credential_type + credential.userIndex = user_index + device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials) + status_code = new_lock_utils.STATUS_SUCCESS + break + end + end + return status_code +end + +new_lock_utils.get_code_id_from_notification_event = function(event_params, v1_alarm_level) + -- some locks do not properly include the code ID in the event params, but do encode it + -- in the v1 alarm level + local code_id = v1_alarm_level + if event_params ~= nil and event_params ~= "" then + event_params = {event_params:byte(1,-1)} + code_id = (#event_params == 1) and event_params[1] or event_params[3] + end + return tostring(code_id) +end + +return new_lock_utils diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua new file mode 100644 index 0000000000..813c6217b4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/samsung-lock/init.lua @@ -0,0 +1,111 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local access_control_event = Notification.event.access_control + +local json = require "dkjson" +local constants = require "st.zwave.constants" + +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local get_lock_codes = LockCodesDefaults.get_lock_codes +local clear_code_state = LockCodesDefaults.clear_code_state +local code_deleted = LockCodesDefaults.code_deleted + +local SAMSUNG_MFR = 0x022E + +local function can_handle_samsung_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SAMSUNG_MFR +end + +local function get_ongoing_code_set(device) + local code_id + local code_state = device:get_field(constants.CODE_STATE) + if code_state ~= nil then + for key, state in pairs(code_state) do + if state ~= nil then + code_id = key:match("setName(%d)") + end + end + end + return code_id +end + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.NEW_USER_CODE_ADDED then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + device:send(UserCode:Get({user_identifier = code_id})) + return + end + elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) + clear_code_state(device, code_id) + end + elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then + -- Update Master Code in the same way as in defaults... + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + -- ...and delete rest of them, as lock does + local lock_codes = get_lock_codes(device) + for code_id, _ in pairs(lock_codes) do + if code_id ~= "0" then + code_deleted(device, code_id) + end + end + event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) + end + end + + if event ~= nil then + device:emit_event(event) + else + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + end +end + +-- Used doConfigure instead of added to not overwrite parent driver's added_handler +local function do_configure(self, device) + -- taken directly from DTH + -- Samsung locks won't allow you to enter the pairing menu when locked, so it must be unlocked + device:emit_event(capabilities.lock.lock.unlocked()) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) +end + +local samsung_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Samsung Lock", + can_handle = can_handle_samsung_lock, +} + +return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua new file mode 100644 index 0000000000..67e649d869 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/schlage-lock/init.lua @@ -0,0 +1,193 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local constants = require "st.zwave.constants" +local json = require "dkjson" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local user_id_status = UserCode.user_id_status +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=2}) +local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) +local Association = (require "st.zwave.CommandClass.Association")({version=1}) + +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + +local SCHLAGE_MFR = 0x003B +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds + +local function can_handle_schlage_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SCHLAGE_MFR +end + +local function set_code_length(self, device, cmd) + local length = cmd.args.length + if length >= 4 and length <= 8 then + device:send(Configuration:Set({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = length, + size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size + })) + end +end + +local function reload_all_codes(self, device, cmd) + LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + end +end + +local function set_code(self, device, cmd) + if (cmd.args.codePIN == "") then + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = {cmd.args.codeSlot, cmd.args.codeName}, + }) + else + -- copied from defaults with additional check for Schlage's configuration + if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then + if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end + local code_state = device:get_field(constants.CODE_STATE) + code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName + device:set_field(constants.CODE_STATE, code_state, { persist = true }) + end + local send_set_user_code = function () + device:send(UserCode:Set({ + user_identifier = cmd.args.codeSlot, + user_code = cmd.args.codePIN, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + end + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device.thread:call_with_delay(DEFAULT_COMMANDS_DELAY, send_set_user_code) + else + send_set_user_code() + end + end +end + +local function do_configure(self, device) + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function basic_set_handler(self, device, cmd) + device:emit_event(cmd.args.value == 0 and capabilities.lock.lock.unlocked() or capabilities.lock.lock.locked()) + device:send(Association:Remove({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function configuration_report(self, device, cmd) + local parameter_number = cmd.args.parameter_number + if parameter_number == SCHLAGE_LOCK_CODE_LENGTH_PARAM.number then + local reported_code_length = cmd.args.configuration_value + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length ~= nil and current_code_length ~= reported_code_length then + local all_codes_deleted_mocked_command = Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.ALL_USER_CODES_DELETED + }) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) + end + device:emit_event(capabilities.lockCodes.codeLength(reported_code_length)) + end +end + +local function is_user_code_report_mfr_specific(device, cmd) + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local code_id = cmd.args.user_identifier + + if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 + (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then + local code_state = device:get_field(constants.CODE_STATE) + return user_code == "**********" or user_code == nil or (code_state ~= nil and code_state["setName"..cmd.args.user_identifier] ~= nil) + else + return (code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE) or + reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE + end +end + +local function user_code_report_handler(self, device, cmd) + local code_id = cmd.args.user_identifier + if is_user_code_report_mfr_specific(device, cmd) then + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local event + + if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 + (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then + local code_name = LockCodesDefaults.get_code_name(device, code_id) + local change_type = LockCodesDefaults.get_change_type(device, code_id) + event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) + event.data = {codeName = code_name} + if code_id ~= 0 then -- ~= MASTER_CODE + LockCodesDefaults.code_set_event(device, code_id, code_name) + end + elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then + local lock_codes = LockCodesDefaults.get_lock_codes(device) + for _code_id, _ in pairs(lock_codes) do + LockCodesDefaults.code_deleted(device, _code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) + else -- user_id_status.STATUS_NOT_AVAILABLE + event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) + end + + if event ~= nil then + device:emit_event(event) + end + LockCodesDefaults.clear_code_state(device, code_id) + LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) + else + LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + end +end + +local schlage_lock = { + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, + [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [capabilities.lockCodes.commands.setCode.NAME] = set_code + } + }, + zwave_handlers = { + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + }, + [cc.BASIC] = { + [Basic.SET] = basic_set_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "Schlage Lock", + can_handle = can_handle_schlage_lock, +} + +return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua new file mode 100644 index 0000000000..44d978999b --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-new-capabilities/zwave-alarm-v1-lock/init.lua @@ -0,0 +1,165 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Alarm +local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.defaults.lockCodes +local lock_code_defaults = require "st.zwave.defaults.lockCodes" +local json = require "dkjson" + +local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + COMMAND = "command", + AUTO = "auto" +} + +--- Determine whether the passed command is a V1 alarm command +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @return boolean true if the device is smoke co alarm +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 +end + +--- Default handler for alarm command class reports, these were largely OEM-defined +--- +--- This converts alarm V1 reports to correct lock events +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.Alarm.Report +local function alarm_report_handler(driver, device, cmd) + local alarm_type = cmd.args.alarm_type + local event = nil + local lock_codes = lock_code_defaults.get_lock_codes(device) + local code_id = nil + if (cmd.args.alarm_level ~= nil) then + code_id = tostring(cmd.args.alarm_level) + end + if (alarm_type == 9 or alarm_type == 17) then + event = capabilities.lock.lock.unknown() + elseif (alarm_type == 16 or alarm_type == 19) then + event = capabilities.lock.lock.unlocked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 18) then + event = capabilities.lock.lock.locked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 21) then + event = capabilities.lock.lock.locked() + if (cmd.args.alarm_level == 2) then + event["data"] = {method = METHOD.MANUAL} + else + event["data"] = {method = METHOD.KEYPAD} + end + elseif (alarm_type == 22) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.MANUAL} + elseif (alarm_type == 23) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 24) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 25) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 26) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 27) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 32) then + -- all user codes deleted + for code_id, _ in pairs(lock_codes) do + lock_code_defaults.code_deleted(device, code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + elseif (alarm_type == 33) then + -- user code deleted + if (code_id ~= nil) then + lock_code_defaults.clear_code_state(device, code_id) + if (lock_codes[code_id] ~= nil) then + lock_code_defaults.code_deleted(device, code_id) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + end + end + elseif (alarm_type == 13 or alarm_type == 112) then + -- user code changed/set + if (code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + local change_type = lock_code_defaults.get_change_type(device, code_id) + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) + code_changed_event["data"] = { codeName = code_name} + lock_code_defaults.code_set_event(device, code_id, code_name) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 34 or alarm_type == 113) then + -- duplicate lock code + if (code_id ~= nil) then + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 130) then + -- batteries replaced + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 161) then + -- tamper alarm + event = capabilities.tamperAlert.tamper.detected() + elseif (alarm_type == 167) then + -- low battery + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 168) then + -- critical battery + event = capabilities.battery.battery(1) + elseif (alarm_type == 169) then + -- battery too low to operate + event = capabilities.battery.battery(0) + end + + if (event ~= nil) then + device:emit_event(event) + end +end + +local zwave_lock = { + zwave_handlers = { + [cc.ALARM] = { + [Alarm.REPORT] = alarm_report_handler + } + }, + NAME = "Z-Wave lock alarm V1", + can_handle = can_handle_v1_alarm, +} + +return zwave_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua new file mode 100644 index 0000000000..8151871ac3 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device, ...) + local capabilities = require "st.capabilities" + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if not lock_codes_migrated then + local subdriver = require("using-old-capabilities") + return true, subdriver + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua new file mode 100644 index 0000000000..08cd518841 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/init.lua @@ -0,0 +1,175 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" + +local SCAN_CODES_CHECK_INTERVAL = 30 + +local function periodic_codes_state_verification(driver, device) + local scan_codes_state = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.scanCodes.NAME) + if scan_codes_state == "Scanning" then + driver:inject_capability_command(device, + { capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + } + ) + device.thread:call_with_delay( + SCAN_CODES_CHECK_INTERVAL, + function(d) + periodic_codes_state_verification(driver, device) + end + ) + end +end + +local init_handler = function(driver, device, event) + local constants = require "st.zwave.constants" + -- temp fix before this can be changed from being persisted in memory + device:set_field(constants.CODE_STATE, nil, { persist = true }) +end + +--- Builds up initial state for the device +--- +--- @param self st.zwave.Driver +--- @param device st.zwave.Device +local function added_handler(self, device) + self:inject_capability_command(device, + { capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} }) + device.thread:call_with_delay( + SCAN_CODES_CHECK_INTERVAL, + function(d) + periodic_codes_state_verification(self, device) + end + ) + local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) + device:send(DoorLock:OperationGet({})) + device:send(Battery:Get({})) + if (device:supports_capability(capabilities.tamperAlert)) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function update_codes(driver, device, cmd) + local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + local delay = 0 + -- args.codes is json + for name, code in pairs(cmd.args.codes) do + -- these seem to come in the format "code[slot#]: code" + local code_slot = tonumber(string.gsub(name, "code", ""), 10) + if (code_slot ~= nil) then + if (code ~= nil and (code ~= "0" and code ~= "")) then + -- code changed + device.thread:call_with_delay(delay, function () + device:send(UserCode:Set({ + user_identifier = code_slot, + user_code = code, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + end) + delay = delay + 2.2 + else + -- code deleted + device.thread:call_with_delay(delay, function () + device:send(UserCode:Set({user_identifier = code_slot, user_id_status = UserCode.user_id_status.AVAILABLE})) + end) + delay = delay + 2.2 + device.thread:call_with_delay(delay, function () + device:send(UserCode:Get({user_identifier = code_slot})) + end) + delay = delay + 2.2 + end + end + end +end + +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function migrate(driver, device, cmd) + local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + local get_lock_codes = LockCodesDefaults.get_lock_codes + local lock_users = {} + local lock_credentials = {} + local lock_codes = get_lock_codes(device) + local ordered_codes = {} + + for code in pairs(lock_codes) do + table.insert(ordered_codes, code) + end + + table.sort(ordered_codes) + for index = 1, #ordered_codes do + local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ] + table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name}) + table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) + end + + local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 4) + local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 10) + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME, 8) + if (code_length ~= nil) then + max_code_len = code_length + min_code_len = code_length + end + + device:emit_event(capabilities.lockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) +end + +local using_old_capabilities = { + lifecycle_handlers = { + init = init_handler, + added = added_handler, + }, + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes + }, + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.migrate.NAME] = migrate + }, + }, + sub_drivers = { + require("using-old-capabilities.zwave-alarm-v1-lock"), + require("using-old-capabilities.schlage-lock"), + require("using-old-capabilities.samsung-lock"), + require("using-old-capabilities.keywe-lock"), + }, + can_handle = function(opts, driver, device, ...) + if not device:supports_capability_by_id(capabilities.lockCodes.ID) then return false end + local lock_codes_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, + capabilities.lockCodes.migrated.NAME, false) + if not lock_codes_migrated then + local subdriver = require("using-old-capabilities") + return true, subdriver + end + return false + end, + NAME = "Using old capabilities" +} + +return using_old_capabilities diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua new file mode 100644 index 0000000000..d39aa45d1c --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/keywe-lock/init.lua @@ -0,0 +1,86 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Association = (require "st.zwave.CommandClass.Association")({version=2}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control + +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local TamperDefaults = require "st.zwave.defaults.tamperAlert" + +local KEYWE_MFR = 0x037B +local TAMPER_CLEAR_DELAY = 10 + +local function can_handle_keywe_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == KEYWE_MFR +end + +local function clear_tamper_if_needed(device) + local current_tamper_state = device:get_latest_state("main", capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME) + if current_tamper_state == "detected" then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_OPEN then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_CLOSED then + event = capabilities.lock.lock.locked() + end + if event ~= nil then + event["data"] = {method = "manual"} + end + end + + if event ~= nil then + device:emit_event(event) + else + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + device.thread:call_with_delay( + TAMPER_CLEAR_DELAY, + function(d) + clear_tamper_if_needed(device) + end + ) + end +end + +local function do_configure(self, device) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local keywe_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Keywe Lock", + can_handle = can_handle_keywe_lock, +} + +return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua new file mode 100644 index 0000000000..813c6217b4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/samsung-lock/init.lua @@ -0,0 +1,111 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local access_control_event = Notification.event.access_control + +local json = require "dkjson" +local constants = require "st.zwave.constants" + +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local get_lock_codes = LockCodesDefaults.get_lock_codes +local clear_code_state = LockCodesDefaults.clear_code_state +local code_deleted = LockCodesDefaults.code_deleted + +local SAMSUNG_MFR = 0x022E + +local function can_handle_samsung_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SAMSUNG_MFR +end + +local function get_ongoing_code_set(device) + local code_id + local code_state = device:get_field(constants.CODE_STATE) + if code_state ~= nil then + for key, state in pairs(code_state) do + if state ~= nil then + code_id = key:match("setName(%d)") + end + end + end + return code_id +end + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.NEW_USER_CODE_ADDED then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + device:send(UserCode:Get({user_identifier = code_id})) + return + end + elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) + clear_code_state(device, code_id) + end + elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then + -- Update Master Code in the same way as in defaults... + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + -- ...and delete rest of them, as lock does + local lock_codes = get_lock_codes(device) + for code_id, _ in pairs(lock_codes) do + if code_id ~= "0" then + code_deleted(device, code_id) + end + end + event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) + end + end + + if event ~= nil then + device:emit_event(event) + else + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + end +end + +-- Used doConfigure instead of added to not overwrite parent driver's added_handler +local function do_configure(self, device) + -- taken directly from DTH + -- Samsung locks won't allow you to enter the pairing menu when locked, so it must be unlocked + device:emit_event(capabilities.lock.lock.unlocked()) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) +end + +local samsung_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Samsung Lock", + can_handle = can_handle_samsung_lock, +} + +return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua new file mode 100644 index 0000000000..67e649d869 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/schlage-lock/init.lua @@ -0,0 +1,193 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local constants = require "st.zwave.constants" +local json = require "dkjson" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local user_id_status = UserCode.user_id_status +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=2}) +local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) +local Association = (require "st.zwave.CommandClass.Association")({version=1}) + +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + +local SCHLAGE_MFR = 0x003B +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds + +local function can_handle_schlage_lock(opts, self, device, cmd, ...) + return device.zwave_manufacturer_id == SCHLAGE_MFR +end + +local function set_code_length(self, device, cmd) + local length = cmd.args.length + if length >= 4 and length <= 8 then + device:send(Configuration:Set({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = length, + size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size + })) + end +end + +local function reload_all_codes(self, device, cmd) + LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + end +end + +local function set_code(self, device, cmd) + if (cmd.args.codePIN == "") then + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = {cmd.args.codeSlot, cmd.args.codeName}, + }) + else + -- copied from defaults with additional check for Schlage's configuration + if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then + if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end + local code_state = device:get_field(constants.CODE_STATE) + code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName + device:set_field(constants.CODE_STATE, code_state, { persist = true }) + end + local send_set_user_code = function () + device:send(UserCode:Set({ + user_identifier = cmd.args.codeSlot, + user_code = cmd.args.codePIN, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + end + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device.thread:call_with_delay(DEFAULT_COMMANDS_DELAY, send_set_user_code) + else + send_set_user_code() + end + end +end + +local function do_configure(self, device) + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function basic_set_handler(self, device, cmd) + device:emit_event(cmd.args.value == 0 and capabilities.lock.lock.unlocked() or capabilities.lock.lock.locked()) + device:send(Association:Remove({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function configuration_report(self, device, cmd) + local parameter_number = cmd.args.parameter_number + if parameter_number == SCHLAGE_LOCK_CODE_LENGTH_PARAM.number then + local reported_code_length = cmd.args.configuration_value + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length ~= nil and current_code_length ~= reported_code_length then + local all_codes_deleted_mocked_command = Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.ALL_USER_CODES_DELETED + }) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) + end + device:emit_event(capabilities.lockCodes.codeLength(reported_code_length)) + end +end + +local function is_user_code_report_mfr_specific(device, cmd) + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local code_id = cmd.args.user_identifier + + if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 + (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then + local code_state = device:get_field(constants.CODE_STATE) + return user_code == "**********" or user_code == nil or (code_state ~= nil and code_state["setName"..cmd.args.user_identifier] ~= nil) + else + return (code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE) or + reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE + end +end + +local function user_code_report_handler(self, device, cmd) + local code_id = cmd.args.user_identifier + if is_user_code_report_mfr_specific(device, cmd) then + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local event + + if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 + (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then + local code_name = LockCodesDefaults.get_code_name(device, code_id) + local change_type = LockCodesDefaults.get_change_type(device, code_id) + event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) + event.data = {codeName = code_name} + if code_id ~= 0 then -- ~= MASTER_CODE + LockCodesDefaults.code_set_event(device, code_id, code_name) + end + elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then + local lock_codes = LockCodesDefaults.get_lock_codes(device) + for _code_id, _ in pairs(lock_codes) do + LockCodesDefaults.code_deleted(device, _code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) + else -- user_id_status.STATUS_NOT_AVAILABLE + event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) + end + + if event ~= nil then + device:emit_event(event) + end + LockCodesDefaults.clear_code_state(device, code_id) + LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) + else + LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + end +end + +local schlage_lock = { + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, + [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [capabilities.lockCodes.commands.setCode.NAME] = set_code + } + }, + zwave_handlers = { + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + }, + [cc.BASIC] = { + [Basic.SET] = basic_set_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "Schlage Lock", + can_handle = can_handle_schlage_lock, +} + +return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua new file mode 100644 index 0000000000..44d978999b --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/using-old-capabilities/zwave-alarm-v1-lock/init.lua @@ -0,0 +1,165 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Alarm +local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.defaults.lockCodes +local lock_code_defaults = require "st.zwave.defaults.lockCodes" +local json = require "dkjson" + +local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + COMMAND = "command", + AUTO = "auto" +} + +--- Determine whether the passed command is a V1 alarm command +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @return boolean true if the device is smoke co alarm +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 +end + +--- Default handler for alarm command class reports, these were largely OEM-defined +--- +--- This converts alarm V1 reports to correct lock events +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.Alarm.Report +local function alarm_report_handler(driver, device, cmd) + local alarm_type = cmd.args.alarm_type + local event = nil + local lock_codes = lock_code_defaults.get_lock_codes(device) + local code_id = nil + if (cmd.args.alarm_level ~= nil) then + code_id = tostring(cmd.args.alarm_level) + end + if (alarm_type == 9 or alarm_type == 17) then + event = capabilities.lock.lock.unknown() + elseif (alarm_type == 16 or alarm_type == 19) then + event = capabilities.lock.lock.unlocked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 18) then + event = capabilities.lock.lock.locked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 21) then + event = capabilities.lock.lock.locked() + if (cmd.args.alarm_level == 2) then + event["data"] = {method = METHOD.MANUAL} + else + event["data"] = {method = METHOD.KEYPAD} + end + elseif (alarm_type == 22) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.MANUAL} + elseif (alarm_type == 23) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 24) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 25) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 26) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 27) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 32) then + -- all user codes deleted + for code_id, _ in pairs(lock_codes) do + lock_code_defaults.code_deleted(device, code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + elseif (alarm_type == 33) then + -- user code deleted + if (code_id ~= nil) then + lock_code_defaults.clear_code_state(device, code_id) + if (lock_codes[code_id] ~= nil) then + lock_code_defaults.code_deleted(device, code_id) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + end + end + elseif (alarm_type == 13 or alarm_type == 112) then + -- user code changed/set + if (code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + local change_type = lock_code_defaults.get_change_type(device, code_id) + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) + code_changed_event["data"] = { codeName = code_name} + lock_code_defaults.code_set_event(device, code_id, code_name) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 34 or alarm_type == 113) then + -- duplicate lock code + if (code_id ~= nil) then + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 130) then + -- batteries replaced + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 161) then + -- tamper alarm + event = capabilities.tamperAlert.tamper.detected() + elseif (alarm_type == 167) then + -- low battery + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 168) then + -- critical battery + event = capabilities.battery.battery(1) + elseif (alarm_type == 169) then + -- battery too low to operate + event = capabilities.battery.battery(0) + end + + if (event ~= nil) then + device:emit_event(event) + end +end + +local zwave_lock = { + zwave_handlers = { + [cc.ALARM] = { + [Alarm.REPORT] = alarm_report_handler + } + }, + NAME = "Z-Wave lock alarm V1", + can_handle = can_handle_v1_alarm, +} + +return zwave_lock