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
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr
local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL)
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, 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 All @@ -147,6 +145,10 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr

if camera_utils.optional_capabilities_list_changed(optional_supported_component_capabilities, device.profile.components) then
device:try_update_metadata({profile = "camera", optional_component_capabilities = optional_supported_component_capabilities})
if #doorbell_endpoints > 0 then
Copy link
Contributor Author

@nickolas-deboom nickolas-deboom Jan 6, 2026

Choose a reason for hiding this comment

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

button config can be put behind the optional_capabilities_list_changed gate so it doesn't run each time CameraAvStreamManagement.AttributeList is received.

CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1])
button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}))
end
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,28 +120,51 @@ function CameraUtils.profile_changed(synced_components, prev_components)
return false
end

function CameraUtils.optional_capabilities_list_changed(optional_capabilities, prev_component_list)
local prev_optional_capabilities = {}
for idx, comp in pairs(prev_component_list or {}) do
local cap_list = {}
for _, capability in pairs(comp.capabilities or {}) do
table.insert(cap_list, capability.id)
function CameraUtils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list)
local previous_capability_map = {}
local component_sizes = {}

local previous_component_count = 0
for component_name, component in pairs(previous_component_capability_list or {}) do
previous_capability_map[component_name] = {}
component_sizes[component_name] = 0
for _, capability in pairs(component.capabilities or {}) do
if capability.id ~= "firmwareUpdate" and capability.id ~= "refresh" then
previous_capability_map[component_name][capability.id] = true
component_sizes[component_name] = component_sizes[component_name] + 1
end
end
table.insert(prev_optional_capabilities, {idx, cap_list})
end
if #optional_capabilities ~= #prev_optional_capabilities then
return true
previous_component_count = previous_component_count + 1
end
for _, capability in pairs(optional_capabilities or {}) do
if not switch_utils.tbl_contains(prev_optional_capabilities, capability) then

local number_of_components_counted = 0
for _, new_component_capabilities in pairs(new_component_capability_list or {}) do
local component_name = new_component_capabilities[1]
local capability_list = new_component_capabilities[2]

number_of_components_counted = number_of_components_counted + 1
Copy link
Contributor

Choose a reason for hiding this comment

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

I think, since this list is actually just a simple table that we know the structure of, we can just do

local new_list_num_components = #new_component_capability_list

and similar for the capabilities:

local new_list_num_capabilities_in_component = #capability_list

Copy link
Contributor

Choose a reason for hiding this comment

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

or maybe we don't even need to define them as variables in this case, and just call this directly as needed

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 tried that but apparently it doesn't work for keyed tables. I didn't try very hard tho, so lemme see if it'll work

Copy link
Contributor

Choose a reason for hiding this comment

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

ah my bad, that's true. I forgot about that.

Copy link
Contributor

Choose a reason for hiding this comment

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

actually, ins't the capability_list a basic table? That one might work


if not previous_capability_map[component_name] then
Copy link
Contributor

Choose a reason for hiding this comment

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

nit but can we do if <something> == nil then in these cases? Idk, since we're talking about whether these were found this structuring seems more natural? Just throwing it out there, what do you think

return true
end
end
for _, capability in pairs(prev_optional_capabilities or {}) do
if not switch_utils.tbl_contains(optional_capabilities, capability) then

local capabilities_in_component = 0
for _, capability in ipairs(capability_list) do
if not previous_capability_map[component_name][capability] then
return true
end
capabilities_in_component = capabilities_in_component + 1
end

if capabilities_in_component ~= component_sizes[component_name] then
return true
end
end

if number_of_components_counted ~= previous_component_count then
return true
end

return false
end

Expand Down
59 changes: 57 additions & 2 deletions drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local t_utils = require "integration_test.utils"
local test = require "integration_test"
local uint32 = require "st.matter.data_types.Uint32"

test.disable_startup_messages()

Expand Down Expand Up @@ -155,7 +156,6 @@ test.set_test_init_function(test_init)

local function update_device_profile()
test.socket.matter:__set_channel_ordering("relaxed")
local uint32 = require "st.matter.data_types.Uint32"
local expected_metadata = {
optional_component_capabilities = {
{
Expand Down Expand Up @@ -214,8 +214,9 @@ local function update_device_profile()
uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID)
})
})
test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)})
mock_device:expect_metadata_update(expected_metadata)
test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)})
test.wait_for_events()
local updated_device_profile = t_utils.get_profile_definition(
"camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities}
)
Expand Down Expand Up @@ -1816,5 +1817,59 @@ test.register_coroutine_test(
end
)

test.register_coroutine_test(
"Camera profile should not update for an unchanged Status Light AttributeList report",
function()
update_device_profile()
test.wait_for_events()
test.socket.matter:__queue_receive({
mock_device.id,
clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, {
uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID),
uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID)
})
})
end
)

test.register_coroutine_test(
"Camera profile should update for a changed Status Light AttributeList report",
function()
update_device_profile()
test.wait_for_events()
test.socket.matter:__queue_receive({
mock_device.id,
clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, {
uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID)
})
})
local expected_metadata = {
optional_component_capabilities = {
{ "main",
{ "videoCapture2", "cameraViewportSettings", "localMediaStorage", "audioRecording", "cameraPrivacyMode",
"imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement",
"webrtc", "motionSensor", "sounds", }
},
{ "statusLed",
{ "switch" } -- only switch capability remains
},
{ "speaker",
{ "audioMute", "audioVolume" }
},
{ "microphone",
{ "audioMute", "audioVolume" }
},
{ "doorbell",
{ "button" }
}
},
profile = "camera"
}
mock_device:expect_metadata_update(expected_metadata)
test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)})
test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false})))
end
)

-- run the tests
test.run_registered_tests()
Loading