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
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ CameraAttributeHandlers.enabled_state_factory = function(attribute)
camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageFlipHorizontal")
elseif attribute == capabilities.imageControl.imageFlipVertical then
camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageFlipVertical")
elseif attribute == capabilities.cameraPrivacyMode.hardPrivacyMode then
camera_utils.update_supported_attributes(device, ib, capabilities.cameraPrivacyMode, "hardPrivacyMode")
end
end
end
Expand Down Expand Up @@ -450,7 +448,7 @@ end

function CameraAttributeHandlers.camera_av_stream_management_attribute_list_handler(driver, device, ib, response)
if not ib.data.elements then return end
local status_light_enabled_present, status_light_brightness_present = false, false
local status_light_enabled_present, status_light_brightness_present, hard_privacy_mode_present = false, false, false
local attribute_ids = {}
for _, attr in ipairs(ib.data.elements) do
if attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID then
Expand All @@ -459,6 +457,8 @@ function CameraAttributeHandlers.camera_av_stream_management_attribute_list_hand
elseif attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID then
status_light_brightness_present = true
table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID)
elseif attr.value == clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn.ID then
hard_privacy_mode_present = true
end
end
local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}
Expand All @@ -469,6 +469,7 @@ function CameraAttributeHandlers.camera_av_stream_management_attribute_list_hand
}
device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true})
camera_cfg.update_status_light_attribute_presence(device, status_light_enabled_present, status_light_brightness_present)
camera_cfg.update_hard_privacy_mode_attribute_presence(device, hard_privacy_mode_present)
camera_cfg.reconcile_profile_and_capabilities(device)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ local function set_status_light_presence(device, status_light_enabled_present, s
device:set_field(camera_fields.STATUS_LIGHT_BRIGHTNESS_PRESENT, status_light_brightness_present == true, { persist = true })
end

local function get_hard_privacy_mode_presence(device)
return device:get_field(camera_fields.HARD_PRIVACY_MODE_PRESENT)
end

local function set_hard_privacy_mode_presence(device, hard_privacy_mode_present)
device:set_field(camera_fields.HARD_PRIVACY_MODE_PRESENT, hard_privacy_mode_present == true, { persist = true })
end

local function build_webrtc_supported_features()
return {
bundle = true,
Expand Down Expand Up @@ -100,12 +108,32 @@ local function build_video_stream_settings_supported_features(device)
return supported_features
end

local function build_camera_privacy_supported_attributes()
return { "softRecordingPrivacyMode", "softLivestreamPrivacyMode" }
local function camera_privacy_feature_supported(device)
return camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.PRIVACY)
end

local function build_camera_privacy_supported_commands()
return { "setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode" }
-- SoftRecordingPrivacyModeEnabled/SoftLivestreamPrivacyModeEnabled are conditionally mandatory on the PRIV
-- feature, while HardPrivacyModeOn has independent optional conformance.
-- A device can support either, both, or neither, so each is built independently.
local function build_camera_privacy_supported_attributes(device)
local supported_attributes = {}
if camera_privacy_feature_supported(device) then
table.insert(supported_attributes, "softRecordingPrivacyMode")
table.insert(supported_attributes, "softLivestreamPrivacyMode")
end
if get_hard_privacy_mode_presence(device) then
table.insert(supported_attributes, "hardPrivacyMode")
end
return supported_attributes
end

local function build_camera_privacy_supported_commands(device)
local supported_commands = {}
if camera_privacy_feature_supported(device) then
table.insert(supported_commands, "setSoftRecordingPrivacyMode")
table.insert(supported_commands, "setSoftLivestreamPrivacyMode")
end
return supported_commands
end

local function capabilities_needing_reinit(device)
Expand Down Expand Up @@ -155,8 +183,8 @@ local function capabilities_needing_reinit(device)
capabilities_to_reinit.video_stream_settings = true
end

if should_init(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedAttributes, build_camera_privacy_supported_attributes()) or
should_init(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedCommands, build_camera_privacy_supported_commands()) then
if should_init(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedAttributes, build_camera_privacy_supported_attributes(device)) or
should_init(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedCommands, build_camera_privacy_supported_commands(device)) then
capabilities_to_reinit.camera_privacy_mode = true
end

Expand Down Expand Up @@ -194,6 +222,7 @@ end

function CameraDeviceConfiguration.match_profile(device)
local status_light_enabled_present, status_light_brightness_present = get_status_light_presence(device)
local hard_privacy_mode_present = get_hard_privacy_mode_presence(device)
local profile_update_requested = false
local optional_supported_component_capabilities = {}
local main_component_capabilities = {}
Expand Down Expand Up @@ -234,7 +263,7 @@ function CameraDeviceConfiguration.match_profile(device)
if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then
table.insert(main_component_capabilities, capabilities.imageCapture.ID)
end
if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.PRIVACY) then
if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.PRIVACY) or hard_privacy_mode_present then
table.insert(main_component_capabilities, capabilities.cameraPrivacyMode.ID)
end
if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then
Expand Down Expand Up @@ -361,8 +390,8 @@ end
local function init_camera_privacy_mode(device)
if device:supports_capability(capabilities.cameraPrivacyMode) then
local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID)
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedAttributes(build_camera_privacy_supported_attributes()))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(build_camera_privacy_supported_commands()))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedAttributes(build_camera_privacy_supported_attributes(device)))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(build_camera_privacy_supported_commands(device)))
end
end

Expand Down Expand Up @@ -442,6 +471,10 @@ function CameraDeviceConfiguration.update_status_light_attribute_presence(device
set_status_light_presence(device, status_light_enabled_present, status_light_brightness_present)
end

function CameraDeviceConfiguration.update_hard_privacy_mode_attribute_presence(device, hard_privacy_mode_present)
set_hard_privacy_mode_presence(device, hard_privacy_mode_present)
end

function CameraDeviceConfiguration.reinitialize_changed_camera_capabilities_and_subscriptions(device, old_profile, new_profile)
local changed_capabilities = changed_capabilities_from_profiles(old_profile, new_profile)
initialize_selected_camera_capabilities(device, changed_capabilities)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ CameraFields.TRIGGERED_ZONES = "__triggered_zones"
CameraFields.DPTZ_VIEWPORTS = "__dptz_viewports"
CameraFields.STATUS_LIGHT_ENABLED_PRESENT = "__status_light_enabled_present"
CameraFields.STATUS_LIGHT_BRIGHTNESS_PRESENT = "__status_light_brightness_present"
CameraFields.HARD_PRIVACY_MODE_PRESENT = "__hard_privacy_mode_present"

CameraFields.CameraAVSMFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.CameraAvStreamManagement.ID }
CameraFields.CameraAVSULMFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.CameraAvSettingsUserLevelManagement.ID }
Expand Down
163 changes: 159 additions & 4 deletions drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@ test.register_coroutine_test(
subscribe = function() subscribe_called = true end,
supports_capability = function() return false end,
get_endpoints = function() return { DOORBELL_EP } end,
get_field = function() return nil end,
}

local original_match_profile = camera_cfg.match_profile
Expand Down Expand Up @@ -894,6 +895,9 @@ test.register_coroutine_test(
get_endpoints = function()
return { CAMERA_EP }
end,
get_field = function()
return nil
end,
emit_event_for_endpoint = function()
init_event_count = init_event_count + 1
end
Expand All @@ -909,6 +913,132 @@ test.register_coroutine_test(
}
)

test.register_coroutine_test(
"Camera privacy mode should be added to profile when HardPrivacyModeOn is present even without the PRIV feature",
function()
local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration"

local updated_metadata = nil
local fake_device = {
profile = { components = {} },
endpoints = {
{
endpoint_id = CAMERA_EP,
device_types = {
{device_type_id = 0x0142, device_type_revision = 1} -- Camera
},
clusters = {
{
cluster_id = clusters.CameraAvStreamManagement.ID,
feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO, -- no PRIVACY feature
cluster_type = "SERVER"
}
}
}
},
get_field = function(_, field)
return field == camera_fields.HARD_PRIVACY_MODE_PRESENT
end,
get_endpoints = function() return {} end,
try_update_metadata = function(_, metadata) updated_metadata = metadata end,
}

camera_cfg.match_profile(fake_device)

assert(updated_metadata ~= nil, "profile update should be requested when HardPrivacyModeOn is present")
local main_capabilities
for _, component in ipairs(updated_metadata.optional_component_capabilities) do
if component[1] == "main" then
main_capabilities = component[2]
end
end
local found = false
for _, cap_id in ipairs(main_capabilities or {}) do
if cap_id == capabilities.cameraPrivacyMode.ID then
found = true
end
end
assert(found, "cameraPrivacyMode should be added to the profile based on HardPrivacyModeOn presence alone")
end,
{
min_api_version = 14
}
)

test.register_coroutine_test(
"Camera privacy mode supportedAttributes/supportedCommands should only expose hardPrivacyMode when PRIV feature is absent",
function()
local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration"

local emitted = {}
local fake_device = {
endpoints = {
{
endpoint_id = CAMERA_EP,
clusters = {
{
cluster_id = clusters.CameraAvStreamManagement.ID,
feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO, -- no PRIVACY feature
cluster_type = "SERVER"
}
}
}
},
supports_capability = function(_, capability)
return capability == capabilities.cameraPrivacyMode
end,
get_field = function(_, field)
return field == camera_fields.HARD_PRIVACY_MODE_PRESENT
end,
get_endpoints = function(self, cluster_id, opts)
opts = opts or {}
local eps = {}
for _, ep in ipairs(self.endpoints) do
for _, clus in ipairs(ep.clusters) do
if clus.cluster_id == cluster_id and
(opts.feature_bitmap == nil or (clus.feature_map & opts.feature_bitmap) == opts.feature_bitmap) then
table.insert(eps, ep.endpoint_id)
end
end
end
return eps
end,
emit_event_for_endpoint = function(_, _, capability_event)
table.insert(emitted, capability_event)
end,
}

camera_cfg.initialize_camera_capabilities(fake_device)

local function list_equals(a, b)
if #a ~= #b then return false end
for i, v in ipairs(a) do
if v ~= b[i] then return false end
end
return true
end

local supported_attributes_event, supported_commands_event
for _, event in ipairs(emitted) do
if event.attribute == capabilities.cameraPrivacyMode.supportedAttributes then
supported_attributes_event = event
elseif event.attribute == capabilities.cameraPrivacyMode.supportedCommands then
supported_commands_event = event
end
end

assert(supported_attributes_event ~= nil, "supportedAttributes should be emitted")
assert(list_equals(supported_attributes_event.value.value, {"hardPrivacyMode"}),
"supportedAttributes should contain only hardPrivacyMode when PRIV feature is absent")
assert(supported_commands_event ~= nil, "supportedCommands should be emitted")
assert(list_equals(supported_commands_event.value.value, {}),
"supportedCommands should be empty when PRIV feature is absent, since HardPrivacyModeOn is read-only")
end,
{
min_api_version = 14
}
)

test.register_coroutine_test(
"Reports mapping to EnabledState capability data type should generate appropriate events",
function()
Expand Down Expand Up @@ -940,10 +1070,6 @@ test.register_coroutine_test(
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal", "imageFlipVertical"}))
)
elseif v.capability == capabilities.cameraPrivacyMode.hardPrivacyMode then
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes({"softRecordingPrivacyMode", "softLivestreamPrivacyMode", "hardPrivacyMode"}))
)
end
test.socket.matter:__queue_receive({
mock_device.id,
Expand Down Expand Up @@ -3432,6 +3558,35 @@ test.register_coroutine_test(
}
)

test.register_coroutine_test(
"Camera privacy mode supportedAttributes should include hardPrivacyMode when reported in AttributeList",
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),
uint32(clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn.ID)
})
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes(
{"softRecordingPrivacyMode", "softLivestreamPrivacyMode", "hardPrivacyMode"}
))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedCommands(
{"setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode"}
))
)
end,
{
min_api_version = 14
}
)

test.register_coroutine_test(
"Camera profile should include zoneManagement when USER_DEFINED feature is present",
function()
Expand Down
Loading