Skip to content

Commit 9c7d6f9

Browse files
CHAD-16364: SLGA Zigbee: Add lockUsers and lockCredentials capabilities
1 parent 6a4aab5 commit 9c7d6f9

File tree

23 files changed

+2813
-315
lines changed

23 files changed

+2813
-315
lines changed

drivers/SmartThings/zigbee-lock/profiles/base-lock.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ components:
66
version: 1
77
- id: lockCodes
88
version: 1
9+
- id: lockCredentials
10+
version: 1
11+
- id: lockUsers
12+
version: 1
913
- id: battery
1014
version: 1
1115
- id: firmwareUpdate

drivers/SmartThings/zigbee-lock/src/init.lua

Lines changed: 26 additions & 311 deletions
Large diffs are not rendered by default.

drivers/SmartThings/zigbee-lock/src/lock_utils.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ local LockCodes = capabilities.lockCodes
2020
local lock_utils = {
2121
-- Constants
2222
LOCK_CODES = "lockCodes",
23+
LOCK_USERS = "lockUsers",
2324
CHECKING_CODE = "checkingCode",
2425
CODE_STATE = "codeState",
2526
MIGRATION_COMPLETE = "migrationComplete",
2627
MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped",
27-
CHECKED_CODE_SUPPORT = "checkedCodeSupport"
28+
CHECKED_CODE_SUPPORT = "checkedCodeSupport",
2829
}
2930

3031
lock_utils.get_lock_codes = function(device)
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
local utils = require "st.utils"
15+
local capabilities = require "st.capabilities"
16+
local INITIAL_INDEX = 1
17+
18+
local new_lock_utils = {
19+
-- Constants
20+
ADD_CREDENTIAL = "addCredential",
21+
ADD_USER = "addUser",
22+
BUSY = "busy",
23+
COMMAND_NAME = "commandName",
24+
CREDENTIAL_TYPE = "pin",
25+
CHECKING_CODE = "checkingCode",
26+
DELETE_ALL_CREDENTIALS = "deleteAllCredentials",
27+
DELETE_ALL_USERS = "deleteAllUsers",
28+
DELETE_CREDENTIAL = "deleteCredential",
29+
DELETE_USER = "deleteUser",
30+
LOCK_CREDENTIALS = "lockCredentials",
31+
LOCK_USERS = "lockUsers",
32+
ACTIVE_CREDENTIAL = "activeCredential",
33+
STATUS_BUSY = "busy",
34+
STATUS_DUPLICATE = "duplicate",
35+
STATUS_FAILURE = "failure",
36+
STATUS_INVALID_COMMAND = "invalidCommand",
37+
STATUS_OCCUPIED = "occupied",
38+
STATUS_RESOURCE_EXHAUSTED = "resourceExhausted",
39+
STATUS_SUCCESS = "success",
40+
UPDATE_CREDENTIAL = "updateCredential",
41+
UPDATE_USER = "updateUser",
42+
USER_INDEX = "userIndex",
43+
USER_NAME = "userName",
44+
USER_TYPE = "userType"
45+
}
46+
47+
-- check if we are currently busy performing a task.
48+
-- if we aren't then set as busy.
49+
new_lock_utils.busy_check_and_set = function (device, command, override_busy_check)
50+
if override_busy_check then
51+
-- the function was called by an injected command.
52+
return false
53+
end
54+
55+
local c_time = os.time()
56+
local busy_state = device:get_field(new_lock_utils.BUSY) or false
57+
58+
if busy_state == false or c_time - busy_state > 10 then
59+
device:set_field(new_lock_utils.COMMAND_NAME, command)
60+
device:set_field(new_lock_utils.BUSY, c_time)
61+
return false
62+
else
63+
local command_result_info = {
64+
commandName = command.name,
65+
statusCode = new_lock_utils.STATUS_BUSY
66+
}
67+
if command.type == new_lock_utils.LOCK_USERS then
68+
device:emit_event(capabilities.lockUsers.commandResult(
69+
command_result_info, { state_change = true, visibility = { displayed = true } }
70+
))
71+
else
72+
device:emit_event(capabilities.lockCredentials.commandResult(
73+
command_result_info, { state_change = true, visibility = { displayed = true } }
74+
))
75+
end
76+
return true
77+
end
78+
end
79+
80+
new_lock_utils.clear_busy_state = function(device, status, override_busy_check)
81+
if override_busy_check then
82+
return
83+
end
84+
local command = device:get_field(new_lock_utils.COMMAND_NAME)
85+
local active_credential = device:get_field(new_lock_utils.ACTIVE_CREDENTIAL)
86+
if command ~= nil then
87+
local command_result_info = {
88+
commandName = command.name,
89+
statusCode = status
90+
}
91+
if command.type == new_lock_utils.LOCK_USERS then
92+
if active_credential ~= nil and active_credential.userIndex ~= nil then
93+
command_result_info.userIndex = active_credential.userIndex
94+
end
95+
device:emit_event(capabilities.lockUsers.commandResult(
96+
command_result_info, { state_change = true, visibility = { displayed = true } }
97+
))
98+
else
99+
if active_credential ~= nil and active_credential.userIndex ~= nil then
100+
command_result_info.userIndex = active_credential.userIndex
101+
end
102+
if active_credential ~= nil and active_credential.credentialIndex ~= nil then
103+
command_result_info.credentialIndex = active_credential.credentialIndex
104+
end
105+
device:emit_event(capabilities.lockCredentials.commandResult(
106+
command_result_info, { state_change = true, visibility = { displayed = true } }
107+
))
108+
end
109+
end
110+
111+
device:set_field(new_lock_utils.ACTIVE_CREDENTIAL, nil)
112+
device:set_field(new_lock_utils.COMMAND_NAME, nil)
113+
device:set_field(new_lock_utils.BUSY, false)
114+
end
115+
116+
117+
new_lock_utils.reload_tables = function(device)
118+
local users = device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.users.NAME, {})
119+
local credentials = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.credentials.NAME, {})
120+
device:set_field(new_lock_utils.LOCK_USERS, users)
121+
device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials)
122+
end
123+
124+
new_lock_utils.get_users = function(device)
125+
local users = device:get_field(new_lock_utils.LOCK_USERS)
126+
return users ~= nil and users or {}
127+
end
128+
129+
new_lock_utils.get_user = function(device, user_index)
130+
for _, user in pairs(new_lock_utils.get_users(device)) do
131+
if user.userIndex == user_index then
132+
return user
133+
end
134+
end
135+
136+
return nil
137+
end
138+
139+
new_lock_utils.get_available_user_index = function(device)
140+
local max = device:get_latest_state("main", capabilities.lockUsers.ID,
141+
capabilities.lockUsers.totalUsersSupported.NAME, 0)
142+
local current_users = new_lock_utils.get_users(device)
143+
local available_index = nil
144+
local used_index = {}
145+
for _, user in pairs(current_users) do
146+
used_index[user.userIndex] = true
147+
end
148+
if current_users ~= {} then
149+
for index = 1, max do
150+
if used_index[index] == nil then
151+
available_index = index
152+
break
153+
end
154+
end
155+
else
156+
available_index = INITIAL_INDEX
157+
end
158+
return available_index
159+
end
160+
161+
new_lock_utils.get_credentials = function(device)
162+
local credentials = device:get_field(new_lock_utils.LOCK_CREDENTIALS)
163+
return credentials ~= nil and credentials or {}
164+
end
165+
166+
new_lock_utils.get_credential = function(device, credential_index)
167+
for _, credential in pairs(new_lock_utils.get_credentials(device)) do
168+
if credential.credentialIndex == credential_index then
169+
return credential
170+
end
171+
end
172+
return nil
173+
end
174+
175+
new_lock_utils.get_credential_by_user_index = function(device, user_index)
176+
for _, credential in pairs(new_lock_utils.get_credentials(device)) do
177+
if credential.userIndex == user_index then
178+
return credential
179+
end
180+
end
181+
182+
return nil
183+
end
184+
185+
new_lock_utils.get_available_credential_index = function(device)
186+
local max = device:get_latest_state("main", capabilities.lockCredentials.ID,
187+
capabilities.lockCredentials.pinUsersSupported.NAME, 0)
188+
local current_credentials = new_lock_utils.get_credentials(device)
189+
local available_index = nil
190+
local used_index = {}
191+
for _, credential in pairs(current_credentials) do
192+
used_index[credential.credentialIndex] = true
193+
end
194+
if current_credentials ~= {} then
195+
for index = 1, max do
196+
if used_index[index] == nil then
197+
available_index = index
198+
break
199+
end
200+
end
201+
else
202+
available_index = INITIAL_INDEX
203+
end
204+
return available_index
205+
end
206+
207+
new_lock_utils.create_user = function(device, user_name, user_type, user_index)
208+
if user_name == nil then
209+
user_name = "Guest" .. user_index
210+
end
211+
212+
local current_users = new_lock_utils.get_users(device)
213+
table.insert(current_users, { userIndex = user_index, userType = user_type, userName = user_name })
214+
device:set_field(new_lock_utils.LOCK_USERS, current_users)
215+
end
216+
217+
new_lock_utils.delete_user = function(device, user_index)
218+
local current_users = new_lock_utils.get_users(device)
219+
local status_code = new_lock_utils.STATUS_FAILURE
220+
221+
for index, user in pairs(current_users) do
222+
if user.userIndex == user_index then
223+
table.remove(current_users, index)
224+
device:set_field(new_lock_utils.LOCK_USERS, current_users)
225+
status_code = new_lock_utils.STATUS_SUCCESS
226+
break
227+
end
228+
end
229+
return status_code
230+
end
231+
232+
new_lock_utils.add_credential = function(device, user_index, credential_type, credential_index)
233+
local credentials = new_lock_utils.get_credentials(device)
234+
table.insert(credentials,
235+
{ userIndex = user_index, credentialIndex = credential_index, credentialType = credential_type })
236+
device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials)
237+
return new_lock_utils.STATUS_SUCCESS
238+
end
239+
240+
new_lock_utils.delete_credential = function(device, credential_index)
241+
local credentials = new_lock_utils.get_credentials(device)
242+
local status_code = new_lock_utils.STATUS_FAILURE
243+
244+
for index, credential in pairs(credentials) do
245+
if credential.credentialIndex == credential_index then
246+
new_lock_utils.delete_user(device, credential.userIndex)
247+
table.remove(credentials, index)
248+
device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials)
249+
status_code = new_lock_utils.STATUS_SUCCESS
250+
break
251+
end
252+
end
253+
254+
return status_code
255+
end
256+
257+
new_lock_utils.update_credential = function(device, credential_index, user_index, credential_type)
258+
local credentials = new_lock_utils.get_credentials(device)
259+
local status_code = new_lock_utils.STATUS_FAILURE
260+
261+
for _, credential in pairs(credentials) do
262+
if credential.credentialIndex == credential_index then
263+
credential.credentialType = credential_type
264+
credential.userIndex = user_index
265+
device:set_field(new_lock_utils.LOCK_CREDENTIALS, credentials)
266+
status_code = new_lock_utils.STATUS_SUCCESS
267+
break
268+
end
269+
end
270+
return status_code
271+
end
272+
273+
return new_lock_utils
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
-- Mock out globals
16+
local test = require "integration_test"
17+
local zigbee_test_utils = require "integration_test.zigbee_test_utils"
18+
local t_utils = require "integration_test.utils"
19+
20+
local clusters = require "st.zigbee.zcl.clusters"
21+
local PowerConfiguration = clusters.PowerConfiguration
22+
local DoorLock = clusters.DoorLock
23+
local Alarm = clusters.Alarms
24+
local capabilities = require "st.capabilities"
25+
26+
local json = require "st.json"
27+
28+
local mock_datastore = require "integration_test.mock_env_datastore"
29+
30+
local mock_device = test.mock_device.build_test_zigbee_device(
31+
{
32+
profile = t_utils.get_profile_definition("base-lock.yml"),
33+
data = {
34+
lockCodes = json.encode({
35+
["1"] = "Zach",
36+
["5"] = "Steven"
37+
}),
38+
}
39+
}
40+
)
41+
42+
zigbee_test_utils.prepare_zigbee_env_info()
43+
local function test_init()end
44+
45+
test.set_test_init_function(test_init)
46+
47+
test.register_coroutine_test(
48+
"Device called 'migrate' command",
49+
function()
50+
-- test.mock_device.add_test_device(mock_device)
51+
-- -- test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
52+
-- test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) })
53+
-- test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) })
54+
-- test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) })
55+
-- test.wait_for_events()
56+
-- -- Validate lockCodes field
57+
-- mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" })
58+
-- -- Validate migration complete flag
59+
-- mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true)
60+
61+
-- -- Set min/max code length attributes
62+
-- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) })
63+
-- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) })
64+
-- test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) })
65+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } })))
66+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } })))
67+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } })))
68+
-- test.wait_for_events()
69+
-- -- Validate `migrate` command functionality.
70+
-- test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } })
71+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(5, { visibility = { displayed = false } })))
72+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
73+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } })))
74+
-- 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 } })))
75+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })))
76+
-- 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 } })))
77+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } })))
78+
-- test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })))
79+
-- test.wait_for_events()
80+
end
81+
)
82+
83+
test.run_registered_tests()

0 commit comments

Comments
 (0)