Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions drivers/SmartThings/zigbee-lock/profiles/base-lock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
320 changes: 13 additions & 307 deletions drivers/SmartThings/zigbee-lock/src/init.lua

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-lock/src/lock_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ local lock_utils = {
CODE_STATE = "codeState",
MIGRATION_COMPLETE = "migrationComplete",
MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped",
CHECKED_CODE_SUPPORT = "checkedCodeSupport"
CHECKED_CODE_SUPPORT = "checkedCodeSupport",
}

lock_utils.get_lock_codes = function(device)
Expand Down
216 changes: 216 additions & 0 deletions drivers/SmartThings/zigbee-lock/src/new_lock_utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
-- 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

return new_lock_utils
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

-- Mock out globals
local test = require "integration_test"
local zigbee_test_utils = require "integration_test.zigbee_test_utils"
local t_utils = require "integration_test.utils"

local clusters = require "st.zigbee.zcl.clusters"
local PowerConfiguration = clusters.PowerConfiguration
local DoorLock = clusters.DoorLock
local Alarm = clusters.Alarms
local capabilities = require "st.capabilities"

local json = require "st.json"

local mock_datastore = require "integration_test.mock_env_datastore"

local mock_device = test.mock_device.build_test_zigbee_device(
{
profile = t_utils.get_profile_definition("base-lock.yml"),
data = {
lockCodes = json.encode({
["1"] = "Zach",
["5"] = "Steven"
}),
}
}
)

zigbee_test_utils.prepare_zigbee_env_info()
local function test_init()end

test.set_test_init_function(test_init)

test.register_coroutine_test(
"Device called 'migrate' command",
function()
test.mock_device.add_test_device(mock_device)
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) })
test.wait_for_events()
-- Validate lockCodes field
mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" })
-- Validate migration complete flag
mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true)

-- Set min/max code length attributes
test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) })
test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) })
test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) })
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } })))
test.wait_for_events()
-- Validate `migrate` command functionality.
test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } })
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(5, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })))
test.wait_for_events()
end
)

test.run_registered_tests()
Loading
Loading