Skip to content
64 changes: 43 additions & 21 deletions drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ local function match_profile_modular(driver, device)
end

table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities})
if lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then
if modular_profile_name == "lock-modular-embedded-unlatch" -- the embedded config that may be needed is not checked by an optional capability comparison
or lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then
device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs})
end
end
Expand Down Expand Up @@ -1554,15 +1555,31 @@ local function clear_user_response_handler(driver, device, ib, response)
device.log.warn(string.format("Failed to clear user: %s", status))
end

-- Update commandResult
local command_result_info = {
commandName = cmdName,
userIndex = userIdx,
statusCode = status
}
device:emit_event(capabilities.lockUsers.commandResult(
command_result_info, {state_change = true, visibility = {displayed = false}}
))
-- In the "defaultSchedule" cmd failure path, when a guest user's credentials are set but the scheduling
-- fails during default setup, those credentials should be removed, so we wait to log lock credentials until here.
if cmdName == "defaultSchedule" then
-- note: if clear user succeeds, we'd log credential settings as a "failure" since it's effectively a no-op.
-- If clear user fails, log "success" since the credentials would still be present.
local lock_credential_status = status == "success" and "failure" or "success"
local command_result_info = {
commandName = "addCredential",
userIndex = userIdx,
statusCode = lock_credential_status
}
device:emit_event(capabilities.lockCredentials.commandResult(
command_result_info, {state_change = true, visibility = {displayed = false}}
))
device:set_field(lock_utils.BUSY_STATE, false, {persist = true})
else
local command_result_info = {
commandName = cmdName,
userIndex = userIdx,
statusCode = status
}
device:emit_event(capabilities.lockUsers.commandResult(
command_result_info, {state_change = true, visibility = {displayed = false}}
))
end
device:set_field(lock_utils.BUSY_STATE, false, {persist = true})
end

Expand Down Expand Up @@ -2418,17 +2435,22 @@ local function set_year_day_schedule_handler(driver, device, ib, response)
local cmdName = "addCredential"
local credIdx = device:get_field(lock_utils.CRED_INDEX)

-- Update commandResult
local command_result_info = {
commandName = cmdName,
userIndex = userIdx,
credentialIndex = credIdx,
statusCode = status
}
device:emit_event(capabilities.lockCredentials.commandResult(
command_result_info, {state_change = true, visibility = {displayed = false}}
))
device:set_field(lock_utils.BUSY_STATE, false, {persist = true})
if status == "success" then
-- Update commandResult
local command_result_info = {
commandName = cmdName,
userIndex = userIdx,
credentialIndex = credIdx,
statusCode = status
}
device:emit_event(capabilities.lockCredentials.commandResult(
command_result_info, {state_change = true, visibility = {displayed = false}}
))
device:set_field(lock_utils.BUSY_STATE, false, {persist = true})
else
local ep = find_default_endpoint(device, clusters.DoorLock.ID)
device:send(DoorLock.server.commands.ClearUser(device, ep, userIdx))
end
return
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,77 @@ test.register_coroutine_test(
}
)


local mock_nuki_smart_lock_ultra = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("lock-nocodes-notamper.yml"),
manufacturer_info = {
vendor_id = 0x135D,
product_id = 0x00A1,
},
endpoints = {
{
endpoint_id = 0,
clusters = {
{ cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" },
},
device_types = {
{ device_type_id = 0x0016, device_type_revision = 1 } -- RootNode
}
},
{
endpoint_id = 1,
clusters = {
{
cluster_id = DoorLock.ID,
cluster_type = "SERVER",
cluster_revision = 1,
feature_map = 0x1000, -- UNBOLT
},
{
cluster_id = clusters.PowerSource.ID,
cluster_type = "SERVER",
feature_map = 10
},
},
device_types = {
{ device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock
}
}
}
})

local battery_support = {
NO_BATTERY = "NO_BATTERY",
BATTERY_LEVEL = "BATTERY_LEVEL",
BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE"
}

local profiling_data = {
BATTERY_SUPPORT = "__BATTERY_SUPPORT",
}

test.register_coroutine_test(
"Test Nuki Smart Lock Ultra profile change with user and pin supported",
function()
-- technically, since power source attributes must be read, this wouldn't be running via doConfigure, but this is straightforward.
test.socket.device_lifecycle:__queue_receive({ mock_nuki_smart_lock_ultra.id, "doConfigure" })
test.socket.capability:__expect_send(
mock_nuki_smart_lock_ultra:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}}))
)
test.socket.capability:__expect_send(
mock_nuki_smart_lock_ultra:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}}))
)
mock_nuki_smart_lock_ultra:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"battery"}}}})
mock_nuki_smart_lock_ultra:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end,
{
test_init = function()
test.disable_startup_messages()
test.mock_device.add_test_device(mock_nuki_smart_lock_ultra)
mock_nuki_smart_lock_ultra:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, {persist = true}) -- assume this has been set previously
end,
min_api_version = 17
}
)

test.run_registered_tests()
136 changes: 134 additions & 2 deletions drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ local mock_device = test.mock_device.build_test_matter_device({
cluster_id = DoorLock.ID,
cluster_type = "SERVER",
cluster_revision = 1,
feature_map = 0x0181, -- PIN & USR & COTA
feature_map = 0x0591, -- PIN & WDSCH & USR & COTA & YDSCH
}
},
device_types = {
Expand Down Expand Up @@ -76,7 +76,7 @@ local function test_init()
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}}))
)
mock_device:expect_metadata_update({ profile = "lock-user-pin" })
mock_device:expect_metadata_update({ profile = "lock-user-pin-schedule" })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end

Expand Down Expand Up @@ -2100,4 +2100,136 @@ test.register_coroutine_test(
}
)

-- mock_device:set_field(lock_utils.COTA_CRED, "654123", {persist = true}) --overwrite random cred for test expectation
test.register_coroutine_test(
"Add Guest User and failure response ",
function()
test.socket.capability:__queue_receive(
{
mock_device.id,
{
capability = capabilities.lockCredentials.ID,
command = "addCredential",
args = {0, "guest", "pin", "654123"}
},
}
)
test.socket.matter:__expect_send(
{
mock_device.id,
DoorLock.server.commands.SetCredential(
mock_device, 1, -- endpoint
DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type
DoorLock.types.CredentialStruct(
{credential_type = DoorLock.types.CredentialTypeEnum.PIN, credential_index = 1}
), -- credential
"654123", -- credential_data
nil, -- user_index
nil, -- user_status
DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER -- user_type
),
}
)
test.wait_for_events()
test.socket.matter:__queue_receive(
{
mock_device.id,
DoorLock.client.commands.SetCredentialResponse:build_test_command_response(
mock_device, 1,
DoorLock.types.DlStatus.SUCCESS, -- status
1, -- user_index
2 -- next_credential_index
),
}
)
test.socket.capability:__expect_send(
mock_device:generate_test_message(
"main",
capabilities.lockUsers.users({{userIndex = 1, userType = "guest"}}, {visibility={displayed=false}})
)
)
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.matter:__expect_send(
{
mock_device.id,
DoorLock.server.commands.SetYearDaySchedule(
mock_device, 1, -- endpoint
1, -- year_day_index
1, -- user_index
0, -- local_start_time
0xffffffff -- local_end_time
),
}
)
test.wait_for_events()
test.socket.matter:__queue_receive(
{
mock_device.id,
DoorLock.server.commands.SetYearDaySchedule:build_test_command_response(
mock_device, 1,
DoorLock.types.DlStatus.FAILURE -- status
),
}
)
test.socket.matter:__expect_send({
mock_device.id,
DoorLock.server.commands.ClearUser(
mock_device, 1,
1
)
})
test.wait_for_events()
test.socket.matter:__queue_receive(
{
mock_device.id,
DoorLock.server.commands.ClearUser:build_test_command_response(
mock_device, 1
),
}
)
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.credentials({}, {visibility={displayed=false}})
)
)
test.socket.capability:__expect_send(
mock_device:generate_test_message(
"main",
capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}})
)
)
test.socket.capability:__expect_send(
mock_device:generate_test_message(
"main",
capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}})
)
)
test.socket.capability:__expect_send(
mock_device:generate_test_message(
"main",
capabilities.lockCredentials.commandResult(
{commandName="addCredential", statusCode="failure", userIndex=1}, {state_change=true, visibility={displayed=false}}
)
)
)
end,
{
min_api_version = 17
}
)

test.run_registered_tests()
5 changes: 5 additions & 0 deletions drivers/SmartThings/zigbee-button/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ zigbeeManufacturer:
manufacturer: WALL HERO
model: ACL-401SCA4
deviceProfileName: thirty-buttons
- id: "MultIR/MIR-SO100"
deviceLabel: MultiIR Smart button MIR-SO100
manufacturer: MultIR
model: MIR-SO100
deviceProfileName: one-button-battery-no-fw-update
zigbeeGeneric:
- id: "generic-button-sensor"
deviceLabel: "Zigbee Generic Button"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: one-button-battery-no-fw-update
components:
- id: main
capabilities:
- id: button
version: 1
- id: battery
version: 1
- id: refresh
version: 1
categories:
- name: Button
13 changes: 13 additions & 0 deletions drivers/SmartThings/zigbee-button/src/MultiIR/can_handle.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

return function(opts, driver, device, ...)
local FINGERPRINTS = require "MultiIR.fingerprints"
for _, fingerprint in ipairs(FINGERPRINTS) do
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
local subdriver = require("MultiIR")
return true, subdriver
end
end
return false
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

return {
{ mfr = "MultIR", model = "MIR-SO100" }
}
Loading
Loading