Skip to content
Closed
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
20 changes: 14 additions & 6 deletions drivers/SmartThings/zigbee-lock/src/lock_handlers/capabilities.lua
Original file line number Diff line number Diff line change
Expand Up @@ -133,27 +133,35 @@ function CapabilityHandlers.add_credential(driver, device, command)
return
end

-- A userIndex of 0 means "auto-assign the next available slot". Credential index 0 is reserved for the master code,
-- so auto-assigned credentials start from 1.
local user_index = command.args.userIndex == 0 and tables.next_index(device, "users") or command.args.userIndex
local cred_index = command.args.userIndex == 0 and tables.next_index(device, "credentials", 1) or command.args.userIndex
-- addCredential has no credentialIndex argument, so its userIndex argument selects the credential slot. A value
-- of 0 means "auto-assign the next available slot". The user is always new, and takes the next available user slot.
local cred_index = command.args.userIndex == 0 and tables.next_index(device, "credentials") or command.args.userIndex
local user_index = tables.next_index(device, "users")

if #(tables.get_state(device, "credentials") or {}) >= tables.get_max_entries(device, "credentials") then
local max_credentials = tables.get_max_entries(device, "credentials")
if #(tables.get_state(device, "credentials") or {}) >= max_credentials then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.RESOURCE_EXHAUSTED)
return
end

-- a requested slot must not already hold a credential
if tables.find_entry(device, "credentials", cred_index) then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.OCCUPIED)
return
end

-- Set busy state and attempt to set the PIN on-device.
lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.ADD, {
userIndex = user_index,
userType = command.args.userType,
credentialIndex = cred_index,
credentialType = command.args.credentialType,
credentialName = command.args.credentialName,
})
device:send(LockCluster.server.commands.SetPINCode(device,
cred_index,
UserStatusEnum.OCCUPIED_ENABLED,
UserTypeEnum.UNRESTRICTED,
command.args.userType == consts.MASTER.USER_TYPE and UserTypeEnum.MASTER_USER or UserTypeEnum.UNRESTRICTED,
command.args.credentialData)
)
end
Expand Down
5 changes: 2 additions & 3 deletions drivers/SmartThings/zigbee-lock/src/lock_utils/tables.lua
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,13 @@ end

-- Return the lowest integer, starting at the table's minimum, not yet used as the
-- match_key in the named table. Used to auto-assign the next available slot for a new entry.
-- An optional `start` overrides the table's minimum, e.g. to skip a reserved slot.
function table_utils.next_index(device, table_name, start)
function table_utils.next_index(device, table_name)
local def = resolve_table_def(device, table_name)
assert(def, string.format("table_helpers: unknown table %q", tostring(table_name)))
local t = table_utils.get_state(device, def) or {}
local occupied = {}
for _, entry in ipairs(t) do occupied[entry[def.match_key]] = true end
local idx = start or def.minimum
local idx = def.minimum
while occupied[idx] do idx = idx + 1 end
return idx
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@ local ResponseStatus = DoorLock.types.DrlkPassFailStatus
local mock_device = test.mock_device.build_test_zigbee_device({
profile = t_utils.get_profile_definition("base-lock.yml"),
})
local mock_device_yale = test.mock_device.build_test_zigbee_device({
profile = t_utils.get_profile_definition("base-lock.yml"),
zigbee_endpoints = {
[1] = {
id = 1,
manufacturer = "Yale",
model = "YRD256",
server_clusters = { DoorLock.ID },
},
},
})
zigbee_test_utils.prepare_zigbee_env_info()

-- ── helpers ────────────────────────────────────────────────────────────────

local function test_init()
test.mock_device.add_test_device(mock_device)
test.mock_device.add_test_device(mock_device_yale)
end
test.set_test_init_function(test_init)

Expand Down Expand Up @@ -73,14 +85,14 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 1, "guest", "pin", "1234" } },
args = { 0, "guest", "pin", "1234" } },
})

-- Expect SetPINCode to be sent to the lock
test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
})
test.wait_for_events()

Expand All @@ -94,7 +106,7 @@ test.register_coroutine_test(
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.credentials(
{ { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } },
{ { userIndex = 1, credentialIndex = 0, credentialType = "pin", credentialName = "Guest 1" } },
{ visibility = { displayed = false } }
))
)
Expand All @@ -109,7 +121,7 @@ test.register_coroutine_test(
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.commandResult(
{ commandName = "addCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 },
{ commandName = "addCredential", statusCode = "success", userIndex = 1, credentialIndex = 0 },
{ state_change = true, visibility = { displayed = false } }
))
)
Expand All @@ -118,19 +130,46 @@ test.register_coroutine_test(
)

test.register_coroutine_test(
"addCredential: auto-assign (userIndex 0) skips credential slot 0, which is reserved for the master code",
"addCredential: creates the user with the userType given in the command",
function()
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 0, "guest", "pin", "1234" } },
args = { 0, "adminMember", "pin", "1234" } },
})
test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
})
test.wait_for_events()

test.socket.zigbee:__queue_receive({
mock_device.id,
DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS),
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.credentials(
{ { userIndex = 1, credentialIndex = 0, credentialType = "pin", credentialName = "Guest 1" } },
{ visibility = { displayed = false } }
))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockUsers.users(
{ { userIndex = 1, userName = "Guest 1", userType = "adminMember" } },
{ visibility = { displayed = false } }
))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.commandResult(
{ commandName = "addCredential", statusCode = "success", userIndex = 1, credentialIndex = 0 },
{ state_change = true, visibility = { displayed = false } }
))
)
test.wait_for_events()
end
)

Expand All @@ -140,13 +179,13 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 1, "guest", "pin", "1234" } },
args = { 0, "guest", "pin", "1234" } },
})

test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
})
test.wait_for_events()

Expand All @@ -172,13 +211,13 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 2, "guest", "pin", "5678" } },
args = { 0, "guest", "pin", "5678" } },
})

test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
2, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "5678"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "5678"),
})
test.wait_for_events()

Expand All @@ -204,13 +243,13 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 1, "guest", "pin", "1234" } },
args = { 0, "guest", "pin", "1234" } },
})

test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
})
test.wait_for_events()

Expand All @@ -237,20 +276,20 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 1, "guest", "pin", "1234" } },
args = { 0, "guest", "pin", "1234" } },
})
test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
})
test.wait_for_events()

-- Second addCredential while first is still pending → should get busy
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 2, "guest", "pin", "5678" } },
args = { 0, "guest", "pin", "5678" } },
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
Expand Down Expand Up @@ -706,11 +745,85 @@ test.register_coroutine_test(
)

-- ============================================================================
-- Master codes: known master codes keep their type on update
-- Master codes: addCredential accepts the master userType; known master codes keep their type
-- ============================================================================

local MASTER_USER = { userIndex = 1, userName = "Master Code", userType = "programmingUser" }

test.register_coroutine_test(
"addCredential: the master userType is sent to the lock as MASTER_USER",
function()
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 0, "programmingUser", "pin", "1234" } },
})
test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.MASTER_USER, "1234"),
})
test.wait_for_events()
end
)

test.register_coroutine_test(
"addCredential: rejects a requested credential slot that already holds a master code",
function()
seed_users({ MASTER_USER })
seed_credentials({ { userIndex = 1, credentialIndex = 4, credentialType = "pin", credentialName = "Master Code" } })
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 4, "guest", "pin", "1234" } },
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.commandResult(
{ commandName = "addCredential", statusCode = "occupied" },
{ state_change = true, visibility = { displayed = false } }
))
)
test.wait_for_events()
end
)

test.register_coroutine_test(
"addCredential: a requested credential slot beyond pinUsersSupported is still sent to the lock",
function()
local max = mock_device:get_latest_state("main", capabilities.lockCredentials.ID,
capabilities.lockCredentials.pinUsersSupported.NAME, 20)
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { max, "guest", "pin", "1234" } },
})
test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
max, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
})
test.wait_for_events()
end
)

test.register_coroutine_test(
"addCredential: a requested free credential slot is used, with the user in the next free user slot",
function()
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 5, "guest", "pin", "1234" } },
})
test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
5, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"),
})
test.wait_for_events()
end
)

test.register_coroutine_test(
"updateCredential: a known master code is sent as MASTER_USER, based on the credential's stored user",
function()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,13 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 3, "guest", "pin", "pin03" } },
args = { 0, "guest", "pin", "pin03" } },
})

test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin03"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin03"),
})
test.wait_for_events()

Expand All @@ -264,7 +264,7 @@ test.register_coroutine_test(
{
{ userIndex = 1, credentialIndex = 1, credentialType = "pin" },
{ userIndex = 2, credentialIndex = 2, credentialType = "pin" },
{ userIndex = 3, credentialIndex = 3, credentialType = "pin", credentialName = "Guest 3" },
{ userIndex = 3, credentialIndex = 0, credentialType = "pin", credentialName = "Guest 3" },
},
{ visibility = { displayed = false } }
))
Expand All @@ -283,7 +283,7 @@ test.register_coroutine_test(
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.commandResult(
{ commandName = "addCredential", statusCode = "success", userIndex = 3, credentialIndex = 3 },
{ commandName = "addCredential", statusCode = "success", userIndex = 3, credentialIndex = 0 },
{ state_change = true, visibility = { displayed = false } }
))
)
Expand All @@ -299,13 +299,13 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 3, "guest", "pin", "pin01" } },
args = { 0, "guest", "pin", "pin01" } },
})

test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin01"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin01"),
})
test.wait_for_events()

Expand Down Expand Up @@ -333,13 +333,13 @@ test.register_coroutine_test(
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = capabilities.lockCredentials.ID, command = "addCredential",
args = { 3, "guest", "pin", "pin03" } },
args = { 0, "guest", "pin", "pin03" } },
})

test.socket.zigbee:__expect_send({
mock_device.id,
DoorLock.server.commands.SetPINCode(mock_device,
3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin03"),
0, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin03"),
})
test.wait_for_events()

Expand Down
Loading
Loading