Skip to content

Commit c248678

Browse files
CHAD-16365: SLGA: Add implementation for ZWave lockCodes:migrate command
1 parent 88d8d99 commit c248678

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

drivers/SmartThings/zwave-lock/src/init.lua

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,55 @@ local function update_codes(driver, device, cmd)
146146
end
147147
end
148148

149+
--- @param driver st.zwave.Driver
150+
--- @param device st.zwave.Device
151+
--- @param cmd table
152+
local function migrate(driver, device, cmd)
153+
local lock_users = {}
154+
local lock_credentials = {}
155+
local lc_data = json.decode(device.data.lockCodes)
156+
local lock_codes = {}
157+
local ordered_codes = {}
158+
for k, v in pairs(lc_data) do
159+
lock_codes[k] = v
160+
end
161+
162+
for code in pairs(lock_codes) do
163+
table.insert(ordered_codes, code)
164+
end
165+
166+
table.sort(ordered_codes)
167+
for index = 1, #ordered_codes do
168+
local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ]
169+
table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name})
170+
table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"})
171+
end
172+
173+
local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME)
174+
local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME)
175+
local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME)
176+
local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME)
177+
178+
if (min_code_len == nil) then
179+
min_code_len = 4 -- per ZWave spec
180+
end
181+
if (max_code_len == nil) then
182+
max_code_len = 10 -- per ZWave spec
183+
end
184+
if (code_length ~= nil) then
185+
max_code_len = code_length
186+
min_code_len = code_length
187+
end
188+
189+
device:emit_event(capabilities.lockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } }))
190+
device:emit_event(capabilities.lockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } }))
191+
device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } }))
192+
device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } }))
193+
device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))
194+
device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } }))
195+
device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))
196+
end
197+
149198
local function time_get_handler(driver, device, cmd)
150199
local time = os.date("*t")
151200
device:send_to_component(
@@ -162,6 +211,8 @@ local driver_template = {
162211
supported_capabilities = {
163212
capabilities.lock,
164213
capabilities.lockCodes,
214+
capabilities.lockUsers,
215+
capabilities.lockCredentials,
165216
capabilities.battery,
166217
capabilities.tamperAlert
167218
},
@@ -173,6 +224,9 @@ local driver_template = {
173224
[capabilities.lockCodes.ID] = {
174225
[capabilities.lockCodes.commands.updateCodes.NAME] = update_codes
175226
},
227+
[capabilities.lockCodes.ID] = {
228+
[capabilities.lockCodes.commands.migrate.NAME] = migrate
229+
},
176230
[capabilities.refresh.ID] = {
177231
[capabilities.refresh.commands.refresh.NAME] = do_refresh
178232
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
-- Mock out globals
16+
local test = require "integration_test"
17+
local capabilities = require "st.capabilities"
18+
local zw = require "st.zwave"
19+
--- @type st.zwave.CommandClass.DoorLock
20+
local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 })
21+
--- @type st.zwave.CommandClass.Battery
22+
local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 })
23+
--- @type st.zwave.CommandClass.UserCode
24+
local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 })
25+
local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 })
26+
local t_utils = require "integration_test.utils"
27+
local zw_test_utils = require "integration_test.zwave_test_utils"
28+
local utils = require "st.utils"
29+
local mock_datastore = require "integration_test.mock_env_datastore"
30+
local json = require "dkjson"
31+
32+
local SCHLAGE_MANUFACTURER_ID = 0x003B
33+
local SCHLAGE_PRODUCT_TYPE = 0x0002
34+
local SCHLAGE_PRODUCT_ID = 0x0469
35+
36+
local zwave_lock_endpoints = {
37+
{
38+
command_classes = {
39+
{ value = zw.BATTERY },
40+
{ value = zw.DOOR_LOCK },
41+
{ value = zw.USER_CODE },
42+
{ value = zw.NOTIFICATION }
43+
}
44+
}
45+
}
46+
47+
local lockCodes = {
48+
["1"] = "Zach",
49+
["5"] = "Steven"
50+
}
51+
52+
local mock_device = test.mock_device.build_test_zwave_device(
53+
{
54+
profile = t_utils.get_profile_definition("base-lock-tamper.yml"),
55+
zwave_endpoints = zwave_lock_endpoints,
56+
data = {
57+
lockCodes = json.encode(utils.deep_copy(lockCodes))
58+
}
59+
}
60+
)
61+
62+
local schlage_mock_device = test.mock_device.build_test_zwave_device(
63+
{
64+
profile = t_utils.get_profile_definition("base-lock.yml"),
65+
zwave_endpoints = zwave_lock_endpoints,
66+
zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID,
67+
zwave_product_type = SCHLAGE_PRODUCT_TYPE,
68+
zwave_product_id = SCHLAGE_PRODUCT_ID,
69+
data = {
70+
lockCodes = json.encode(utils.deep_copy(lockCodes))
71+
}
72+
}
73+
)
74+
75+
local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1}
76+
77+
test.register_coroutine_test(
78+
"Device called 'migrate' command",
79+
function()
80+
test.mock_device.add_test_device(mock_device)
81+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
82+
test.socket.zwave:__expect_send(
83+
zw_test_utils.zwave_test_build_send_command(
84+
mock_device,
85+
DoorLock:OperationGet({})
86+
)
87+
)
88+
test.socket.zwave:__expect_send(
89+
zw_test_utils.zwave_test_build_send_command(
90+
mock_device,
91+
Battery:Get({})
92+
)
93+
)
94+
test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()))
95+
test.wait_for_events()
96+
-- Validate lockCodes field
97+
mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" })
98+
-- Validate migration complete flag
99+
mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true)
100+
-- setup codes
101+
test.socket.zwave:__queue_receive({mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) })
102+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } })))
103+
test.wait_for_events()
104+
-- Validate migrate command
105+
test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } })
106+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } })))
107+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
108+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } })))
109+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } })))
110+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })))
111+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } })))
112+
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })))
113+
end
114+
)
115+
116+
test.register_coroutine_test(
117+
"Schlage-Lock device called 'migrate' command, validate codeLength is being properly set",
118+
function()
119+
test.mock_device.add_test_device(schlage_mock_device)
120+
test.socket.device_lifecycle:__queue_receive({ schlage_mock_device.id, "added" })
121+
test.socket.zwave:__expect_send(
122+
zw_test_utils.zwave_test_build_send_command(
123+
schlage_mock_device,
124+
DoorLock:OperationGet({})
125+
)
126+
)
127+
test.socket.zwave:__expect_send(
128+
zw_test_utils.zwave_test_build_send_command(
129+
schlage_mock_device,
130+
Battery:Get({})
131+
)
132+
)
133+
test.wait_for_events()
134+
-- Validate lockCodes field
135+
mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "_lock_codes", { ["1"] = "Zach", ["5"] = "Steven" })
136+
-- Validate migration complete flag
137+
mock_datastore.__assert_device_store_contains(schlage_mock_device.id, "migrationComplete", true)
138+
-- setup codes
139+
test.socket.zwave:__queue_receive({schlage_mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) })
140+
test.socket.zwave:__queue_receive({schlage_mock_device.id, Configuration:Report({ parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, configuration_value = 6 })})
141+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } })))
142+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)))
143+
test.wait_for_events()
144+
-- Validate migrate command
145+
test.socket.capability:__queue_receive({ schlage_mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } })
146+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6, { visibility = { displayed = false } })))
147+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6, { visibility = { displayed = false } })))
148+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } })))
149+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } })))
150+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })))
151+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } })))
152+
test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })))
153+
end
154+
)
155+
156+
test.run_registered_tests()

0 commit comments

Comments
 (0)