Summary
The Matter camera subdriver already supports the standard Matter Privacy model through the SmartThings cameraPrivacyMode capability, but a Matter camera with Privacy has no normal main.switch capability.
That leaves users without the conventional camera on/off control on the dashboard and without the simplest on/off camera control in routines.
A practical mapping has now been tested successfully on a real Matter 1.5.1 camera:
switch ON -> soft recording privacy disabled
soft livestream privacy disabled
switch OFF -> soft recording privacy enabled
soft livestream privacy enabled
HardPrivacyModeOn remains read-only and must never be written.
The existing granular cameraPrivacyMode capability should remain available; switch is a user-facing master abstraction, not a replacement for the detailed Matter states.
Environment / verification
Verified against SmartThingsCommunity/SmartThingsEdgeDrivers main at commit
60bbf2716412fdf04fd59f967bdd37be6995d664 (2026-08-12).
Real-device validation used:
- Aqara Camera Hub G350
- VID/PID:
0x115F / 0x3013
- firmware:
4.5.70 / 4005070
- reported Matter SpecificationVersion:
1.5.1
- Camera device type:
0x0142
- Camera endpoint in this device: endpoint 2
The proposed changes are generic Matter-camera changes. No Aqara VID/PID fingerprint or proprietary Aqara cluster handling is required.
Current behavior
The current camera profile has:
cameraPrivacyMode on main;
switch only on the optional statusLed component.
See:
The profile's dashboard has no switch state/action, and its detail/routine presentation also omits main.switch:
The current camera capability handlers use switch only as the status LED control:
The three Matter privacy attributes are already handled:
SoftRecordingPrivacyModeEnabled
SoftLivestreamPrivacyModeEnabled
HardPrivacyModeOn
and the two soft attributes already have write commands through cameraPrivacyMode.
Reproduction
- Join a Matter camera that exposes AVSM Privacy.
- Allow the camera profile to initialize.
- Inspect its SmartThings capabilities/presentation.
On the G350 before this change, SmartThings exposed:
cameraPrivacyMode
softRecordingPrivacyMode
softLivestreamPrivacyMode
hardPrivacyMode
but no main.switch.
As a result:
- the device page had no simple camera on/off control;
- routines could use the detailed privacy capability, but not the normal SmartThings on/off abstraction;
- the camera dashboard had no power/privacy button.
Existing SmartThings UI precedent
A cloud-connected Aeotec Cam 360 integrated by SmartThings exposes:
and its presentation includes:
Dashboard state: {switch.value}
Dashboard action: standbyPowerSwitch
Detail view: switch
Automation: switch condition + switch action
This provides a useful existing SmartThings camera UX precedent.
Expected behavior
When Matter Privacy is supported and at least one writable soft privacy attribute exists, the camera should expose an optional switch on main.
Suggested semantics:
switch ON:
write false to each supported writable soft privacy attribute
switch OFF:
write true to each supported writable soft privacy attribute
State derivation:
any supported privacy state active
-> switch = off
all supported privacy states known inactive
-> switch = on
required state not known yet
-> do not invent a switch state
HardPrivacyModeOn should contribute to the reported state (off when active) but must remain read-only.
Suggested code changes
Profile
Add optional switch to main:
- id: switch
version: 1
optional: true
Expose it in the camera presentation:
deviceConfig:
dashboard:
states:
- component: main
capability: switch
version: 1
actions:
- component: main
capability: switch
version: 1
detailView:
- component: main
capability: switch
version: 1
automation:
conditions:
- component: main
capability: switch
version: 1
actions:
- component: main
capability: switch
version: 1
The explicit dashboard.actions entry is important: testing showed that merely adding the capability to the profile/detail/routine configuration did not create an interactive dashboard button.
Dynamic profiling
Enable main.switch only when AVSM Privacy is supported and at least one writable soft privacy attribute is available.
Commands
Dispatch switch by component so main and statusLed remain independent:
if cmd.component == "main" then
-- main ON = privacy off
-- main OFF = privacy on
elseif cmd.component == "statusLed" then
-- retain existing StatusLightEnabled behavior
end
Write only:
SoftRecordingPrivacyModeEnabled
SoftLivestreamPrivacyModeEnabled
Never write HardPrivacyModeOn.
Attribute reports
Track the three privacy states and derive switch state after each report. Preserve the individual cameraPrivacyMode events unchanged.
G350 evidence
The G350 on firmware 4.5.70 advertises all three standard privacy attributes.
A test build implementing the mapping above was installed on the real camera.
Results:
main.switch appeared;
- ON/OFF worked;
- the switch was available in routines;
- after adding the dashboard
states and actions entries, the on/off button appeared on the dashboard tile;
- the existing
cameraPrivacyMode capability remained available.
This was tested successfully in the SmartThings app.
Affected files
profiles/camera.yml
src/sub_drivers/camera/camera_utils/device_configuration.lua
src/sub_drivers/camera/camera_utils/utils.lua
src/sub_drivers/camera/camera_handlers/attribute_handlers.lua
src/sub_drivers/camera/camera_handlers/capability_handlers.lua
src/sub_drivers/camera/init.lua
src/test/test_matter_camera.lua
Suggested regression coverage
Verify:
- Privacy feature + writable soft attributes ->
main.switch is enabled.
- switch OFF writes
true to both supported soft privacy attributes.
- switch ON writes
false to both supported soft privacy attributes.
HardPrivacyModeOn is never written.
- hard privacy active -> switch reports
off.
- mixed soft privacy state -> switch reports
off.
- all supported privacy states inactive -> switch reports
on.
statusLed.switch continues to control only StatusLightEnabled.
- profile includes switch in dashboard state/action, detail view and automation.
Minimal suggested diff
diff --git a/profiles/camera.yml b/profiles/camera.yml
index 1f911a9..dd277cd 100644
--- a/profiles/camera.yml
+++ b/profiles/camera.yml
@@ -35,6 +35,9 @@ components:
- id: cameraPrivacyMode
version: 1
optional: true
+ - id: switch
+ version: 1
+ optional: true
- id: zoneManagement
version: 1
optional: true
@@ -92,6 +95,9 @@ components:
deviceConfig:
dashboard:
states:
+ - component: main
+ capability: switch
+ version: 1
- component: main
capability: imageCapture
version: 1
@@ -106,6 +112,10 @@ deviceConfig:
operator: CONTAINS
operand: T
isOffline: false
+ actions:
+ - component: main
+ capability: switch
+ version: 1
basicPlus:
- displayType: camera
camera:
@@ -134,6 +144,9 @@ deviceConfig:
- component: main
capability: motionSensor
version: 1
+ - component: main
+ capability: switch
+ version: 1
- component: main
capability: videoCapture2
version: 1
@@ -142,7 +155,13 @@ deviceConfig:
- component: main
capability: motionSensor
version: 1
+ - component: main
+ capability: switch
+ version: 1
actions:
+ - component: main
+ capability: switch
+ version: 1
- component: main
capability: videoCapture2
version: 1
diff --git a/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua
index 53b3571..e02d9c5 100644
--- a/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua
+++ b/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua
@@ -24,6 +24,36 @@ CameraAttributeHandlers.enabled_state_factory = function(attribute)
end
end
+local function main_component_has_switch(device)
+ local main = device.profile and device.profile.components and device.profile.components[camera_fields.profile_components.main]
+ for _, capability in pairs((main and main.capabilities) or {}) do
+ if capability.id == capabilities.switch.ID then return true end
+ end
+ return false
+end
+
+local function emit_privacy_switch_state(device, ib)
+ if not main_component_has_switch(device) then return end
+ local switch_state = camera_utils.get_privacy_switch_state(device)
+ if switch_state == "off" then
+ device:emit_event_for_endpoint(ib, capabilities.switch.switch.off())
+ elseif switch_state == "on" then
+ device:emit_event_for_endpoint(ib, capabilities.switch.switch.on())
+ end
+end
+
+CameraAttributeHandlers.privacy_state_factory = function(attribute, matter_attribute)
+ return function(driver, device, ib, response)
+ local enabled = ib.data.value == true
+ device:emit_event_for_endpoint(ib, attribute(enabled and "enabled" or "disabled"))
+ camera_utils.update_privacy_state(device, matter_attribute.ID, enabled)
+ if attribute == capabilities.cameraPrivacyMode.hardPrivacyMode then
+ camera_utils.update_supported_attributes(device, ib, capabilities.cameraPrivacyMode, "hardPrivacyMode")
+ end
+ emit_privacy_switch_state(device, ib)
+ end
+end
+
CameraAttributeHandlers.night_vision_factory = function(attribute)
return function(driver, device, ib, response)
if camera_fields.tri_state_map[ib.data.value] then
diff --git a/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/src/sub_drivers/camera/camera_handlers/capability_handlers.lua
index a26afd0..d7116a8 100644
--- a/src/sub_drivers/camera/camera_handlers/capability_handlers.lua
+++ b/src/sub_drivers/camera/camera_handlers/capability_handlers.lua
@@ -120,14 +120,37 @@ function CameraCapabilityHandlers.handle_set_status_light_mode(driver, device, c
clusters.Global.types.ThreeLevelAutoEnum[level_auto_value]))
end
-function CameraCapabilityHandlers.handle_status_led_on(driver, device, cmd)
- local endpoint_id = device:component_to_endpoint(cmd.component)
- device:send(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(device, endpoint_id, true))
+local function set_camera_privacy_from_switch(device, privacy_enabled)
+ local endpoint_id = device:component_to_endpoint(camera_fields.profile_components.main)
+ device:send(clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled:write(
+ device, endpoint_id, privacy_enabled
+ ))
+ device:send(clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled:write(
+ device, endpoint_id, privacy_enabled
+ ))
end
-function CameraCapabilityHandlers.handle_status_led_off(driver, device, cmd)
- local endpoint_id = device:component_to_endpoint(cmd.component)
- device:send(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(device, endpoint_id, false))
+local function set_status_led(device, enabled)
+ local endpoint_id = device:component_to_endpoint(camera_fields.profile_components.statusLed)
+ device:send(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(device, endpoint_id, enabled))
+end
+
+function CameraCapabilityHandlers.handle_switch_on(driver, device, cmd)
+ if cmd.component == camera_fields.profile_components.main then
+ -- Camera ON means privacy OFF. HardPrivacyModeOn remains read-only.
+ set_camera_privacy_from_switch(device, false)
+ elseif cmd.component == camera_fields.profile_components.statusLed then
+ set_status_led(device, true)
+ end
+end
+
+function CameraCapabilityHandlers.handle_switch_off(driver, device, cmd)
+ if cmd.component == camera_fields.profile_components.main then
+ -- Camera OFF means enable both writable soft privacy modes.
+ set_camera_privacy_from_switch(device, true)
+ elseif cmd.component == camera_fields.profile_components.statusLed then
+ set_status_led(device, false)
+ end
end
function CameraCapabilityHandlers.handle_audio_recording(driver, device, cmd)
diff --git a/src/sub_drivers/camera/camera_utils/device_configuration.lua b/src/sub_drivers/camera/camera_utils/device_configuration.lua
index 0619194..45c4e88 100644
--- a/src/sub_drivers/camera/camera_utils/device_configuration.lua
+++ b/src/sub_drivers/camera/camera_utils/device_configuration.lua
@@ -236,6 +236,7 @@ function CameraDeviceConfiguration.match_profile(device)
end
if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.PRIVACY) then
table.insert(main_component_capabilities, capabilities.cameraPrivacyMode.ID)
+ table.insert(main_component_capabilities, capabilities.switch.ID)
end
if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then
table.insert(speaker_component_capabilities, capabilities.audioMute.ID)
diff --git a/src/sub_drivers/camera/camera_utils/fields.lua b/src/sub_drivers/camera/camera_utils/fields.lua
index c88f177..69881fa 100644
--- a/src/sub_drivers/camera/camera_utils/fields.lua
+++ b/src/sub_drivers/camera/camera_utils/fields.lua
@@ -16,6 +16,9 @@ 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.SOFT_RECORDING_PRIVACY = "__soft_recording_privacy"
+CameraFields.SOFT_LIVESTREAM_PRIVACY = "__soft_livestream_privacy"
+CameraFields.HARD_PRIVACY = "__hard_privacy"
CameraFields.CameraAVSMFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.CameraAvStreamManagement.ID }
CameraFields.CameraAVSULMFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.CameraAvSettingsUserLevelManagement.ID }
diff --git a/src/sub_drivers/camera/camera_utils/utils.lua b/src/sub_drivers/camera/camera_utils/utils.lua
index 78792b9..5d0ffda 100644
--- a/src/sub_drivers/camera/camera_utils/utils.lua
+++ b/src/sub_drivers/camera/camera_utils/utils.lua
@@ -82,6 +82,33 @@ function CameraUtils.feature_supported(device, cluster_id, feature_flag)
return #device:get_endpoints(cluster_id, { feature_bitmap = feature_flag }) > 0
end
+local privacy_state_fields = {
+ [clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled.ID] = camera_fields.SOFT_RECORDING_PRIVACY,
+ [clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled.ID] = camera_fields.SOFT_LIVESTREAM_PRIVACY,
+ [clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn.ID] = camera_fields.HARD_PRIVACY,
+}
+
+function CameraUtils.update_privacy_state(device, attribute_id, value)
+ local field = privacy_state_fields[attribute_id]
+ if field ~= nil then
+ device:set_field(field, value == true)
+ end
+end
+
+function CameraUtils.get_privacy_switch_state(device)
+ local soft_recording = device:get_field(camera_fields.SOFT_RECORDING_PRIVACY)
+ local soft_livestream = device:get_field(camera_fields.SOFT_LIVESTREAM_PRIVACY)
+ local hard_privacy = device:get_field(camera_fields.HARD_PRIVACY)
+
+ if hard_privacy == true or soft_recording == true or soft_livestream == true then
+ return "off"
+ end
+ if soft_recording == false and soft_livestream == false then
+ return "on"
+ end
+ return nil
+end
+
function CameraUtils.update_supported_attributes(device, ib, capability, attribute)
local attribute_set = device:get_latest_state(
camera_fields.profile_components.main, capability.ID, capability.supportedAttributes.NAME
diff --git a/src/sub_drivers/camera/init.lua b/src/sub_drivers/camera/init.lua
index 2aa698a..b3509e1 100644
--- a/src/sub_drivers/camera/init.lua
+++ b/src/sub_drivers/camera/init.lua
@@ -78,9 +78,9 @@ local camera_handler = {
[clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal.ID] = attribute_handlers.enabled_state_factory(capabilities.imageControl.imageFlipHorizontal),
[clusters.CameraAvStreamManagement.attributes.ImageFlipVertical.ID] = attribute_handlers.enabled_state_factory(capabilities.imageControl.imageFlipVertical),
[clusters.CameraAvStreamManagement.attributes.ImageRotation.ID] = attribute_handlers.image_rotation_handler,
- [clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.softRecordingPrivacyMode),
- [clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.softLivestreamPrivacyMode),
- [clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.hardPrivacyMode),
+ [clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled.ID] = attribute_handlers.privacy_state_factory(capabilities.cameraPrivacyMode.softRecordingPrivacyMode, clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled),
+ [clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled.ID] = attribute_handlers.privacy_state_factory(capabilities.cameraPrivacyMode.softLivestreamPrivacyMode, clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled),
+ [clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn.ID] = attribute_handlers.privacy_state_factory(capabilities.cameraPrivacyMode.hardPrivacyMode, clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn),
[clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport.ID] = attribute_handlers.two_way_talk_support_handler,
[clusters.CameraAvStreamManagement.attributes.SpeakerMuted.ID] = attribute_handlers.muted_handler,
[clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel.ID] = attribute_handlers.volume_level_handler,
@@ -166,8 +166,8 @@ local camera_handler = {
[capabilities.mode.commands.setMode.NAME] = capability_handlers.handle_set_status_light_mode
},
[capabilities.switch.ID] = {
- [capabilities.switch.commands.on.NAME] = capability_handlers.handle_status_led_on,
- [capabilities.switch.commands.off.NAME] = capability_handlers.handle_status_led_off
+ [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on,
+ [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off
},
[capabilities.audioRecording.ID] = {
[capabilities.audioRecording.commands.setAudioRecording.NAME] = capability_handlers.handle_audio_recording
Summary
The Matter camera subdriver already supports the standard Matter Privacy model through the SmartThings
cameraPrivacyModecapability, but a Matter camera with Privacy has no normalmain.switchcapability.That leaves users without the conventional camera on/off control on the dashboard and without the simplest on/off camera control in routines.
A practical mapping has now been tested successfully on a real Matter 1.5.1 camera:
HardPrivacyModeOnremains read-only and must never be written.The existing granular
cameraPrivacyModecapability should remain available;switchis a user-facing master abstraction, not a replacement for the detailed Matter states.Environment / verification
Verified against
SmartThingsCommunity/SmartThingsEdgeDriversmainat commit60bbf2716412fdf04fd59f967bdd37be6995d664(2026-08-12).Real-device validation used:
0x115F / 0x30134.5.70/40050701.5.10x0142The proposed changes are generic Matter-camera changes. No Aqara VID/PID fingerprint or proprietary Aqara cluster handling is required.
Current behavior
The current camera profile has:
cameraPrivacyModeonmain;switchonly on the optionalstatusLedcomponent.See:
profiles/camera.ymllines 29-64The profile's dashboard has no switch state/action, and its detail/routine presentation also omits
main.switch:profiles/camera.ymllines 92-151The current camera capability handlers use
switchonly as the status LED control:src/sub_drivers/camera/init.lualines 151-174The three Matter privacy attributes are already handled:
and the two soft attributes already have write commands through
cameraPrivacyMode.Reproduction
On the G350 before this change, SmartThings exposed:
but no
main.switch.As a result:
Existing SmartThings UI precedent
A cloud-connected Aeotec Cam 360 integrated by SmartThings exposes:
and its presentation includes:
This provides a useful existing SmartThings camera UX precedent.
Expected behavior
When Matter Privacy is supported and at least one writable soft privacy attribute exists, the camera should expose an optional
switchonmain.Suggested semantics:
State derivation:
HardPrivacyModeOnshould contribute to the reported state (offwhen active) but must remain read-only.Suggested code changes
Profile
Add optional
switchtomain:Expose it in the camera presentation:
The explicit
dashboard.actionsentry is important: testing showed that merely adding the capability to the profile/detail/routine configuration did not create an interactive dashboard button.Dynamic profiling
Enable
main.switchonly when AVSM Privacy is supported and at least one writable soft privacy attribute is available.Commands
Dispatch
switchby component somainandstatusLedremain independent:Write only:
Never write
HardPrivacyModeOn.Attribute reports
Track the three privacy states and derive
switchstate after each report. Preserve the individualcameraPrivacyModeevents unchanged.G350 evidence
The G350 on firmware
4.5.70advertises all three standard privacy attributes.A test build implementing the mapping above was installed on the real camera.
Results:
main.switchappeared;statesandactionsentries, the on/off button appeared on the dashboard tile;cameraPrivacyModecapability remained available.This was tested successfully in the SmartThings app.
Affected files
profiles/camera.ymlsrc/sub_drivers/camera/camera_utils/device_configuration.luasrc/sub_drivers/camera/camera_utils/utils.luasrc/sub_drivers/camera/camera_handlers/attribute_handlers.luasrc/sub_drivers/camera/camera_handlers/capability_handlers.luasrc/sub_drivers/camera/init.luasrc/test/test_matter_camera.luaSuggested regression coverage
Verify:
main.switchis enabled.trueto both supported soft privacy attributes.falseto both supported soft privacy attributes.HardPrivacyModeOnis never written.off.off.on.statusLed.switchcontinues to control onlyStatusLightEnabled.Minimal suggested diff