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
33 changes: 29 additions & 4 deletions drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ end
local function migrate(driver, device, cmd)
local LockCodesDefaults = require "st.zwave.defaults.lockCodes"
local table_utils = require "lock_utils.tables"
local consts = require("lock_utils.constants")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local consts = require("lock_utils.constants")
local consts = require "lock_utils.constants"

nit: it's like this elsewhere in this file

local get_lock_codes = LockCodesDefaults.get_lock_codes
local lock_users = {}
local lock_credentials = {}
Expand All @@ -74,10 +75,23 @@ local function migrate(driver, device, cmd)
end

table.sort(ordered_codes, function(a, b) return a.index < b.index end)
local admin_index = table_utils.admin_credential_index(device)
-- userIndex is assigned sequentially from 1 so every user is valid and unique, except that the admin code's
-- user takes the admin code slot as its index.
local user_index = 0
for _, code in ipairs(ordered_codes) do
local user_index = #lock_users + 1
table.insert(lock_users, {userIndex = user_index, userType = "guest", userName = lock_codes[code.slot]})
table.insert(lock_credentials, {userIndex = user_index, credentialIndex = code.index, credentialType = "pin"})
local is_admin = admin_index ~= nil and code.index == admin_index
if not is_admin then
user_index = user_index + 1
if admin_index ~= nil and user_index == admin_index then user_index = user_index + 1 end
end
local owner_index = is_admin and code.index or user_index
table.insert(lock_users, {
userIndex = owner_index,
userType = is_admin and consts.ADMIN.USER_TYPE or "guest",
userName = lock_codes[code.slot],
})
table.insert(lock_credentials, {userIndex = owner_index, credentialIndex = code.index, credentialType = "pin"})
end
device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } }))
Expand All @@ -99,9 +113,20 @@ local function migrate(driver, device, cmd)

device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))
local consts = require("lock_utils.constants")

device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) -- persist the migrated state to the datastore

-- lockCodes only holds the admin code if SmartThings set it and the lock confirmed it as a user code. A factory
-- admin code, or one a Yale ZW3 confirmed with the "master code changed" notification, is read back instead.
if admin_index ~= nil then
local admin_migrated = false
for _, code in ipairs(ordered_codes) do
if code.index == admin_index then admin_migrated = true end
end
if not admin_migrated then
require("lock_utils.utils").read_admin_code(device)
end
end
end

local legacy_capabilities = {
Expand Down
50 changes: 35 additions & 15 deletions drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function CapabilityHandlers.add_credential(driver, device, command)
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

if #(tables.get_state(device, "credentials") or {}) >= tables.get_max_entries(device, "credentials") then
if tables.is_full(device, "credentials") then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.RESOURCE_EXHAUSTED)
return
end
Expand Down Expand Up @@ -152,24 +152,49 @@ function CapabilityHandlers.update_credential(driver, device, command)
return
end

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

credential index 0 will be dropped out of hand at the top of this handler, so this code can't be hit. This makes all the changes in the updateCredential handler here moot, I think.

-- user identifier 0 addresses every user code on the lock, never a single credential
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,
credentialIndex = credential_index,
credentialType = command.args.credentialType,
credentialName = command.args.credentialName,
})
device:send(UserCode:Set({
user_identifier = command.args.credentialIndex,
user_identifier = credential_index,
user_code = command.args.credentialData,
user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS
}))
end

-- Remove a credential from the lock. The busy state must already be set.
local function send_credential_delete(device, credential_index)
if credential_index == 0 then
-- user identifier 0 addresses every user code on the lock, so a stale entry is only removed locally
lock_utils.delete_credential_report_helper(device, credential_index)
else
device:send(UserCode:Set({
user_identifier = credential_index,
user_id_status = UserCode.user_id_status.AVAILABLE
}))
-- A lock that refuses the delete (e.g. a Yale admin code) ignores the Set and sends nothing, so read the
-- slot back if the delete has not completed. The report tells us whether the code is still there.
device.thread:call_with_delay(consts.DELETE_CONFIRM_DELAY, function()
if lock_utils.is_delete_in_progress(device, credential_index) then
device:send(UserCode:Get({ user_identifier = credential_index }))
end
end)
end
end

function CapabilityHandlers.delete_credential(driver, device, command)
-- Index 0 is the Z-Wave "all users" wildcard; clearing everything must go through deleteAllCredentials.
if command.args.credentialIndex == 0 then
Expand All @@ -182,10 +207,7 @@ function CapabilityHandlers.delete_credential(driver, device, command)
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 {}
device:send(UserCode:Set({
user_identifier = credential_args.credentialIndex,
user_id_status = UserCode.user_id_status.AVAILABLE
}))
send_credential_delete(device, credential_args.credentialIndex)
elseif lock_utils.is_device_busy(device) then
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.BUSY)
else
Expand All @@ -200,29 +222,27 @@ function CapabilityHandlers.delete_credential(driver, device, command)
credentialType = command.args.credentialType,
userIndex = found_cred.userIndex,
})
device:send(UserCode:Set({
user_identifier = command.args.credentialIndex,
user_id_status = UserCode.user_id_status.AVAILABLE
}))
send_credential_delete(device, command.args.credentialIndex)
end
end

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

local send_clear_commands = function()
-- User Identifier 0 with status AVAILABLE tells the lock to clear every user code slot
-- User Identifier 0 with status AVAILABLE tells the lock to clear every user code slot. A lock with a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- User Identifier 0 with status AVAILABLE tells the lock to clear every user code slot. A lock with a
-- User Identifier 0 with status AVAILABLE tells the lock to clear every user code slot. A lock with an

-- admin code (Yale) keeps it, so the admin entries are kept in state below.
device:send(UserCode:Set({
user_identifier = 0,
user_id_status = UserCode.user_id_status.AVAILABLE
}))
-- Delayed cleanup: ensure all tables are cleared after the lock has processed the command
device.thread:call_with_delay(4, function()
if cmd_in_progress == consts.LOCK_USERS.DELETE_ALL then
tables.delete_all_entries(device, "users")
lock_utils.delete_all_non_admin_entries(device, "users")
lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE_ALL, consts.COMMAND_RESULT.SUCCESS)
end
tables.delete_all_entries(device, "credentials")
lock_utils.delete_all_non_admin_entries(device, "credentials")
lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE_ALL, consts.COMMAND_RESULT.SUCCESS)
lock_utils.clear_busy_state(device)
end)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,23 @@ function ZwaveHandlers.user_code_report(driver, device, cmd)

if command_in_progress == consts.SYNC.CODES_FROM_LOCK then
if user_code_occupied then
-- If an entry already exists at this index, this will be a no-op.
tables.add_entry(device, "users", {
userIndex = credential_index,
userName = "Guest " .. credential_index,
userType = "guest",
})
tables.add_entry(device, "credentials", {
userIndex = credential_index,
credentialIndex = credential_index,
credentialType = consts.CRED_TYPE_PIN,
credentialName = "Guest " .. credential_index,
})
-- If a credential already exists at this index, this will be a no-op.
if not tables.find_entry(device, "credentials", credential_index) then
-- Prefer the matching userIndex, but never attach this credential to a different, existing user.
local user_index = tables.find_entry(device, "users", credential_index) and
tables.next_index(device, "users") or credential_index
tables.add_entry(device, "users", {
userIndex = user_index,
userName = "Guest " .. user_index,
userType = "guest",
})
tables.add_entry(device, "credentials", {
userIndex = user_index,
credentialIndex = credential_index,
credentialType = consts.CRED_TYPE_PIN,
credentialName = "Guest " .. user_index,
})
end
-- reset the consecutive unoccupied codes counter since we found an occupied code
device:set_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES, 0)
elseif user_code_available then
Expand All @@ -69,6 +74,9 @@ function ZwaveHandlers.user_code_report(driver, device, cmd)
device:send(UserCode:Get({user_identifier = synced_code_index}))
end
return
elseif user_code_occupied and lock_utils.is_delete_in_progress(device, credential_index) then
-- the code is still on the lock after a delete, so the lock refused it
lock_utils.emit_delete_failure(device)
elseif user_code_occupied then
-- Code slot is now occupied: add or update credential and associated user
lock_utils.set_credential_report_helper(device, credential_index)
Expand Down Expand Up @@ -108,8 +116,9 @@ function ZwaveHandlers.user_code_event_handler(driver, device, cmd)
local access_control_event = Notification.event.access_control

if event == access_control_event.ALL_USER_CODES_DELETED then
tables.delete_all_entries(device, "credentials")
tables.delete_all_entries(device, "users")
-- Deleting all user codes does not affect the admin code.
lock_utils.delete_all_non_admin_entries(device, "credentials")
lock_utils.delete_all_non_admin_entries(device, "users")

elseif event == access_control_event.SINGLE_USER_CODE_DELETED then
lock_utils.delete_credential_report_helper(device, credential_index)
Expand Down
11 changes: 11 additions & 0 deletions drivers/SmartThings/zwave-lock/src/lock_utils/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ lock_constants.SYNC = {
CONSECUTIVE_UNOCCUPIED_CODES = "consecutiveUnoccupiedCodes",
}

lock_constants.ADMIN = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should be in Yale-specific handling?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 00d8eaf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you? Maybe I'm confused or did not explain myself thoroughly- I think things like this: " -- Yale locks keep their admin code in this user code slot.
YALE_SLOT = 251,
YALE_MANUFACTURER_ID = 0x0129," are yale-specific, so should stay in a Yale subdriver.

-- Yale locks keep their admin code in this user code slot.
YALE_SLOT = 251,
YALE_MANUFACTURER_ID = 0x0129,
-- The lockUsers userType for the admin code.
USER_TYPE = "programmingUser",
NAME = "Master Code",
}

lock_constants.COMMAND_RESULT = {
SUCCESS = "success",
FAILURE = "failure",
Expand Down Expand Up @@ -44,5 +53,7 @@ lock_constants.CRED_TYPE_PIN = "pin"
lock_constants.DELAY_LOCK_EVENT = "_delay_lock_event"
lock_constants.DELAY_LOCK_EVENT_TIMER = "_delay_lock_event_timer"
lock_constants.MAX_DELAY = 10
-- Seconds to wait for a delete to be confirmed before reading the slot back
lock_constants.DELETE_CONFIRM_DELAY = 4.2

return lock_constants
52 changes: 44 additions & 8 deletions drivers/SmartThings/zwave-lock/src/lock_utils/tables.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

local capabilities = require "st.capabilities"
local st_utils = require "st.utils"
local COMMAND_RESULT = require "lock_utils.constants".COMMAND_RESULT
local consts = require "lock_utils.constants"
local COMMAND_RESULT = consts.COMMAND_RESULT

local table_utils = {}

Expand Down Expand Up @@ -41,6 +42,14 @@ local DEFS = {
}
}

-- Returns the credentialIndex of the device's admin code slot, or nil if the device does not have one.
-- Only Yale locks have one.
function table_utils.admin_credential_index(device)
if device.zwave_manufacturer_id == consts.ADMIN.YALE_MANUFACTURER_ID then
return consts.ADMIN.YALE_SLOT
end
end

-- Resolve a table name to its definition. Logs an error and returns nil if unknown.
local function resolve_table_def(device, table_name)
local def = DEFS[table_name]
Expand Down Expand Up @@ -156,19 +165,39 @@ function table_utils.get_max_entries(device, table_name)
return device:get_latest_state("main", def.capability.ID, def.max_entries.NAME, 20) -- arbitrary, default to 20 if the attribute is missing
end

-- Return true if the named table has reached its max_entries limit. The admin code credential, and the
-- user that owns it, are not one of the lock's user slots, so they do not count toward the limit.
function table_utils.is_full(device, table_name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than do it like this, I recommend we increase the "max number of credentials" allowed by 1, in lock step with each admin credential. That, or we create a separate DEF that works on admin credentials, and follows distinct logic from the normal credentials flow.

local def = resolve_table_def(device, table_name)
if not def then return true end
local admin_index = table_utils.admin_credential_index(device)
local admin_cred = admin_index and table_utils.find_entry(device, "credentials", admin_index)
local count = 0
for _, entry in ipairs(table_utils.get_state(device, def) or {}) do
local is_admin
if table_name == "credentials" then
is_admin = admin_index ~= nil and entry.credentialIndex == admin_index
else
is_admin = admin_cred ~= nil and entry.userIndex == admin_cred.userIndex
end
if not is_admin then count = count + 1 end
end
return count >= table_utils.get_max_entries(device, table_name)
end

-- Add an entry to a named table. The entry must satisfy all required_keys for
-- that table. An entry whose match_key value already exists in the
-- table is skipped to prevent duplicates. If the table has a max_entries limit,
-- entries that exceed the limit are not added.
function table_utils.add_entry(device, table_name, entry)
-- entries that exceed the limit are not added, unless exempt_from_limit is true (used for the admin code).
function table_utils.add_entry(device, table_name, entry, exempt_from_limit)
device.log.debug("table_helpers: attempting to add entry " .. st_utils.stringify_table(entry) .. " to " .. table_name)
local def = resolve_table_def(device, table_name)
if not def then return COMMAND_RESULT.FAILURE end
if not validate_entry(device, entry, def.required_keys) then return COMMAND_RESULT.FAILURE end
local t = table_utils.get_state(device, def)
if not t then return COMMAND_RESULT.FAILURE end

if #t >= table_utils.get_max_entries(device, table_name) then
if not exempt_from_limit and table_utils.is_full(device, table_name) then
device.log.warn(string.format(
"table_helpers: cannot add entry to %q, max entries reached", table_name
))
Expand Down Expand Up @@ -247,13 +276,20 @@ function table_utils.delete_entry(device, table_name, matcher)
return COMMAND_RESULT.FAILURE
end

-- Delete all entries from a table.
function table_utils.delete_all_entries(device, table_name)
-- Delete all entries from a table. If keep is given, entries for which keep(entry)
-- returns true are retained.
function table_utils.delete_all_entries(device, table_name, keep)
device.log.debug("table_helpers: attempting to delete all entries from " .. table_name)
local def = resolve_table_def(device, table_name)
if not def then return COMMAND_RESULT.FAILURE end
device:emit_event(def.attribute({}, {visibility = {displayed = false}}))
persist_table(device, def, {})
local kept = {}
if keep then
for _, entry in ipairs(table_utils.get_state(device, def) or {}) do
if keep(entry) then table.insert(kept, entry) end
end
end
device:emit_event(def.attribute(kept, {visibility = {displayed = false}}))
persist_table(device, def, kept)
return COMMAND_RESULT.SUCCESS
end

Expand Down
Loading
Loading