Skip to content
Merged
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
3 changes: 3 additions & 0 deletions drivers/SmartThings/zwave-lock/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ function LockLifecycle.init(driver, device)
device:emit_event(capabilities.lockCredentials.supportedCredentials({ consts.CRED_TYPE_PIN }, { visibility = { displayed = false } }))
end

-- existing devices may predate the default PIN length being set
lock_utils.ensure_pin_length_bounds(device)

if device:supports_capability(capabilities.tamperAlert) then
-- ensure our user/credential state is accurate to the current device state
device:emit_event(capabilities.tamperAlert.tamper.clear())
Expand Down
14 changes: 8 additions & 6 deletions drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ local function migrate(driver, device, cmd)
device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } }))

local consts = require "lock_utils.constants"

-- 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)
if code_length then
device:emit_event(capabilities.lockCredentials.minPinCodeLen(code_length, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCredentials.maxPinCodeLen(code_length, { visibility = { displayed = false } }))
end
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
Expand All @@ -99,8 +103,6 @@ local function migrate(driver, device, cmd)

device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))
device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))
local consts = require("lock_utils.constants")

device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) -- persist the migrated state to the datastore
end

Expand Down
4 changes: 4 additions & 0 deletions drivers/SmartThings/zwave-lock/src/lock_utils/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ lock_constants.LOCK_USERS = {
}

lock_constants.CRED_TYPE_PIN = "pin"
-- User Code Command Class has no capabilities report for PIN length, so
-- the zwave spec fixes pin length to 4-10 digits
lock_constants.DEFAULT_MIN_PIN_LENGTH = 4
lock_constants.DEFAULT_MAX_PIN_LENGTH = 10
lock_constants.DELAY_LOCK_EVENT = "_delay_lock_event"
lock_constants.DELAY_LOCK_EVENT_TIMER = "_delay_lock_event_timer"
lock_constants.MAX_DELAY = 10
Expand Down
18 changes: 18 additions & 0 deletions drivers/SmartThings/zwave-lock/src/lock_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ function lock_utils.sync_device_state(device)
device:send(UserCode:Get({user_identifier = device:get_field(consts.SYNC.CODE_INDEX)}))
end

-- Emit the spec default PIN length range (4-10) if no min/max has been reported yet.
-- Migrated devices copy theirs from lockCodes, so those are left alone to avoid
-- clobbering/misinterpreting them. Schlage locks report their own length later via a
-- configuration parameter, which overrides these defaults.
Comment thread
hcarter-775 marked this conversation as resolved.
Resolved
function lock_utils.ensure_pin_length_bounds(device)
if not device:supports_capability(capabilities.lockCredentials) then
return
end
if device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) == nil then
device:emit_event(capabilities.lockCredentials.minPinCodeLen(consts.DEFAULT_MIN_PIN_LENGTH,
{ visibility = { displayed = false } }))
end
if device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) == nil then
device:emit_event(capabilities.lockCredentials.maxPinCodeLen(consts.DEFAULT_MAX_PIN_LENGTH,
{ visibility = { displayed = false } }))
end
end


-- [[ COMMAND RESULT STATE MANAGEMENT ]] --

Expand Down
18 changes: 15 additions & 3 deletions drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ local tables = require "lock_utils.tables"
local zwave_handlers = require "lock_handlers.zwave_responses"
local cap_handlers = require "lock_handlers.capabilities"

-- If the code length reported by the lock differs from the last known length, the
-- driver assumes the lock wiped all of its codes (as the legacy Schlage handler did
-- by synthesizing an ALL_USER_CODES_DELETED notification) and clears its own records.
--
-- Before the lock reports its PIN length, the driver emits the default bounds of 4-10.
-- Many devices do not report PIN length, so this is specific to Schlage.
-- PIN_LENGTH_FROM_DEVICE is set to true once the lock itself has reported its length,
-- so the default bounds are never mistaken for a reported length and treated as a change.
local PIN_LENGTH_FROM_DEVICE = "pinLengthFromDevice"
local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1}

local function do_configure(self, device)
Expand All @@ -33,7 +42,10 @@ local function configuration_report(self, device, cmd)
if parameter_number == SCHLAGE_LOCK_CODE_LENGTH_PARAM.number then
local reported_code_length = cmd.args.configuration_value
local current_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME)
if current_code_length ~= nil and current_code_length ~= reported_code_length then
-- only a length previously reported by the lock counts; the spec default emitted at startup does not
local previously_reported = device:get_field(PIN_LENGTH_FROM_DEVICE) == true
device:set_field(PIN_LENGTH_FROM_DEVICE, true, { persist = true })
if previously_reported and current_code_length ~= nil and current_code_length ~= reported_code_length then
-- when the code length is changed, all the codes have been wiped
tables.delete_all_entries(device, "credentials")
tables.delete_all_entries(device, "users")
Expand Down Expand Up @@ -79,11 +91,11 @@ end

local function add_credential_handler(self, device, cmd)
local DEFAULT_COMMANDS_DELAY = 4.2
local current_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME)
local base_handler = function()
cap_handlers.add_credential(self, device, cmd)
end
if current_code_length == nil then
-- the default PIN length is not authoritative; wait for the lock's own value if we don't have it yet
if device:get_field(PIN_LENGTH_FROM_DEVICE) ~= true then
device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number}))
device.thread:call_with_delay(DEFAULT_COMMANDS_DELAY, base_handler)
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ local mock_device_typed = test.mock_device.build_test_zwave_device({
_provisioning_state = "TYPED",
})

-- Schlage manufacturer (reports its own PIN length via configuration)
local mock_device_schlage = test.mock_device.build_test_zwave_device({
profile = t_utils.get_profile_definition("base-lock.yml"),
zwave_endpoints = zwave_lock_endpoints,
zwave_manufacturer_id = 0x003B,
})

-- lock-battery profile: lock + battery only (no lockCodes / lockCredentials)
local mock_device_battery = test.mock_device.build_test_zwave_device({
profile = t_utils.get_profile_definition("lock-battery.yml"),
Expand All @@ -58,6 +65,16 @@ local function make_test_init(device)
end
end


local function expect_default_pin_length(device)
test.socket.capability:__expect_send(
device:generate_test_message("main",
capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } })))
test.socket.capability:__expect_send(
device:generate_test_message("main",
capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
end

-- ============================================================================
-- added (device_added)
-- ============================================================================
Expand Down Expand Up @@ -137,6 +154,7 @@ test.register_coroutine_test(
mock_device_base:generate_test_message("main",
capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } }))
)
expect_default_pin_length(mock_device_base)
test.wait_for_events()
end,
{ test_init = make_test_init(mock_device_base) }
Expand All @@ -152,6 +170,44 @@ test.register_coroutine_test(
{ test_init = make_test_init(mock_device_base) }
)

test.register_coroutine_test(
"init: default PIN length is only emitted once",
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" })
-- migrated emits
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 } })))
expect_default_pin_length(mock_device_base)
test.wait_for_events()
-- second init: values are now cached, so only the migrated events are re-emitted
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: Schlage device also gets the default PIN length (the config report overrides it later)",
function()
mock_device_schlage:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true })
test.socket.device_lifecycle:__queue_receive({ mock_device_schlage.id, "init" })
test.socket.capability:__expect_send(mock_device_schlage:generate_test_message("main",
capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })))
test.socket.capability:__expect_send(mock_device_schlage:generate_test_message("main",
capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })))
expect_default_pin_length(mock_device_schlage)
test.wait_for_events()
end,
{ test_init = make_test_init(mock_device_schlage) }
)

test.register_coroutine_test(
"init: lock-battery device (no lockCodes) does nothing regardless of SLGA_MIGRATED",
function()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ test.register_coroutine_test(
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.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.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.lockCredentials.minPinCodeLen(4, { 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.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })))
Expand All @@ -90,6 +92,8 @@ test.register_coroutine_test(
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.credentials({}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device.id))
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.lockCodes.migrated(true, { visibility = { displayed = false } })))
Expand All @@ -107,6 +111,8 @@ test.register_coroutine_test(
-- slot 0 is below the minimum credentialIndex for Z-Wave (1), so it is dropped
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device.id))
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.lockCodes.migrated(true, { visibility = { displayed = false } })))
Expand All @@ -124,6 +130,8 @@ test.register_coroutine_test(
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.credentials({{credentialIndex=2, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}, {credentialIndex=10, credentialType="pin", userIndex=3}}, { 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"}, {userIndex=3, userName="Alex", userType="guest"}}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } })))
test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } })))
test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device.id))
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.lockCodes.migrated(true, { visibility = { displayed = false } })))
Expand Down
29 changes: 29 additions & 0 deletions drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 })
local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 })
local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 })

local PIN_LENGTH_FROM_DEVICE = "pinLengthFromDevice"

local zwave_lock_endpoints = {
{
command_classes = {
Expand Down Expand Up @@ -127,6 +129,33 @@ test.register_coroutine_test(
{ min_api_version = 15 }
)

test.register_coroutine_test(
"First configuration report after the default PIN length should not wipe existing credentials",
function()
added()
-- default (4-10) is in place; add a cached credential to confirm it survives the first real report
mock_device:set_field(PIN_LENGTH_FROM_DEVICE, nil, {})
local tables = require "lock_utils.tables"
test.socket.capability:__set_channel_ordering("relaxed")
test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }},
{ visibility = { displayed = false } })))
tables.add_entry(mock_device, "credentials", { userIndex = 1, credentialIndex = 1, credentialType = "pin" })
test.socket.zwave:__queue_receive({
mock_device.id,
Configuration:Report({ parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, configuration_value = 6 })
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)))
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)))
test.wait_for_events()
assert(#(tables.get_state(mock_device, "credentials") or {}) == 1, "credential must not be wiped")
end,
{ min_api_version = 15 }
)

test.register_coroutine_test(
"Configuration report indicating code deletion should be handled",
function()
Expand Down
Loading