Skip to content
Open
14 changes: 9 additions & 5 deletions drivers/SmartThings/zigbee-lock/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ function LockLifecycle.init(driver, device)
-- We should check its PIN users if it should be re-profiled.
device:send(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(device))
end

-- retry sync on each driver init until it has been run to completion, in case of some kind of interruption (network, power, etc.)
-- ensure our user/credential state is accurate to the current device state
device.thread:call_with_delay(15, function() lock_utils.sync_device_state(device) end)
end

function LockLifecycle.do_configure(driver, device)
Expand All @@ -57,11 +61,6 @@ function LockLifecycle.do_configure(driver, device)

device:send(device_management.build_bind_request(device, clusters.Alarms.ID, driver.environment_info.hub_zigbee_eui))
device:send(clusters.Alarms.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0))

if device:supports_capability(capabilities.lockCredentials) then
-- ensure our user/credential state is accurate to the current device state
device.thread:call_with_delay(15, function() lock_utils.sync_device_state(device) end)
end
end

function LockLifecycle.info_changed(driver, device, event, args)
Expand Down Expand Up @@ -100,6 +99,11 @@ local zigbee_lock_driver = {
infoChanged = LockLifecycle.info_changed,
},
zigbee_handlers = {
global = {
[clusters.DoorLock.ID] = {
[0x0B] = zigbee_handlers.default_response, -- ZCL Default Response
}
},
cluster = {
[clusters.Alarms.ID] = {
[clusters.Alarms.client.commands.Alarm.ID] = zigbee_handlers.alarm
Expand Down
16 changes: 13 additions & 3 deletions drivers/SmartThings/zigbee-lock/src/legacy-handlers/migrate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ local LockUsers = capabilities.lockUsers
local LockCredentials = capabilities.lockCredentials

-- Migrate lockCodes state to lockUsers/lockCredentials.
return function(driver, device, command)
-- `programming_code_slot` is the user_id, if any, known to hold a master code on this device. It is migrated
-- as a programmingUser user rather than a guest.
return function(driver, device, command, programming_code_slot)
-- set supported credentials
device:emit_event(LockCredentials.supportedCredentials({ post_migration_consts.CRED_TYPE_PIN }, { visibility = { displayed = false } }))

Expand Down Expand Up @@ -43,14 +45,21 @@ return function(driver, device, command)
for code_slot_str, _ in pairs(cached_lock_codes) do
local code_slot = tonumber(code_slot_str)
-- Slots that are not valid credential indices are dropped rather than migrated.
if math.type(code_slot) == "integer" and code_slot >= min_credential then
if code_slot and math.type(code_slot) == "integer" and code_slot >= min_credential then
table.insert(ordered_code_slots, code_slot)
if code_slot >= min_user then occupied_user_indices[code_slot] = true end
else
device.log.warn(string.format("migrate: dropping lockCodes slot %q, not a valid credential index", tostring(code_slot_str)))
end
end

-- A passed-in programming user code slot should always be migrated, even if it is not among the known codes.
if math.type(programming_code_slot) == "integer" and programming_code_slot >= min_credential
and cached_lock_codes[tostring(programming_code_slot)] == nil then
cached_lock_codes[tostring(programming_code_slot)] = post_migration_consts.PROGRAMMING_USER.DEFAULT_NAME
table.insert(ordered_code_slots, programming_code_slot)
if programming_code_slot >= min_user then occupied_user_indices[programming_code_slot] = true end
end
table.sort(ordered_code_slots)
local next_free_user_index = min_user
for _, code_slot in ipairs(ordered_code_slots) do
Expand All @@ -64,7 +73,8 @@ return function(driver, device, command)
occupied_user_indices[user_index] = true
end
-- cached_lock_codes is formatted as { ["1"] = "Guest 1", ["2"] = "Guest 2" }, etc
table.insert(users, { userIndex = user_index, userType = "guest", userName = code_name })
local user_type = code_slot == programming_code_slot and post_migration_consts.PROGRAMMING_USER.USER_TYPE or "guest"
table.insert(users, { userIndex = user_index, userType = user_type, userName = code_name })
table.insert(credentials, { userIndex = user_index, credentialIndex = code_slot, credentialType = post_migration_consts.CRED_TYPE_PIN, credentialName = code_name })
end
-- manually ensure user/cred state is persisted, then emit events to populate the new capabilities with the migrated data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- Licensed under the Apache License, Version 2.0

return function(opts, driver, device)
local fingerprints = require("yale-fingerprint-lock.fingerprints")
local fingerprints = require("yale.yale-fingerprint-lock.fingerprints")
for _, fingerprint in ipairs(fingerprints) do
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
local subdriver = require("legacy-handlers.yale-fingerprint-lock")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ local get_pin_response_handler = function(driver, device, zb_mess)
end
end

local migrate = function(driver, device, command)
require("legacy-handlers.migrate")(driver, device, command, 0)
end

local yale_door_lock_driver = {
NAME = "Yale Door Lock",
zigbee_handlers = {
Expand All @@ -139,7 +143,8 @@ local yale_door_lock_driver = {
capability_handlers = {
[LockCodes.ID] = {
[LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes,
[LockCodes.commands.setCode.NAME] = set_code
[LockCodes.commands.setCode.NAME] = set_code,
[LockCodes.commands.migrate.NAME] = migrate
}
},
can_handle = require("legacy-handlers.yale.can_handle")
Expand Down
69 changes: 63 additions & 6 deletions drivers/SmartThings/zigbee-lock/src/lock_handlers/capabilities.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ function CapabilityHandlers.add_user(driver, device, command)
return
end

-- programming users cannot be created through the capability
if command.args.userType == consts.PROGRAMMING_USER.USER_TYPE then
lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.ADD, consts.COMMAND_RESULT.INVALID_COMMAND)
return
end

-- Find the smallest positive userIndex not already in the table
local next_available_index = tables.next_index(device, "users")
local status = tables.add_entry(device, "users", {
Expand All @@ -52,6 +58,12 @@ function CapabilityHandlers.update_user(driver, device, command)
return
end

-- a user cannot be made a programming user, and a programming user cannot be changed to another type
if (command.args.userType == consts.PROGRAMMING_USER.USER_TYPE) ~= lock_utils.is_programming_user(device, command.args.userIndex) then
lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.UPDATE, consts.COMMAND_RESULT.INVALID_COMMAND)
return
end

local result_status
if not tables.find_entry(device, "users", command.args.userIndex) then
result_status = tables.add_entry(device, "users", {
Expand Down Expand Up @@ -127,15 +139,30 @@ function CapabilityHandlers.add_credential(driver, device, command)
return
end

-- A userIndex of 0 means "auto-assign the next available slot"
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") or command.args.userIndex
-- userIndex 0 means "create a new user for this credential" (its index is chosen when the lock confirms the
-- code). Any other userIndex must name an existing user that does not already have a credential (for now,
-- each user may have at most one credential).
-- A programming user cannot be created or given a credential through the capability.
local user_index = command.args.userIndex
if command.args.userType == consts.PROGRAMMING_USER.USER_TYPE
or user_index ~= 0 and (not tables.find_entry(device, "users", user_index)
or lock_utils.is_programming_user(device, user_index)
or tables.find_entry_by(device, "credentials", "userIndex", user_index)) then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.INVALID_COMMAND)
return
end

if #(tables.get_state(device, "credentials") or {}) >= tables.get_max_entries(device, "credentials") then
local credentials_full = #(tables.get_state(device, "credentials") or {}) >= tables.get_max_entries(device, "credentials")
-- if we are being asked to create a new user (index 0), this also needs a free user index.
local users_full = (user_index == 0) and #(tables.get_state(device, "users") or {}) >= tables.get_max_entries(device, "users")
if credentials_full or users_full then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.RESOURCE_EXHAUSTED)
return
end

-- the credential always takes the next available slot.
local cred_index = tables.next_index(device, "credentials", 1)

-- Set busy state and attempt to set the PIN on-device.
lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.ADD, {
userIndex = user_index,
Expand All @@ -157,11 +184,18 @@ function CapabilityHandlers.update_credential(driver, device, command)
return
end

if not tables.find_entry(device, "credentials", command.args.credentialIndex) then
local existing = tables.find_entry(device, "credentials", command.args.credentialIndex)
if not existing then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.FAILURE)
return
end

-- a credential cannot be moved to another user: the given userIndex must already own this credential
if existing.userIndex ~= command.args.userIndex then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.INVALID_COMMAND)
return
end

lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.UPDATE, {
userIndex = command.args.userIndex,
credentialIndex = command.args.credentialIndex,
Expand All @@ -171,17 +205,33 @@ function CapabilityHandlers.update_credential(driver, device, command)
device:send(LockCluster.server.commands.SetPINCode(device,
command.args.credentialIndex,
UserStatusEnum.OCCUPIED_ENABLED,
UserTypeEnum.UNRESTRICTED,
-- the credential's stored owner decides its type, so a known programming code stays a programming code
lock_utils.is_programming_user(device, existing.userIndex) and UserTypeEnum.MASTER_USER or UserTypeEnum.UNRESTRICTED,
command.args.credentialData)
)
end

-- Some locks report a successful ClearPINCode for a programming code but do not actually delete it,
-- so refuse rather than dropping the credential from our tables.
local function refuse_programming_credential_delete(device, injected_by_delete_user)
device.log.warn("ignoring deleteCredential for a programming code, it cannot be deleted")
if injected_by_delete_user then
lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, consts.COMMAND_RESULT.FAILURE)
lock_utils.clear_busy_state(device)
end
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.FAILURE)
end

function CapabilityHandlers.delete_credential(driver, device, command)
local cmd_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS)

if cmd_in_progress == consts.LOCK_USERS.DELETE then
-- Injected by deleteUser; busy state was already set with the full LOCK_USERS.DELETE context.
local credential_args = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) or {}
if lock_utils.is_programming_user(device, credential_args.userIndex) then
refuse_programming_credential_delete(device, true)
return
end
device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true))
device:send(LockCluster.server.commands.ClearPINCode(device, credential_args.credentialIndex))
elseif lock_utils.is_device_busy(device) then
Expand All @@ -193,6 +243,10 @@ function CapabilityHandlers.delete_credential(driver, device, command)
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.FAILURE)
return
end
if lock_utils.is_programming_user(device, found_cred.userIndex) then
refuse_programming_credential_delete(device, false)
return
end
lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.DELETE, {
credentialIndex = command.args.credentialIndex,
credentialType = command.args.credentialType,
Expand Down Expand Up @@ -237,6 +291,9 @@ function CapabilityHandlers.refresh(driver, device, cmd)
(device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then
device:send(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(device))
end

-- Allow callers to try to sync device state- fails silently if the device is busy or already synced.
device.thread:call_with_delay(15, function() lock_utils.sync_device_state(device) end)
elseif not device:supports_capability(capabilities.lockCodes) then
-- Generically fingerprinted devices may support PINs even though the profile does not yet
-- reflect it. Read NumberOfPINUsersSupported so the response handler can trigger a profile
Expand Down
Loading
Loading