From 62d9d9ca3e1500e5978bfb76b1322be785568358 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 17 Jul 2026 12:05:08 -0500 Subject: [PATCH] Zwave Lock: force migration revert on init --- drivers/SmartThings/zwave-lock/src/init.lua | 26 +++++- .../src/test/test_init_lifecycle_handlers.lua | 20 ----- .../src/test/test_lock_users_commands.lua | 89 +++++++++++++++++++ 3 files changed, 112 insertions(+), 23 deletions(-) diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index b49489a008..8463cb8190 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -34,6 +34,28 @@ function LockLifecycle.device_added(driver, device) }) end +local function revert_migration(driver, device) + local json = require "st.json" + + local latest_users = table_utils.get_state(device, "users") or {} + print("Latest users: ", #latest_users) + local lock_codes = {} + for _, user in ipairs(latest_users) do + lock_codes[string.format("%s", user.userIndex)] = user.userName or ("Code " .. user.userIndex) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_codes), { visibility = { displayed = false } })) + local lock_codes_key = "_lock_codes" + device:set_field(lock_codes_key, lock_codes, { persist = true }) + + local max_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) + if max_code_length then + device:emit_event(capabilities.lockCodes.codeLength(max_code_length, { visibility = { displayed = false } })) + end + + device:emit_event(capabilities.lockCodes.migrated(false, { visibility = { displayed = false } })) + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, nil, { persist = true }) -- persist the un-migrated state to the datastore +end + function LockLifecycle.init(driver, device) -- Restore users/credentials capability state from the persistent store in case -- the capability state cache was wiped since the last driver run. @@ -41,9 +63,7 @@ function LockLifecycle.init(driver, device) local lock_pins_supported_by_profile = device:supports_capability(capabilities.lockCodes) if lock_pins_supported_by_profile and device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) == true then - -- ensure lockCodes capability state is reflected correctly for already migrated devices - device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockCredentials.supportedCredentials({ consts.CRED_TYPE_PIN }, { visibility = { displayed = false } })) + revert_migration(driver, device) end if device:supports_capability(capabilities.tamperAlert) then diff --git a/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua index d521e2d3a0..ca1909ef0d 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua @@ -10,7 +10,6 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" -local capabilities = require "st.capabilities" local zw = require "st.zwave" local constants = require "lock_utils.constants" @@ -91,25 +90,6 @@ test.register_coroutine_test( -- init (LockLifecycle.init) -- ============================================================================ -test.register_coroutine_test( - "init: device with lockCodes and SLGA_MIGRATED=true emits migrated + supportedCredentials", - function() - mock_device_base:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) - - test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "init" }) - test.socket.capability:__expect_send( - mock_device_base:generate_test_message("main", - capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - ) - test.socket.capability:__expect_send( - mock_device_base:generate_test_message("main", - capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) - ) - test.wait_for_events() - end, - { test_init = make_test_init(mock_device_base) } -) - test.register_coroutine_test( "init: device with lockCodes but SLGA_MIGRATED not set does nothing", function() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua index da39c8c692..3f12f50d35 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua @@ -8,6 +8,7 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local zw = require "st.zwave" +local st_utils = require "st.utils" local table_utils = require "lock_utils.tables" local constants = require "lock_utils.constants" local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) @@ -702,4 +703,92 @@ test.register_coroutine_test( end ) +-- ============================================================================ +-- revert Migration on init +-- ============================================================================ + +test.register_coroutine_test( + "Revert Migration on init, after added", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 5, userName = "Charlie", userType = "guest" }, + }) + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 5, credentialIndex = 5, credentialType = "pin" }, + }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.capability:__set_channel_ordering("relaxed") + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 5, userName = "Charlie", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 5, credentialIndex = 5, credentialType = "pin" }, + }, + { visibility = { displayed = false } } + )) + ) + + -- Reversion of Migration should be handled for the device on initialization + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({ + ["1"] = "Alice", ["2"] = "Bob", ["5"] = "Charlie" + }), { visibility = { displayed = false } }) + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.migrated(false, { visibility = { displayed = false } }) + ) + ) + test.wait_for_events() + assert(mock_device:get_field(constants.SLGA_MIGRATED) == nil, "Device should not be marked as migrated") + local stored_codes = st_utils.deep_copy(mock_device:get_field("_lock_codes")) + assert(stored_codes["1"] == "Alice") + assert(stored_codes["2"] == "Bob") + assert(stored_codes["5"] == "Charlie") + + test.wait_for_events() + + -- ensure codeChanged now triggers correctly after setting a new code + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 3, "1234", "test" } } }) + test.socket.zwave:__expect_send(UserCode:Set({user_identifier = 3, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}):build_test_tx(mock_device.id) ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 3, user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({ + ["1"] = "Alice", ["2"] = "Bob", ["3"] = "test", ["5"] = "Charlie" + }), { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("3 set", { data = { codeName = "test"}, state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + test.run_registered_tests()