From 9866fc1c01739e4cee0eca9b798d7baee2d114e2 Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Mon, 21 Sep 2026 10:34:36 -0500 Subject: [PATCH 1/5] CHAD-19242: Exclude audioVolume for unusable range --- .../camera_handlers/attribute_handlers.lua | 4 + .../camera_utils/device_configuration.lua | 17 +- .../camera/camera_utils/fields.lua | 2 + .../src/test/test_matter_camera.lua | 146 ++++++++++++++++++ 4 files changed, 167 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index 075f9af600..8f4786c49f 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -68,23 +68,27 @@ end function CameraAttributeHandlers.max_volume_level_handler(driver, device, ib, response) local component = device:endpoint_to_component(ib) local max_volume = ib.data.value + device:set_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. component, max_volume) local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) if max_volume > camera_fields.ABS_VOL_MAX or (min_volume and max_volume <= min_volume) then device.log.warn(string.format("Device reported invalid maximum (%d) %s volume level range value", ib.data.value, component)) max_volume = camera_fields.ABS_VOL_MAX end device:set_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component, max_volume) + camera_cfg.reconcile_profile_and_capabilities(device) end function CameraAttributeHandlers.min_volume_level_handler(driver, device, ib, response) local component = device:endpoint_to_component(ib) local min_volume = ib.data.value + device:set_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. component, min_volume) local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) if min_volume < camera_fields.ABS_VOL_MIN or (max_volume and min_volume >= max_volume) then device.log.warn(string.format("Device reported invalid minimum (%d) %s volume level range value", ib.data.value, component)) min_volume = camera_fields.ABS_VOL_MIN end device:set_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component, min_volume) + camera_cfg.reconcile_profile_and_capabilities(device) end function CameraAttributeHandlers.status_light_enabled_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua index 282d33677f..f7caca0b29 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -41,6 +41,15 @@ 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 volume_range_usable(device, component) + local raw_min_volume = device:get_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. component) + local raw_max_volume = device:get_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. component) + if raw_min_volume == nil or raw_max_volume == nil then + return false + end + return raw_max_volume > raw_min_volume +end + local function build_webrtc_supported_features() return { bundle = true, @@ -258,7 +267,9 @@ function CameraDeviceConfiguration.match_profile(device) table.insert(main_component_capabilities, capabilities.audioRecording.ID) end table.insert(microphone_component_capabilities, capabilities.audioMute.ID) - table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) + if volume_range_usable(device, camera_fields.profile_components.microphone) then + table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) + end end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then table.insert(main_component_capabilities, capabilities.imageCapture.ID) @@ -268,7 +279,9 @@ function CameraDeviceConfiguration.match_profile(device) end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then table.insert(speaker_component_capabilities, capabilities.audioMute.ID) - table.insert(speaker_component_capabilities, capabilities.audioVolume.ID) + if volume_range_usable(device, camera_fields.profile_components.speaker) then + table.insert(speaker_component_capabilities, capabilities.audioVolume.ID) + end end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL) then table.insert(main_component_capabilities, capabilities.imageControl.ID) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua index edb4deb125..8915e4afb8 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua @@ -9,6 +9,8 @@ CameraFields.MAX_ENCODED_PIXEL_RATE = "__max_encoded_pixel_rate" CameraFields.MAX_FRAMES_PER_SECOND = "__max_frames_per_second" CameraFields.MAX_VOLUME_LEVEL = "__max_volume_level" CameraFields.MIN_VOLUME_LEVEL = "__min_volume_level" +CameraFields.RAW_MAX_VOLUME_LEVEL = "__raw_max_volume_level" +CameraFields.RAW_MIN_VOLUME_LEVEL = "__raw_min_volume_level" CameraFields.SUPPORTED_RESOLUTIONS = "__supported_resolutions" CameraFields.MAX_RESOLUTION = "__max_resolution" CameraFields.MIN_RESOLUTION = "__min_resolution" diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index e1467edecf..69d10f93bb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -420,6 +420,15 @@ local additional_subscribed_attributes = { clusters.Switch.server.events.MultiPressComplete } +-- audioVolume is only added to the speaker/microphone components once a usable (max > min) volume +-- range has been reported; seed a usable range directly so unrelated tests don't have to simulate it. +local function seed_usable_volume_range(device) + device:set_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.speaker, 0) + device:set_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.speaker, 200) + device:set_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.microphone, 0) + device:set_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.microphone, 200) +end + local expected_metadata = { optional_component_capabilities = { { @@ -473,6 +482,7 @@ local expected_metadata = { } local function update_device_profile() + seed_usable_volume_range(mock_device) test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { @@ -712,6 +722,7 @@ end test.register_coroutine_test( "Software version change should trigger camera reprofiling when camera endpoint is present", function() + seed_usable_volume_range(mock_device) test.socket.device_lifecycle:__queue_receive( mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } }) ) @@ -965,6 +976,141 @@ test.register_coroutine_test( } ) +local function build_fake_device_with_volume_ranges(raw_levels) + return { + 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.AUDIO | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER, + cluster_type = "SERVER" + } + } + } + }, + get_field = function(_, field) return raw_levels[field] end, + get_endpoints = function() return {} end, + try_update_metadata = function(self, metadata) self.updated_metadata = metadata end, + } +end + +local function component_capabilities(updated_metadata, component_name) + for _, component in ipairs(updated_metadata.optional_component_capabilities) do + if component[1] == component_name then + return component[2] + end + end + return {} +end + +local function list_contains(list, value) + for _, v in ipairs(list) do + if v == value then return true end + end + return false +end + +test.register_coroutine_test( + "audioVolume should be excluded from speaker and microphone when the volume range is unusable or unknown", + function() + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + + -- microphone reports an unusable range (min >= max); speaker hasn't reported a range at all yet + local fake_device = build_fake_device_with_volume_ranges({ + [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_microphone"] = 100, + [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_microphone"] = 100, + }) + + camera_cfg.match_profile(fake_device) + + assert(fake_device.updated_metadata ~= nil, "profile update should be requested") + local speaker_capabilities = component_capabilities(fake_device.updated_metadata, "speaker") + local microphone_capabilities = component_capabilities(fake_device.updated_metadata, "microphone") + + assert(list_contains(speaker_capabilities, capabilities.audioMute.ID), "audioMute should still be present on speaker") + assert(not list_contains(speaker_capabilities, capabilities.audioVolume.ID), "audioVolume should be excluded from speaker when its range is unknown") + assert(list_contains(microphone_capabilities, capabilities.audioMute.ID), "audioMute should still be present on microphone") + assert(not list_contains(microphone_capabilities, capabilities.audioVolume.ID), "audioVolume should be excluded from microphone when min >= max") + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "audioVolume should be included on speaker and microphone when the volume range is usable", + function() + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + + local fake_device = build_fake_device_with_volume_ranges({ + [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_speaker"] = 0, + [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_speaker"] = 200, + [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_microphone"] = 0, + [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_microphone"] = 200, + }) + + camera_cfg.match_profile(fake_device) + + assert(fake_device.updated_metadata ~= nil, "profile update should be requested") + local speaker_capabilities = component_capabilities(fake_device.updated_metadata, "speaker") + local microphone_capabilities = component_capabilities(fake_device.updated_metadata, "microphone") + + assert(list_contains(speaker_capabilities, capabilities.audioVolume.ID), "audioVolume should be included on speaker when max > min") + assert(list_contains(microphone_capabilities, capabilities.audioVolume.ID), "audioVolume should be included on microphone when max > min") + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Speaker audioVolume should be removed from the profile when its volume range becomes unusable", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMaxLevel:build_test_report_data(mock_device, CAMERA_EP, 0) + }) + + local updated_expected_metadata = { + optional_component_capabilities = { + { "main", + { "videoCapture2", "cameraViewportSettings", "videoStreamSettings", "localMediaStorage", "audioRecording", + "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "zoneManagement", + "webrtc", "motionSensor", "sounds" } + }, + { "statusLed", + { "switch", "mode" } + }, + { "speaker", + { "audioMute" } -- audioVolume removed: reported max (0) is no longer greater than the known min (0) + }, + { "microphone", + { "audioMute", "audioVolume" } + }, + { "doorbell", + { "button" } + } + }, + profile = "camera" + } + mock_device:expect_metadata_update(updated_expected_metadata) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + end, + { + min_api_version = 14 + } +) + test.register_coroutine_test( "Camera privacy mode supportedAttributes/supportedCommands should only expose hardPrivacyMode when PRIV feature is absent", function() From 3ffcbca974a2c3b37077187fffea7fb525b26750 Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Mon, 21 Sep 2026 11:03:25 -0500 Subject: [PATCH 2/5] Keep the capability as-is when ranges are unknown --- .../camera_utils/device_configuration.lua | 10 +-- .../src/test/test_matter_camera.lua | 61 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua index f7caca0b29..583098f873 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -41,11 +41,13 @@ 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 volume_range_usable(device, component) +-- Decides whether audioVolume belongs on this component. If the min/max levels aren't both known +-- yet, leaves the capability as-is instead of guessing; otherwise requires max > min to include it. +local function should_include_volume_capability(device, component) local raw_min_volume = device:get_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. component) local raw_max_volume = device:get_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. component) if raw_min_volume == nil or raw_max_volume == nil then - return false + return device:supports_capability(capabilities.audioVolume, component) end return raw_max_volume > raw_min_volume end @@ -267,7 +269,7 @@ function CameraDeviceConfiguration.match_profile(device) table.insert(main_component_capabilities, capabilities.audioRecording.ID) end table.insert(microphone_component_capabilities, capabilities.audioMute.ID) - if volume_range_usable(device, camera_fields.profile_components.microphone) then + if should_include_volume_capability(device, camera_fields.profile_components.microphone) then table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) end end @@ -279,7 +281,7 @@ function CameraDeviceConfiguration.match_profile(device) end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then table.insert(speaker_component_capabilities, capabilities.audioMute.ID) - if volume_range_usable(device, camera_fields.profile_components.speaker) then + if should_include_volume_capability(device, camera_fields.profile_components.speaker) then table.insert(speaker_component_capabilities, capabilities.audioVolume.ID) end end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 69d10f93bb..0c464cb2bf 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -997,6 +997,7 @@ local function build_fake_device_with_volume_ranges(raw_levels) }, get_field = function(_, field) return raw_levels[field] end, get_endpoints = function() return {} end, + supports_capability = function() return false end, try_update_metadata = function(self, metadata) self.updated_metadata = metadata end, } end @@ -1070,6 +1071,66 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "audioVolume should be preserved on a component whose volume range isn't fully known yet", + function() + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + + -- Speaker already has audioVolume from a prior match; its raw min/max aren't in raw_levels (nil), + -- so it should be carried over as-is rather than being dropped for lack of information. + local existing_components = { + speaker = { capabilities = { audioVolume = { id = capabilities.audioVolume.ID } } } + } + local raw_levels = { + [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_microphone"] = 100, + [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_microphone"] = 100, -- unusable; forces a real profile change so we can inspect it + } + local fake_device = { + profile = { components = existing_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.AUDIO | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER, + cluster_type = "SERVER" + } + } + } + }, + get_field = function(_, field) return raw_levels[field] end, + get_endpoints = function() return {} end, + supports_capability = function(_, capability, component) + local comp = existing_components[component] + if not comp then return false end + for _, cap in pairs(comp.capabilities) do + if cap.id == capability.ID then return true end + end + return false + end, + try_update_metadata = function(self, metadata) self.updated_metadata = metadata end, + } + + camera_cfg.match_profile(fake_device) + + assert(fake_device.updated_metadata ~= nil, "profile update should be requested") + local speaker_capabilities = component_capabilities(fake_device.updated_metadata, "speaker") + local microphone_capabilities = component_capabilities(fake_device.updated_metadata, "microphone") + + assert(list_contains(speaker_capabilities, capabilities.audioVolume.ID), "audioVolume should be preserved on speaker while its range is unknown") + assert(list_contains(microphone_capabilities, capabilities.audioMute.ID), "audioMute should still be present on microphone") + assert(not list_contains(microphone_capabilities, capabilities.audioVolume.ID), "audioVolume should be excluded from microphone when min >= max") + end, + { + min_api_version = 14 + } +) + test.register_coroutine_test( "Speaker audioVolume should be removed from the profile when its volume range becomes unusable", function() From 0750c21ce37a11cbd8c85a9cbd1df228a9b5691e Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Mon, 21 Sep 2026 14:30:45 -0500 Subject: [PATCH 3/5] Update gating for min/max attr subscription, fix log --- .../camera_handlers/attribute_handlers.lua | 6 +- .../sub_drivers/camera/camera_utils/utils.lua | 14 ++- .../src/test/test_matter_camera.lua | 91 +++++++++++++++++++ 3 files changed, 104 insertions(+), 7 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index 8f4786c49f..3897d3ff65 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -70,7 +70,8 @@ function CameraAttributeHandlers.max_volume_level_handler(driver, device, ib, re local max_volume = ib.data.value device:set_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. component, max_volume) local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) - if max_volume > camera_fields.ABS_VOL_MAX or (min_volume and max_volume <= min_volume) then + -- min == max is a valid (non-adjustable) range per the Matter spec, so only max < min is malformed. + if max_volume > camera_fields.ABS_VOL_MAX or (min_volume and max_volume < min_volume) then device.log.warn(string.format("Device reported invalid maximum (%d) %s volume level range value", ib.data.value, component)) max_volume = camera_fields.ABS_VOL_MAX end @@ -83,7 +84,8 @@ function CameraAttributeHandlers.min_volume_level_handler(driver, device, ib, re local min_volume = ib.data.value device:set_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. component, min_volume) local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) - if min_volume < camera_fields.ABS_VOL_MIN or (max_volume and min_volume >= max_volume) then + -- min == max is a valid (non-adjustable) range per the Matter spec, so only min > max is malformed. + if min_volume < camera_fields.ABS_VOL_MIN or (max_volume and min_volume > max_volume) then device.log.warn(string.format("Device reported invalid minimum (%d) %s volume level range value", ib.data.value, component)) min_volume = camera_fields.ABS_VOL_MIN end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index 78792b94b3..eee790a55d 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -212,18 +212,22 @@ function CameraUtils.subscribe(device) clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin }, + -- SpeakerMaxLevel/MinLevel and MicrophoneMaxLevel/MinLevel are subscribed alongside audioMute + -- (unconditional on the AUDIO/SPEAKER feature) rather than audioVolume, since audioVolume's own + -- presence in the profile depends on first discovering these values -- gating their subscription + -- on audioVolume already being present would make that discovery impossible. [capabilities.audioMute.ID] = { clusters.CameraAvStreamManagement.attributes.SpeakerMuted, - clusters.CameraAvStreamManagement.attributes.MicrophoneMuted - }, - [capabilities.audioVolume.ID] = { - clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted, clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel }, + [capabilities.audioVolume.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel + }, [capabilities.mode.ID] = { clusters.CameraAvStreamManagement.attributes.StatusLightBrightness }, diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 0c464cb2bf..81560a87b3 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -1172,6 +1172,97 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Speaker/Microphone volume range attributes should stay subscribed even when audioVolume isn't granted yet", + function() + -- Fresh pairing: no volume range has been reported yet, so audioVolume shouldn't be granted on + -- the very first profile match. + 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) + }) + }) + local first_pass_metadata = { + optional_component_capabilities = { + { "main", + { "videoCapture2", "cameraViewportSettings", "videoStreamSettings", "localMediaStorage", "audioRecording", + "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "zoneManagement", + "webrtc", "motionSensor", "sounds" } + }, + { "statusLed", { "switch", "mode" } }, + { "speaker", { "audioMute" } }, + { "microphone", { "audioMute" } }, + { "doorbell", { "button" } } + }, + profile = "camera" + } + mock_device:expect_metadata_update(first_pass_metadata) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.wait_for_events() + + local first_pass_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = first_pass_metadata.optional_component_capabilities} + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = first_pass_profile })) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.supportedFeatures( + {audio="sendrecv", bundle=true, order="audio/video", supportTrickleICE=true, turnSource="player", video="recvonly"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.supportedAttributes( + {"pan", "panRange", "tilt", "tiltRange", "zoom", "zoomRange", "presets", "maxPresets"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.supportedFeatures( + {"triggerAugmentation", "perZoneSensitivity"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.localMediaStorage.supportedAttributes( + {"localVideoRecording"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.audioRecording.audioRecording("enabled")) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports", "watermark", "onScreenDisplay"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes( + {"softRecordingPrivacyMode", "softLivestreamPrivacyMode"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedCommands( + {"setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode"} + )) + ) + + -- audioVolume isn't active yet, so its current-value attributes shouldn't be (re-)subscribed, but + -- the min/max discovery attributes -- now bundled with audioMute -- must still be. Without that, + -- the driver could never learn the range needed to decide whether audioVolume belongs at all. + for _, attr in ipairs(additional_subscribed_attributes) do + if attr ~= clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel and + attr ~= clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel then + subscribe_request:merge(attr:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + end, + { + min_api_version = 14 + } +) + test.register_coroutine_test( "Camera privacy mode supportedAttributes/supportedCommands should only expose hardPrivacyMode when PRIV feature is absent", function() From b6314ae119f2bf50c753fb95848a13dcab9369c8 Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Tue, 22 Sep 2026 01:18:46 -0500 Subject: [PATCH 4/5] Guard against divide-by-zero --- .../camera_handlers/attribute_handlers.lua | 9 +++- .../src/test/test_matter_camera.lua | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index 3897d3ff65..aacb1d7e63 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -59,8 +59,15 @@ function CameraAttributeHandlers.volume_level_handler(driver, device, ib, respon local component = device:endpoint_to_component(ib) local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MAX local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MIN - -- Convert from [min_volume, max_volume] to [0, 100] before emitting capability local limited_range = max_volume - min_volume + if limited_range <= 0 then + -- Non-adjustable (min == max) range: nothing to normalize against. A report can still arrive here + -- while the profile update dropping audioVolume is in flight, so report a fixed value instead of + -- dividing by zero. + device:emit_event_for_endpoint(ib, capabilities.audioVolume.volume(0)) + return + end + -- Convert from [min_volume, max_volume] to [0, 100] before emitting capability local normalized_volume = utils.round((ib.data.value - min_volume) * 100.0 / limited_range) device:emit_event_for_endpoint(ib, capabilities.audioVolume.volume(normalized_volume)) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 81560a87b3..9039fadf01 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -1580,6 +1580,56 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "A volume level report racing a zero-width range update should not divide by zero", + function() + update_device_profile() + test.wait_for_events() + + -- Narrow the range to min == max. This drops audioVolume from the profile, but the internal + -- MAX/MIN_VOLUME_LEVEL fields used for normalization are updated synchronously, before the + -- platform round-trips the narrowed profile back down. + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMaxLevel:build_test_report_data(mock_device, CAMERA_EP, 100) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMinLevel:build_test_report_data(mock_device, CAMERA_EP, 100) + }) + local updated_expected_metadata = { + optional_component_capabilities = { + { "main", + { "videoCapture2", "cameraViewportSettings", "videoStreamSettings", "localMediaStorage", "audioRecording", + "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "zoneManagement", + "webrtc", "motionSensor", "sounds" } + }, + { "statusLed", { "switch", "mode" } }, + { "speaker", { "audioMute" } }, -- audioVolume dropped: the range is now zero-width (100 == 100) + { "microphone", { "audioMute", "audioVolume" } }, + { "doorbell", { "button" } } + }, + profile = "camera" + } + mock_device:expect_metadata_update(updated_expected_metadata) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.wait_for_events() + + -- A late SpeakerVolumeLevel report can still arrive before the narrowed profile comes back down. + -- It should report a fixed value instead of dividing by (max - min) == 0. + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 100) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("speaker", capabilities.audioVolume.volume(0)) + ) + end, + { + min_api_version = 14 + } +) + test.register_coroutine_test( "Status Light Enabled reports should generate appropriate events", function() From d29da26b6a5f66776cb3cc006ce411cfa5b4b377 Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Wed, 23 Sep 2026 11:11:02 -0500 Subject: [PATCH 5/5] Track the raw value directly Co-authored-by: Harrison Carter --- .../camera_handlers/attribute_handlers.lua | 31 ++++++++++--------- .../camera_handlers/capability_handlers.lua | 12 +++---- .../camera_utils/device_configuration.lua | 14 +++++---- .../camera/camera_utils/fields.lua | 2 -- .../sub_drivers/camera/camera_utils/utils.lua | 8 +++++ .../src/test/test_matter_camera.lua | 24 +++++++------- 6 files changed, 50 insertions(+), 41 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index aacb1d7e63..0b08bf27b0 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -57,13 +57,10 @@ end function CameraAttributeHandlers.volume_level_handler(driver, device, ib, response) local component = device:endpoint_to_component(ib) - local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MAX - local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MIN + local max_volume = camera_utils.get_field_for_component(device, camera_fields.MAX_VOLUME_LEVEL, component) or camera_fields.ABS_VOL_MAX + local min_volume = camera_utils.get_field_for_component(device, camera_fields.MIN_VOLUME_LEVEL, component) or camera_fields.ABS_VOL_MIN local limited_range = max_volume - min_volume if limited_range <= 0 then - -- Non-adjustable (min == max) range: nothing to normalize against. A report can still arrive here - -- while the profile update dropping audioVolume is in flight, so report a fixed value instead of - -- dividing by zero. device:emit_event_for_endpoint(ib, capabilities.audioVolume.volume(0)) return end @@ -75,28 +72,32 @@ end function CameraAttributeHandlers.max_volume_level_handler(driver, device, ib, response) local component = device:endpoint_to_component(ib) local max_volume = ib.data.value - device:set_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. component, max_volume) - local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) - -- min == max is a valid (non-adjustable) range per the Matter spec, so only max < min is malformed. - if max_volume > camera_fields.ABS_VOL_MAX or (min_volume and max_volume < min_volume) then + local min_volume = camera_utils.get_field_for_component(device, camera_fields.MIN_VOLUME_LEVEL, component) + -- min == max is a valid (non-adjustable) range per the Matter spec, so only max < min is malformed; + -- an out-of-bounds value is clamped, but a malformed relationship is only logged and left as-is, + -- since volume_level_handler already guards against a non-positive range. + if max_volume > camera_fields.ABS_VOL_MAX then device.log.warn(string.format("Device reported invalid maximum (%d) %s volume level range value", ib.data.value, component)) max_volume = camera_fields.ABS_VOL_MAX + elseif min_volume and max_volume < min_volume then + device.log.warn(string.format("Device reported invalid maximum (%d) %s volume level range value", ib.data.value, component)) end - device:set_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component, max_volume) + camera_utils.set_field_for_component(device, camera_fields.MAX_VOLUME_LEVEL, component, max_volume) camera_cfg.reconcile_profile_and_capabilities(device) end function CameraAttributeHandlers.min_volume_level_handler(driver, device, ib, response) local component = device:endpoint_to_component(ib) local min_volume = ib.data.value - device:set_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. component, min_volume) - local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) - -- min == max is a valid (non-adjustable) range per the Matter spec, so only min > max is malformed. - if min_volume < camera_fields.ABS_VOL_MIN or (max_volume and min_volume > max_volume) then + local max_volume = camera_utils.get_field_for_component(device, camera_fields.MAX_VOLUME_LEVEL, component) + -- See max_volume_level_handler: min == max is valid, only min > max is malformed and only logged. + if min_volume < camera_fields.ABS_VOL_MIN then device.log.warn(string.format("Device reported invalid minimum (%d) %s volume level range value", ib.data.value, component)) min_volume = camera_fields.ABS_VOL_MIN + elseif max_volume and min_volume > max_volume then + device.log.warn(string.format("Device reported invalid minimum (%d) %s volume level range value", ib.data.value, component)) end - device:set_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component, min_volume) + camera_utils.set_field_for_component(device, camera_fields.MIN_VOLUME_LEVEL, component, min_volume) camera_cfg.reconcile_profile_and_capabilities(device) end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua index ef52efcdbe..ebf0411f99 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua @@ -59,8 +59,8 @@ end function CameraCapabilityHandlers.handle_set_volume(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) - local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX - local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + local max_volume = camera_utils.get_field_for_component(device, camera_fields.MAX_VOLUME_LEVEL, cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = camera_utils.get_field_for_component(device, camera_fields.MIN_VOLUME_LEVEL, cmd.component) or camera_fields.ABS_VOL_MIN -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute local volume_range = max_volume - min_volume local volume = utils.round(cmd.args.volume * volume_range / 100.0 + min_volume) @@ -75,8 +75,8 @@ end function CameraCapabilityHandlers.handle_volume_up(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) - local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX - local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + local max_volume = camera_utils.get_field_for_component(device, camera_fields.MAX_VOLUME_LEVEL, cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = camera_utils.get_field_for_component(device, camera_fields.MIN_VOLUME_LEVEL, cmd.component) or camera_fields.ABS_VOL_MIN local volume = device:get_latest_state(cmd.component, capabilities.audioVolume.ID, capabilities.audioVolume.volume.NAME) if not volume or volume >= max_volume then return end -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute @@ -91,8 +91,8 @@ end function CameraCapabilityHandlers.handle_volume_down(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) - local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX - local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + local max_volume = camera_utils.get_field_for_component(device, camera_fields.MAX_VOLUME_LEVEL, cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = camera_utils.get_field_for_component(device, camera_fields.MIN_VOLUME_LEVEL, cmd.component) or camera_fields.ABS_VOL_MIN local volume = device:get_latest_state(cmd.component, capabilities.audioVolume.ID, capabilities.audioVolume.volume.NAME) if not volume or volume <= min_volume then return end -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua index 583098f873..8a5db7c5b5 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -41,15 +41,17 @@ 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 --- Decides whether audioVolume belongs on this component. If the min/max levels aren't both known --- yet, leaves the capability as-is instead of guessing; otherwise requires max > min to include it. +--- Leaves audioVolume as-is if the min/max levels aren't both known yet instead of guessing; +--- otherwise requires max > min to include it. +--- @param component string a profile component name +--- @return should_include boolean whether audioVolume belongs on the component argument. local function should_include_volume_capability(device, component) - local raw_min_volume = device:get_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. component) - local raw_max_volume = device:get_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. component) - if raw_min_volume == nil or raw_max_volume == nil then + local min_volume = camera_utils.get_field_for_component(device, camera_fields.MIN_VOLUME_LEVEL, component) + local max_volume = camera_utils.get_field_for_component(device, camera_fields.MAX_VOLUME_LEVEL, component) + if min_volume == nil or max_volume == nil then return device:supports_capability(capabilities.audioVolume, component) end - return raw_max_volume > raw_min_volume + return max_volume > min_volume end local function build_webrtc_supported_features() diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua index 8915e4afb8..edb4deb125 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua @@ -9,8 +9,6 @@ CameraFields.MAX_ENCODED_PIXEL_RATE = "__max_encoded_pixel_rate" CameraFields.MAX_FRAMES_PER_SECOND = "__max_frames_per_second" CameraFields.MAX_VOLUME_LEVEL = "__max_volume_level" CameraFields.MIN_VOLUME_LEVEL = "__min_volume_level" -CameraFields.RAW_MAX_VOLUME_LEVEL = "__raw_max_volume_level" -CameraFields.RAW_MIN_VOLUME_LEVEL = "__raw_min_volume_level" CameraFields.SUPPORTED_RESOLUTIONS = "__supported_resolutions" CameraFields.MAX_RESOLUTION = "__max_resolution" CameraFields.MIN_RESOLUTION = "__min_resolution" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index eee790a55d..5cc7dee498 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -82,6 +82,14 @@ function CameraUtils.feature_supported(device, cluster_id, feature_flag) return #device:get_endpoints(cluster_id, { feature_bitmap = feature_flag }) > 0 end +function CameraUtils.get_field_for_component(device, field, component) + return device:get_field(string.format("%s_%s", field, component)) +end + +function CameraUtils.set_field_for_component(device, field, component, value, additional_params) + device:set_field(string.format("%s_%s", field, component), value, additional_params) +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/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 9039fadf01..6f5b1251eb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -423,10 +423,10 @@ local additional_subscribed_attributes = { -- audioVolume is only added to the speaker/microphone components once a usable (max > min) volume -- range has been reported; seed a usable range directly so unrelated tests don't have to simulate it. local function seed_usable_volume_range(device) - device:set_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.speaker, 0) - device:set_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.speaker, 200) - device:set_field(camera_fields.RAW_MIN_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.microphone, 0) - device:set_field(camera_fields.RAW_MAX_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.microphone, 200) + device:set_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.speaker, 0) + device:set_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.speaker, 200) + device:set_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.microphone, 0) + device:set_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. camera_fields.profile_components.microphone, 200) end local expected_metadata = { @@ -1025,8 +1025,8 @@ test.register_coroutine_test( -- microphone reports an unusable range (min >= max); speaker hasn't reported a range at all yet local fake_device = build_fake_device_with_volume_ranges({ - [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_microphone"] = 100, - [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_microphone"] = 100, + [camera_fields.MIN_VOLUME_LEVEL .. "_microphone"] = 100, + [camera_fields.MAX_VOLUME_LEVEL .. "_microphone"] = 100, }) camera_cfg.match_profile(fake_device) @@ -1051,10 +1051,10 @@ test.register_coroutine_test( local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" local fake_device = build_fake_device_with_volume_ranges({ - [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_speaker"] = 0, - [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_speaker"] = 200, - [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_microphone"] = 0, - [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_microphone"] = 200, + [camera_fields.MIN_VOLUME_LEVEL .. "_speaker"] = 0, + [camera_fields.MAX_VOLUME_LEVEL .. "_speaker"] = 200, + [camera_fields.MIN_VOLUME_LEVEL .. "_microphone"] = 0, + [camera_fields.MAX_VOLUME_LEVEL .. "_microphone"] = 200, }) camera_cfg.match_profile(fake_device) @@ -1082,8 +1082,8 @@ test.register_coroutine_test( speaker = { capabilities = { audioVolume = { id = capabilities.audioVolume.ID } } } } local raw_levels = { - [camera_fields.RAW_MIN_VOLUME_LEVEL .. "_microphone"] = 100, - [camera_fields.RAW_MAX_VOLUME_LEVEL .. "_microphone"] = 100, -- unusable; forces a real profile change so we can inspect it + [camera_fields.MIN_VOLUME_LEVEL .. "_microphone"] = 100, + [camera_fields.MAX_VOLUME_LEVEL .. "_microphone"] = 100, -- unusable; forces a real profile change so we can inspect it } local fake_device = { profile = { components = existing_components },