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
4 changes: 4 additions & 0 deletions drivers/SmartThings/zigbee-lock/profiles/base-lock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ components:
version: 1
- id: lockCodes
version: 1
- id: lockCredentials
version: 1
- id: lockUsers
version: 1
- id: battery
version: 1
- id: firmwareUpdate
Expand Down
37 changes: 37 additions & 0 deletions drivers/SmartThings/zigbee-lock/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ local capabilities = require "st.capabilities"
local Battery = capabilities.battery
local Lock = capabilities.lock
local LockCodes = capabilities.lockCodes
local LockCredentials = capabilities.lockCredentials
local LockUsers = capabilities.lockUsers

-- Enums
local UserStatusEnum = LockCluster.types.DrlkUserStatus
Expand Down Expand Up @@ -291,6 +293,40 @@ local name_slot = function(driver, device, command)
end
end

local migrate = function(driver, device, command)
local lock_users = {}
local lock_credentials = {}
local lock_codes = lock_utils.get_lock_codes(device)
local ordered_codes = {}

for code in pairs(lock_codes) do
table.insert(ordered_codes, code)
end

table.sort(ordered_codes)
for index, code_slot in ipairs(ordered_codes) do
table.insert(lock_users, {userIndex = index, userType = "guest", userName = lock_codes[code_slot]})
Copy link
Contributor

Choose a reason for hiding this comment

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

Zigbee supports other user types, and IIRC some locks like Yale do use them. It doesn't look like lockCodes kept track of that information, but could we try to get the type with the Get User Type command?

Copy link
Contributor

Choose a reason for hiding this comment

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

My gut says that we should keep it simple for the migration (i.e. limit device communication), but perhaps handle it in the new capability handlers.

table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"})
end

local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME)
local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 4)
local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 8)
local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME)
if (code_length ~= nil) then
max_code_len = code_length
min_code_len = code_length
end

device:emit_event(LockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } }))
device:emit_event(LockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } }))
device:emit_event(LockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } }))
device:emit_event(LockCredentials.credentials(lock_credentials, { visibility = { displayed = false } }))
device:emit_event(LockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))
device:emit_event(LockUsers.users(lock_users, { visibility = { displayed = false } }))
device:emit_event(LockCodes.migrated(true, { visibility = { displayed = false } }))
end

local function device_added(driver, device)
lock_utils.populate_state_from_data(device)

Expand Down Expand Up @@ -436,6 +472,7 @@ local zigbee_lock_driver = {
[LockCodes.commands.requestCode.NAME] = request_code,
[LockCodes.commands.setCode.NAME] = set_code,
[LockCodes.commands.nameSlot.NAME] = name_slot,
[LockCodes.commands.migrate.NAME] = migrate,
},
[Lock.ID] = {
[Lock.commands.lock.NAME] = lock,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

-- Mock out globals
local test = require "integration_test"
local zigbee_test_utils = require "integration_test.zigbee_test_utils"
local t_utils = require "integration_test.utils"

local clusters = require "st.zigbee.zcl.clusters"
local PowerConfiguration = clusters.PowerConfiguration
local DoorLock = clusters.DoorLock
local Alarm = clusters.Alarms
local capabilities = require "st.capabilities"

local json = require "st.json"

local mock_datastore = require "integration_test.mock_env_datastore"

local mock_device = test.mock_device.build_test_zigbee_device(
{
profile = t_utils.get_profile_definition("base-lock.yml"),
data = {
lockCodes = json.encode({
["1"] = "Zach",
["5"] = "Steven"
}),
}
}
)

zigbee_test_utils.prepare_zigbee_env_info()
local function test_init()end

test.set_test_init_function(test_init)

test.register_coroutine_test(
"Device called 'migrate' command",
function()
test.mock_device.add_test_device(mock_device)
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) })
test.wait_for_events()
-- Validate lockCodes field
mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" })
-- Validate migration complete flag
mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true)

-- Set min/max code length attributes
test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) })
test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) })
test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) })
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } })))
test.wait_for_events()
-- Validate `migrate` command functionality.
test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } })
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(5, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } })))
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 } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })))
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 } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })))
test.wait_for_events()
end
)

test.run_registered_tests()
4 changes: 4 additions & 0 deletions drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ components:
version: 1
- id: lockCodes
version: 1
- id: lockCredentials
version: 1
- id: lockUsers
version: 1
- id: battery
version: 1
- id: tamperAlert
Expand Down
4 changes: 4 additions & 0 deletions drivers/SmartThings/zwave-lock/profiles/base-lock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ components:
version: 1
- id: lockCodes
version: 1
- id: lockCredentials
version: 1
- id: lockUsers
version: 1
- id: battery
version: 1
- id: refresh
Expand Down
47 changes: 47 additions & 0 deletions drivers/SmartThings/zwave-lock/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,48 @@ local function update_codes(driver, device, cmd)
end
end

--- @param driver st.zwave.Driver
--- @param device st.zwave.Device
--- @param cmd table
local function migrate(driver, device, cmd)
local lock_users = {}
local lock_credentials = {}
local lc_data = json.decode(device.data.lockCodes)
local lock_codes = {}
local ordered_codes = {}
for k, v in pairs(lc_data) do
lock_codes[k] = v
end

for code in pairs(lock_codes) do
table.insert(ordered_codes, code)
end

table.sort(ordered_codes)
for index = 1, #ordered_codes do
local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ]
table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name})
table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"})
end

local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME)
local min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME, 4)
local max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME, 10)
local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME)
if (code_length ~= nil) then
max_code_len = code_length
min_code_len = code_length
end

device:emit_event(capabilities.lockCredentials.minPinCodeLen(min_code_len, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCredentials.maxPinCodeLen(max_code_len, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))
end

local function time_get_handler(driver, device, cmd)
local time = os.date("*t")
device:send_to_component(
Expand All @@ -162,6 +204,8 @@ local driver_template = {
supported_capabilities = {
capabilities.lock,
capabilities.lockCodes,
capabilities.lockUsers,
capabilities.lockCredentials,
capabilities.battery,
capabilities.tamperAlert
},
Expand All @@ -173,6 +217,9 @@ local driver_template = {
[capabilities.lockCodes.ID] = {
[capabilities.lockCodes.commands.updateCodes.NAME] = update_codes
},
[capabilities.lockCodes.ID] = {
[capabilities.lockCodes.commands.migrate.NAME] = migrate
},
[capabilities.refresh.ID] = {
[capabilities.refresh.commands.refresh.NAME] = do_refresh
}
Expand Down
Loading
Loading