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
44 changes: 17 additions & 27 deletions drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,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
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 @@ -80,26 +85,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 default_endpoint_id = switch_utils.find_default_endpoint(device)
-- ensure subscription to all endpoint attributes- including those mapped to child devices
for _, ep in ipairs(device.endpoints) do
if ep.endpoint_id ~= default_endpoint_id then
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.DEVICE_TYPE_ID.GENERIC_SWITCH 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 @@ -110,6 +96,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 @@ -120,7 +120,7 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr
if #doorbell_endpoints > 0 then
table.insert(doorbell_component_capabilities, capabilities.button.ID)
CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1])
button_cfg.configure_buttons(device)
button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}))
end
if status_light_enabled_present then
table.insert(status_led_component_capabilities, capabilities.switch.ID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function CameraLifecycleHandlers.info_changed(driver, device, event, args)
if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then
camera_cfg.initialize_camera_capabilities(device)
if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then
button_cfg.configure_buttons(device)
button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}))
end
device:subscribe()
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on
return generic_profile or "switch-binary"
end

function SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id)
function SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id)
if #server_onoff_ep_ids == 1 and server_onoff_ep_ids[1] == default_endpoint_id then -- no children will be created
return
end
Expand All @@ -53,16 +53,21 @@ function SwitchDeviceConfiguration.create_child_devices(driver, device, server_o
if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint
local label_and_name = string.format("%s %d", device.label, device_num)
local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true)
driver:try_create_device(
{
local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id)
if not existing_child_device then
driver:try_create_device({
type = "EDGE_CHILD",
label = label_and_name,
profile = child_profile,
parent_device_id = device.id,
parent_assigned_child_key = string.format("%d", ep_id),
vendor_provided_label = label_and_name
}
)
})
else
existing_child_device:try_update_metadata({
profile = child_profile
})
end
end
end

Expand Down Expand Up @@ -121,13 +126,12 @@ function ButtonDeviceConfiguration.update_button_component_map(device, default_e
end


function ButtonDeviceConfiguration.configure_buttons(device)
local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids)
local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE})
local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS})
local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS})

for _, ep in ipairs(ms_eps) do
for _, ep in ipairs(momentary_switch_ep_ids or {}) do
if device.profile.components[switch_utils.endpoint_to_component(device, ep)] then
device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep))
local supportedButtonValues_event
Expand Down Expand Up @@ -184,7 +188,7 @@ function DeviceConfiguration.match_profile(driver, device)

local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH
if #server_onoff_ep_ids > 0 then
SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id)
SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id)
end

if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then
Expand All @@ -206,12 +210,12 @@ function DeviceConfiguration.match_profile(driver, device)
end

-- initialize the main device card with buttons if applicable
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then
ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #button_eps)
local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then
ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momemtary_switch_ep_ids)
-- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id.
ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps)
ButtonDeviceConfiguration.configure_buttons(device)
ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momemtary_switch_ep_ids)
ButtonDeviceConfiguration.configure_buttons(device, momemtary_switch_ep_ids)
return
end

Expand Down
26 changes: 25 additions & 1 deletion drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ function utils.mired_to_kelvin(value, minOrMax)
end

function utils.get_product_override_field(device, override_key)
if fields.vendor_overrides[device.manufacturer_info.vendor_id]
if device.manufacturer_info
and fields.vendor_overrides[device.manufacturer_info.vendor_id]
and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id]
then
return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key]
Expand Down Expand Up @@ -426,4 +427,27 @@ function utils.lazy_load_if_possible(sub_driver_name)
end
end

function utils.update_subscriptions(device)
local default_endpoint_id = 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 ~= default_endpoint_id then
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.DEVICE_TYPE_ID.GENERIC_SWITCH 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()
end

return utils
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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 @@ -23,6 +23,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 @@ -15,6 +15,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 @@ -13,6 +13,7 @@ local CAMERA_EP, FLOODLIGHT_EP, CHIME_EP, DOORBELL_EP = 1, 2, 3, 4
local mock_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("camera.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 @@ -15,6 +15,7 @@ local mock_device = test.mock_device.build_test_matter_device(
{
profile = t_utils.get_profile_definition("5-button-battery.yml"),
manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000},
matter_version = {hardware = 1, sofrware = 1},
endpoints = {
{
endpoint_id = 0,
Expand Down Expand Up @@ -92,8 +93,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 @@ -14,7 +14,8 @@ local mock_device = test.mock_device.build_test_matter_device(
{
profile = t_utils.get_profile_definition("6-button-motion.yml"), -- on a real device we would switch to this, rather than fingerprint to it
manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000},
endpoints = {
matter_version = {hardware = 1, software = 1},
endpoints = {
{
endpoint_id = 0,
clusters = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
local test = require "integration_test"
local capabilities = require "st.capabilities"
local t_utils = require "integration_test.utils"
local utils = require "st.utils"
local dkjson = require "dkjson"

local clusters = require "st.matter.generated.zap_clusters"

Expand All @@ -28,6 +26,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 @@ -108,6 +107,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 @@ -195,27 +195,24 @@ 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
test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
mock_device:expect_metadata_update({ profile = "light-level-3-button" })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
mock_device:expect_device_create({
Expand All @@ -226,19 +223,8 @@ local function test_init()
parent_assigned_child_key = string.format("%d", mock_device_ep5)
})
expect_configure_buttons()
test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
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()

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.device_lifecycle:__queue_receive({ mock_child.id, "init" })
Expand Down Expand Up @@ -462,18 +448,44 @@ 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()
test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump sw 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()
Loading
Loading