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
52 changes: 17 additions & 35 deletions drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ local fields = require "utils.switch_fields"
local switch_utils = require "utils.switch_utils"
local cfg = require "utils.device_configuration"
local device_cfg = cfg.DeviceCfg
local switch_cfg = cfg.SwitchCfg
local button_cfg = cfg.ButtonCfg

local attribute_handlers = require "generic_handlers.attribute_handlers"
Expand Down Expand Up @@ -65,16 +64,21 @@ end

function SwitchLifecycleHandlers.info_changed(driver, device, event, args)
if device.profile.id ~= args.old_st_store.profile.id then
device:subscribe()
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if #button_eps > 0 and device.network_type == device_lib.NETWORK_TYPE_MATTER then
button_cfg.configure_buttons(device)
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
Copy link
Contributor

Choose a reason for hiding this comment

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

is the checking for button support done inside button_cfg?

Copy link
Contributor Author

@hcarter-775 hcarter-775 Oct 24, 2025

Choose a reason for hiding this comment

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

yes, this function will be a no-op if there are no button-mappable endpoints. So I could revert it, but it removes an extra check. Maybe this adds confusion though?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thought about this more, and I improved the api by having the caller pass in the endpoints that will be mapped, still cutting out the extra check while keeping this clarity

device:subscribe()
button_cfg.configure_buttons(device,
device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
)
elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then
switch_utils.update_subscriptions(device:get_parent_device()) -- parent device required to scan through EPs and update subscriptions
end
end
end

function SwitchLifecycleHandlers.device_removed(driver, device)
device.log.info("device removed")
if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then
if device.matter_version.software ~= args.old_st_store.matter_version.software then
device_cfg.match_profile(driver, device)
end
end
end

function SwitchLifecycleHandlers.device_init(driver, device)
Expand All @@ -85,33 +89,7 @@ function SwitchLifecycleHandlers.device_init(driver, device)
if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then
device:set_find_child(switch_utils.find_child)
end
local main_endpoint = switch_utils.find_default_endpoint(device)
-- ensure subscription to all endpoint attributes- including those mapped to child devices
for idx, ep in ipairs(device.endpoints) do
if ep.endpoint_id ~= main_endpoint then
if device:supports_server_cluster(clusters.OnOff.ID, ep) then
local child_profile = switch_cfg.assign_child_profile(device, ep)
if idx == 1 and string.find(child_profile, "energy") then
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true})
end
end
local id = 0
for _, dt in ipairs(ep.device_types) do
id = math.max(id, dt.device_type_id)
end
for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do
if id == fields.GENERIC_SWITCH_ID and
attr ~= clusters.PowerSource.attributes.BatPercentRemaining and
attr ~= clusters.PowerSource.attributes.BatChargeLevel then
device:add_subscribed_event(attr)
else
device:add_subscribed_attribute(attr)
end
end
end
end
device:subscribe()
switch_utils.update_subscriptions(device)

-- device energy reporting must be handled cumulatively, periodically, or by both simulatanously.
-- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported.
Expand All @@ -127,6 +105,10 @@ function SwitchLifecycleHandlers.device_init(driver, device)
end
end

function SwitchLifecycleHandlers.device_removed(driver, device)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just moved this to the bottom of the file. We never touch this and I personally am always getting confused where device_init is when developing with this placed above it.

device.log.info("device removed")
end

local matter_driver_template = {
lifecycle_handlers = {
added = SwitchLifecycleHandlers.device_added,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local button_attr = capabilities.button.button
local aqara_mock_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"),
manufacturer_info = {vendor_id = 0x115F, product_id = 0x2004, product_name = "Aqara Climate Sensor W100"},
matter_version = {hardware = 1, software = 1},
label = "Climate Sensor W100",
device_id = "00000000-1111-2222-3333-000000000001",
endpoints = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ local aqara_child2_ep = 2
local aqara_mock_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("4-button.yml"),
manufacturer_info = {vendor_id = 0x115F, product_id = 0x1009, product_name = "Aqara Light Switch H2"},
matter_version = {hardware = 1, software = 1},
label = "Aqara Light Switch",
device_id = "00000000-1111-2222-3333-000000000001",
endpoints = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ local mock_device = test.mock_device.build_test_matter_device({
{cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}
},
device_types = {
{ device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug
{ device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit
}
}
},
Expand Down Expand Up @@ -88,10 +88,20 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({
{ device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor
}
},
{
endpoint_id = 2,
clusters = {
{ cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, },
},
device_types = {
{ device_type_id = 0x010A, device_type_revision = 1 }, -- On Off Plug In Unit
}
}
},
})

local subscribed_attributes_periodic = {
clusters.OnOff.attributes.OnOff,
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local uint32 = require "st.matter.data_types.Uint32"
local mock_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("button-battery.yml"),
manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000},
matter_version = {hardware = 1, software = 1},
endpoints = {
{
endpoint_id = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ local clusters = require "st.matter.generated.zap_clusters"
local button_attr = capabilities.button.button

-- Mock a 5-button device using endpoints non-consecutive endpoints
local mock_device = test.mock_device.build_test_matter_device(
{
profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it
manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000},
endpoints = {
local mock_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it
manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000},
matter_version = {hardware = 1, software = 1},
endpoints = {
{
endpoint_id = 0,
clusters = {},
Expand Down Expand Up @@ -87,8 +87,7 @@ local mock_device = test.mock_device.build_test_matter_device(
}
},
},
}
)
})

-- add device for each mock device
local CLUSTER_SUBSCRIBE_LIST ={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local mock_device = test.mock_device.build_test_matter_device({
vendor_id = 0x0000,
product_id = 0x0000,
},
matter_version = {hardware = 1, software = 1},
endpoints = {
{
endpoint_id = 0,
Expand Down Expand Up @@ -105,6 +106,7 @@ local mock_device_mcd_unsupported_switch_device_type = test.mock_device.build_te
vendor_id = 0x0000,
product_id = 0x0000,
},
matter_version = {hardware = 1, software = 1},
endpoints = {
{
endpoint_id = 0,
Expand Down Expand Up @@ -192,49 +194,35 @@ local function expect_configure_buttons()
test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false})))
end

-- All messages queued and expectations set are done before the driver is actually run
local function test_init()
-- we dont want the integration test framework to generate init/doConfigure, we are doing that here
-- so we can set the proper expectations on those events.
test.disable_startup_messages()
test.mock_device.add_test_device(mock_device) -- make sure the cache is populated
test.mock_device.add_test_device(mock_child)

-- added sets a bunch of fields on the device, and calls init
local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device)
for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do
if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end
end
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })

-- init results in subscription interaction
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })

--doConfigure sets the provisioning state to provisioned
mock_device:expect_metadata_update({ profile = "light-level-3-button" })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
mock_device:expect_device_create({
type = "EDGE_CHILD",
label = "Matter Switch 2",
profile = "light-color-level",
parent_device_id = mock_device.id,
parent_assigned_child_key = string.format("%d", mock_device_ep5)
})
mock_device:expect_metadata_update({ profile = "light-level-3-button" })
expect_configure_buttons()
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })

-- simulate the profile change update taking affect and the device info changing
local device_info_copy = utils.deep_copy(mock_device.raw_st_data)
device_info_copy.profile.id = "5-buttons-battery"
local device_info_json = dkjson.encode(device_info_copy)
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json })
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
expect_configure_buttons()
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })

test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)})
test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" })
test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)})
test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" })
mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" })
Expand Down Expand Up @@ -452,18 +440,48 @@ test.register_coroutine_test(
test.register_coroutine_test(
"Test driver switched event",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device)
for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do
if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end
end
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" })
mock_child:expect_metadata_update({ profile = "light-color-level" })
mock_device:expect_metadata_update({ profile = "light-level-3-button" })
expect_configure_buttons()
mock_device:expect_device_create({
type = "EDGE_CHILD",
label = "Matter Switch 2",
profile = "light-color-level",
parent_device_id = mock_device.id,
parent_assigned_child_key = string.format("%d", mock_device_ep5)
})
end
)

test.register_coroutine_test(
"Test info changed event with parent device profile update",
function()
local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device)
for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do
if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end
end
local updated_device_profile = t_utils.get_profile_definition("light-level-3-button.yml")
updated_device_profile.id = "updated device profile id"
test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile }))
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
expect_configure_buttons()
end
)

test.register_coroutine_test(
"Test info changed event with matter_version update",
function()
local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device)
for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do
if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end
end
test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2
mock_child:expect_metadata_update({ profile = "light-color-level" })
mock_device:expect_metadata_update({ profile = "light-level-3-button" })
expect_configure_buttons()
end
)


-- run the tests
test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ local mock_device_onoff = test.mock_device.build_test_matter_device({
vendor_id = 0x0000,
product_id = 0x0000,
},
matter_version = {
hardware = 1,
software = 1,
},
endpoints = {
{
endpoint_id = 0,
Expand Down Expand Up @@ -505,6 +509,7 @@ local function test_init_mounted_on_off_control()
test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" })
mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" })
mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end

Expand All @@ -526,6 +531,7 @@ local function test_init_mounted_dimmable_load_control()
test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" })
mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" })
mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end

Expand Down Expand Up @@ -566,6 +572,7 @@ local function test_init_parent_child_different_types()
test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" })
mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" })
mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" })

mock_device_parent_child_different_types:expect_device_create({
Expand Down Expand Up @@ -617,6 +624,7 @@ local function test_init_light_level_motion()
test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" })
mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" })
mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ local mock_device = test.mock_device.build_test_matter_device({
vendor_id = 0x0000,
product_id = 0x0000,
},
matter_version = {
hardware = 1,
software = 1,
},
endpoints = {
{
endpoint_id = 0,
Expand Down Expand Up @@ -189,6 +193,7 @@ local function test_init()
test.socket.matter:__expect_send({mock_device.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
mock_device:expect_metadata_update({ profile = "light-binary" })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })

for _, child in pairs(mock_children) do
Expand Down Expand Up @@ -260,6 +265,7 @@ local function test_init_parent_child_endpoints_non_sequential()
test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" })
mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" })
mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" })

for _, child in pairs(mock_children_non_sequential) do
Expand Down Expand Up @@ -687,4 +693,14 @@ test.register_coroutine_test(
{ test_init = test_init_parent_child_endpoints_non_sequential }
)

test.register_coroutine_test(
"Test info changed event with matter_version update",
function()
test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2
mock_children[child1_ep]:expect_metadata_update({ profile = "light-level" })
mock_children[child2_ep]:expect_metadata_update({ profile = "light-color-level" })
mock_device:expect_metadata_update({ profile = "light-binary" })
end
)

test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ local function test_init()
test.socket.matter:__expect_send({mock_device.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
mock_device:expect_metadata_update({ profile = "plug-binary" })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })

for _, child in pairs(mock_children) do
Expand Down Expand Up @@ -196,6 +197,7 @@ local function test_init_child_profile_override()
test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" })
mock_device_child_profile_override:expect_metadata_update({ profile = "plug-binary" })
mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" })

for _, child in pairs(mock_children_child_profile_override) do
Expand Down
Loading