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
Open
ctowns
wants to merge
16
commits into
main
Choose a base branch
from
zwave-admin-code
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
b840e84
Z-Wave Lock: Let the lock decide whether a credential can be deleted
ctowns 9b51100
Z-Wave Lock: Add generic support for a lock admin code
ctowns 8e61ee2
Z-Wave Lock: Add the Yale admin code sub-driver
ctowns 7204fe2
Z-Wave Lock: Migrate the Yale admin code to a programmingUser user
ctowns 261bb53
Z-Wave Lock: Rename the admin/master code to programming code
ctowns 5c6c5de
Z-Wave Lock: Do not allow addUser, addCredential or updateUser to cre…
ctowns 987a575
Z-Wave Lock: Simplify deleteCredential and ignore reported credential…
ctowns 9a15c12
Z-Wave Lock: Do not allow the programming user to be deleted
ctowns 135387b
Z-Wave Lock: Do not let a programming code report move a Yale sync on
ctowns 6cadd52
Z-Wave Lock: Rely on the sync to populate the Yale programming code
ctowns edd04fc
Fix typo
ctowns 23aa5b5
Gate new tests at min api 15 to match existing tests
ctowns ca8521e
Z-Wave Lock: Expect default pin lengths in the Yale migration tests
ctowns 672f044
Z-Wave Lock: Remove a stale delete timer comment
ctowns bf29421
Z-Wave Lock: Simplify updateUser and deleting all non-programming ent…
ctowns 8aba119
Z-Wave Lock: Do not count programming users toward the entry limit by…
ctowns File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
drivers/SmartThings/zwave-lock/src/legacy-handlers/migrate.lua
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| -- Copyright 2026 SmartThings, Inc. | ||
| -- Licensed under the Apache License, Version 2.0 | ||
|
|
||
| local capabilities = require "st.capabilities" | ||
| local LockCodesDefaults = require "st.zwave.defaults.lockCodes" | ||
|
|
||
| local consts = require "lock_utils.constants" | ||
| local table_utils = require "lock_utils.tables" | ||
|
|
||
| -- Migrate the lock's lockCodes state to lockUsers and lockCredentials. This is shared by the lockCodes.migrate | ||
| -- handlers of the legacy sub-drivers, which pass the programming code slot for a lock that has one. | ||
| --- @param driver st.zwave.Driver | ||
| --- @param device st.zwave.Device | ||
| --- @param programming_code_slot number|nil the credentialIndex of the lock's programming code slot, or nil if it has none | ||
| local function migrate_codes(driver, device, programming_code_slot) | ||
| local get_lock_codes = LockCodesDefaults.get_lock_codes | ||
| local lock_users = {} | ||
| local lock_credentials = {} | ||
| local lock_codes = get_lock_codes(device) | ||
| local ordered_codes = {} | ||
|
|
||
| -- Slots that are not valid credential indices (e.g. 0, which means "all users" in Z-Wave, or non-numeric) | ||
| -- are dropped rather than migrated. | ||
| local min_credential = table_utils.get_minimum(device, "credentials") | ||
| for code_slot in pairs(lock_codes) do | ||
| local credential_index = tonumber(code_slot) | ||
| if math.type(credential_index) == "integer" and credential_index >= min_credential then | ||
| table.insert(ordered_codes, {slot = code_slot, index = credential_index}) | ||
| else | ||
| device.log.warn(string.format("Dropping lock code slot %q during migration: not a valid credential index", tostring(code_slot))) | ||
| end | ||
| end | ||
|
|
||
| table.sort(ordered_codes, function(a, b) return a.index < b.index end) | ||
| -- userIndex is assigned sequentially from 1 so every user is valid and unique, except that the programming code's | ||
| -- user takes the programming code slot as its index. | ||
| local user_index = 0 | ||
| for _, code in ipairs(ordered_codes) do | ||
| local is_programming = programming_code_slot ~= nil and code.index == programming_code_slot | ||
| if not is_programming then | ||
| user_index = user_index + 1 | ||
| if programming_code_slot ~= nil and user_index == programming_code_slot then user_index = user_index + 1 end | ||
| end | ||
| local owner_index = is_programming and code.index or user_index | ||
| table.insert(lock_users, { | ||
| userIndex = owner_index, | ||
| userType = is_programming and consts.PROGRAMMING_USER.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 } })) | ||
|
|
||
| -- lockCodes tracks a single fixed code length, which only some locks (e.g. Schlage) report. | ||
| -- Without one, fall back to the Z-Wave spec default range so the bounds are always set. | ||
| local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) | ||
| local min_pin_length = code_length or consts.DEFAULT_MIN_PIN_LENGTH | ||
| local max_pin_length = code_length or consts.DEFAULT_MAX_PIN_LENGTH | ||
| device:emit_event(capabilities.lockCredentials.minPinCodeLen(min_pin_length, { visibility = { displayed = false } })) | ||
| device:emit_event(capabilities.lockCredentials.maxPinCodeLen(max_pin_length, { visibility = { displayed = false } })) | ||
|
|
||
| local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) | ||
| if max_codes then | ||
| device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) | ||
| device:emit_event(capabilities.lockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) | ||
| end | ||
|
|
||
| device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) | ||
| device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) | ||
|
|
||
| device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) -- persist the migrated state to the datastore | ||
|
|
||
| driver:inject_capability_command(device, { | ||
| capability = capabilities.refresh.ID, | ||
| command = capabilities.refresh.commands.refresh.NAME, | ||
| args = {} | ||
| }) | ||
| end | ||
|
|
||
| return migrate_codes |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
drivers/SmartThings/zwave-lock/src/legacy-handlers/yale-lock/can_handle.lua
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| -- Copyright 2026 SmartThings, Inc. | ||
| -- Licensed under the Apache License, Version 2.0 | ||
|
|
||
| return function(opts, driver, device, ...) | ||
| local yale_programming_code = require "yale-lock.programming_code" | ||
| if device.zwave_manufacturer_id == yale_programming_code.MANUFACTURER_ID then | ||
| return true, require("legacy-handlers.yale-lock") | ||
| end | ||
| return false | ||
| end |
34 changes: 34 additions & 0 deletions
34
drivers/SmartThings/zwave-lock/src/legacy-handlers/yale-lock/init.lua
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| -- Copyright 2026 SmartThings, Inc. | ||
| -- Licensed under the Apache License, Version 2.0 | ||
|
|
||
| local capabilities = require "st.capabilities" | ||
| local LockCodesDefaults = require "st.zwave.defaults.lockCodes" | ||
|
|
||
| local migrate_codes = require "legacy-handlers.migrate" | ||
| local yale_programming_code = require "yale-lock.programming_code" | ||
|
|
||
| -- Migrate a Yale lock, mapping its programming code slot to the programming code user. | ||
| local function yale_migrate(driver, device, cmd) | ||
| migrate_codes(driver, device, yale_programming_code.SLOT) | ||
|
|
||
| -- lockCodes only holds the programming code if SmartThings set it and the lock confirmed it as a user code. A factory | ||
| -- programming code, or one a Yale ZW3 confirmed with the "programming code changed" notification, is read back instead. | ||
| -- This is because the legacy handlers never read the programming code explicitly. Furthermore, if the lock was | ||
| -- running the newer ZW3 module, the legacy driver did not have the corresponding handling for saving | ||
| -- a programming code after a "Programming Code Changed" response. | ||
| if LockCodesDefaults.get_lock_codes(device)[tostring(yale_programming_code.SLOT)] == nil then | ||
| yale_programming_code.read_programming_code(device) | ||
| end | ||
| end | ||
|
|
||
| local legacy_yale_lock = { | ||
| capability_handlers = { | ||
| [capabilities.lockCodes.ID] = { | ||
| [capabilities.lockCodes.commands.migrate.NAME] = yale_migrate | ||
| }, | ||
| }, | ||
| NAME = "Legacy Yale Lock", | ||
| can_handle = require("legacy-handlers.yale-lock.can_handle"), | ||
| } | ||
|
|
||
| return legacy_yale_lock | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I wonder how this is going to interact with the sync logic, which is also doing the same get, but expecting particular data. I suppose it is getting caught at the yale-subdriver, but I wonder if this won't end up causing us to skip an index. I guess it probably won't because the sync doesn't start for 15s, but worth a callout I think
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.
I added a little extra handling to prevent us from skipping an index as you pointed out - commit a5b7914 covers this case!