Repository navigation
Add support for admin code for Yale Z-Wave lock #3312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
93352db
c672102
975d1c2
6c5042d
7a2c6f7
ecdb027
500b881
00d8eaf
cc51717
0fe08ac
cfd0e6a
4819081
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
|
@@ -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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
|
|
@@ -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 | ||||||
|
|
@@ -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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| -- 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) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,15 @@ lock_constants.SYNC = { | |
| CONSECUTIVE_UNOCCUPIED_CODES = "consecutiveUnoccupiedCodes", | ||
| } | ||
|
|
||
| lock_constants.ADMIN = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps this should be in Yale-specific handling?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 00d8eaf
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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", | ||
|
|
@@ -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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = {} | ||
|
|
||
|
|
@@ -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] | ||
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| )) | ||
|
|
@@ -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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it's like this elsewhere in this file