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 140ba6d313..b32b1dd55c 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 @@ -466,7 +466,12 @@ function CameraAttributeHandlers.camera_av_stream_management_attribute_list_hand attribute_ids = attribute_ids, } device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true}) - camera_cfg.match_profile(device, status_light_enabled_present, status_light_brightness_present) + camera_cfg.update_status_light_attribute_presence(device, status_light_enabled_present, status_light_brightness_present) + camera_cfg.reconcile_profile_and_capabilities(device) +end + +function CameraAttributeHandlers.camera_feature_map_handler(driver, device, ib, response) + camera_cfg.reconcile_profile_and_capabilities(device) end return CameraAttributeHandlers 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 f709d25420..c2ea259aa1 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 @@ -12,6 +12,152 @@ local switch_utils = require "switch_utils.utils" local CameraDeviceConfiguration = {} +local managed_capability_map = { + { key = "webrtc", capability = capabilities.webrtc }, + { key = "ptz", capability = capabilities.mechanicalPanTiltZoom }, + { key = "zone_management", capability = capabilities.zoneManagement }, + { key = "local_media_storage", capability = capabilities.localMediaStorage }, + { key = "audio_recording", capability = capabilities.audioRecording }, + { key = "video_stream_settings", capability = capabilities.videoStreamSettings }, + { key = "camera_privacy_mode", capability = capabilities.cameraPrivacyMode }, +} + +local function get_status_light_presence(device) + return device:get_field(camera_fields.STATUS_LIGHT_ENABLED_PRESENT), + device:get_field(camera_fields.STATUS_LIGHT_BRIGHTNESS_PRESENT) +end + +local function set_status_light_presence(device, status_light_enabled_present, status_light_brightness_present) + device:set_field(camera_fields.STATUS_LIGHT_ENABLED_PRESENT, status_light_enabled_present == true, { persist = true }) + device:set_field(camera_fields.STATUS_LIGHT_BRIGHTNESS_PRESENT, status_light_brightness_present == true, { persist = true }) +end + +local function build_webrtc_supported_features() + return { + bundle = true, + order = "audio/video", + audio = "sendrecv", + video = "recvonly", + turnSource = "player", + supportTrickleICE = true + } +end + +local function build_ptz_supported_attributes(device) + local supported_attributes = {} + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then + table.insert(supported_attributes, "pan") + table.insert(supported_attributes, "panRange") + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + table.insert(supported_attributes, "tilt") + table.insert(supported_attributes, "tiltRange") + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then + table.insert(supported_attributes, "zoom") + table.insert(supported_attributes, "zoomRange") + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPRESETS) then + table.insert(supported_attributes, "presets") + table.insert(supported_attributes, "maxPresets") + end + return supported_attributes +end + +local function build_zone_management_supported_features(device) + local supported_features = { "triggerAugmentation" } + if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then + table.insert(supported_features, "perZoneSensitivity") + end + return supported_features +end + +local function build_local_media_storage_supported_attributes(device) + local supported_attributes = {} + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + table.insert(supported_attributes, "localVideoRecording") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then + table.insert(supported_attributes, "localSnapshotRecording") + end + return supported_attributes +end + +local function build_video_stream_settings_supported_features(device) + local supported_features = {} + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + table.insert(supported_features, "liveStreaming") + table.insert(supported_features, "clipRecording") + table.insert(supported_features, "perStreamViewports") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then + table.insert(supported_features, "watermark") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then + table.insert(supported_features, "onScreenDisplay") + end + return supported_features +end + +local function build_camera_privacy_supported_attributes() + return { "softRecordingPrivacyMode", "softLivestreamPrivacyMode" } +end + +local function build_camera_privacy_supported_commands() + return { "setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode" } +end + +local function capabilities_needing_reinit(device) + local main = camera_fields.profile_components.main + + local capabilities_to_reinit = {} + + local function state_differs(capability, attribute_name, expected) + local current = device:get_latest_state(main, capability.ID, attribute_name) + return not switch_utils.deep_equals(current, expected, { ignore_functions = true }) + end + + if device:supports_capability(capabilities.webrtc) and + state_differs(capabilities.webrtc, capabilities.webrtc.supportedFeatures.NAME, build_webrtc_supported_features()) then + capabilities_to_reinit.webrtc = true + end + + if device:supports_capability(capabilities.mechanicalPanTiltZoom) and + state_differs(capabilities.mechanicalPanTiltZoom, capabilities.mechanicalPanTiltZoom.supportedAttributes.NAME, build_ptz_supported_attributes(device)) then + capabilities_to_reinit.ptz = true + end + + if device:supports_capability(capabilities.zoneManagement) and + state_differs(capabilities.zoneManagement, capabilities.zoneManagement.supportedFeatures.NAME, build_zone_management_supported_features(device)) then + capabilities_to_reinit.zone_management = true + end + + if device:supports_capability(capabilities.localMediaStorage) and + state_differs(capabilities.localMediaStorage, capabilities.localMediaStorage.supportedAttributes.NAME, build_local_media_storage_supported_attributes(device)) then + capabilities_to_reinit.local_media_storage = true + end + + if device:supports_capability(capabilities.audioRecording) then + local audio_enabled_state = device:get_latest_state(main, capabilities.audioRecording.ID, capabilities.audioRecording.audioRecording.NAME) + if audio_enabled_state == nil then + capabilities_to_reinit.audio_recording = true + end + end + + if device:supports_capability(capabilities.videoStreamSettings) and + state_differs(capabilities.videoStreamSettings, capabilities.videoStreamSettings.supportedFeatures.NAME, build_video_stream_settings_supported_features(device)) then + capabilities_to_reinit.video_stream_settings = true + end + + if device:supports_capability(capabilities.cameraPrivacyMode) and + (state_differs(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedAttributes.NAME, build_camera_privacy_supported_attributes()) or + state_differs(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedCommands.NAME, build_camera_privacy_supported_commands())) then + capabilities_to_reinit.camera_privacy_mode = true + end + + return capabilities_to_reinit +end + function CameraDeviceConfiguration.create_child_devices(driver, device) local num_floodlight_eps = 0 local parent_child_device = false @@ -41,7 +187,9 @@ function CameraDeviceConfiguration.create_child_devices(driver, device) end end -function CameraDeviceConfiguration.match_profile(device, status_light_enabled_present, status_light_brightness_present) +function CameraDeviceConfiguration.match_profile(device) + local status_light_enabled_present, status_light_brightness_present = get_status_light_presence(device) + local profile_update_requested = false local optional_supported_component_capabilities = {} local main_component_capabilities = {} local status_led_component_capabilities = {} @@ -145,78 +293,42 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr end if camera_utils.optional_capabilities_list_changed(optional_supported_component_capabilities, device.profile.components) then + profile_update_requested = true device:try_update_metadata({profile = "camera", optional_component_capabilities = optional_supported_component_capabilities}) if #doorbell_endpoints > 0 then 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 + + return profile_update_requested end local function init_webrtc(device) if device:supports_capability(capabilities.webrtc) then - -- TODO: Check for individual audio/video and talkback features local transport_provider_ep_ids = device:get_endpoints(clusters.WebRTCTransportProvider.ID) - device:emit_event_for_endpoint(transport_provider_ep_ids[1], capabilities.webrtc.supportedFeatures({ - value = { - bundle = true, - order = "audio/video", - audio = "sendrecv", - video = "recvonly", - turnSource = "player", - supportTrickleICE = true - } - })) + device:emit_event_for_endpoint(transport_provider_ep_ids[1], capabilities.webrtc.supportedFeatures(build_webrtc_supported_features())) end end local function init_ptz(device) if device:supports_capability(capabilities.mechanicalPanTiltZoom) then - local supported_attributes = {} - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then - table.insert(supported_attributes, "pan") - table.insert(supported_attributes, "panRange") - end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then - table.insert(supported_attributes, "tilt") - table.insert(supported_attributes, "tiltRange") - end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then - table.insert(supported_attributes, "zoom") - table.insert(supported_attributes, "zoomRange") - end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPRESETS) then - table.insert(supported_attributes, "presets") - table.insert(supported_attributes, "maxPresets") - end local av_settings_ep_ids = device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID) - device:emit_event_for_endpoint(av_settings_ep_ids[1], capabilities.mechanicalPanTiltZoom.supportedAttributes(supported_attributes)) + device:emit_event_for_endpoint(av_settings_ep_ids[1], capabilities.mechanicalPanTiltZoom.supportedAttributes(build_ptz_supported_attributes(device))) end end local function init_zone_management(device) if device:supports_capability(capabilities.zoneManagement) then - local supported_features = {} - table.insert(supported_features, "triggerAugmentation") - if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then - table.insert(supported_features, "perZoneSensitivity") - end local zone_management_ep_ids = device:get_endpoints(clusters.ZoneManagement.ID) - device:emit_event_for_endpoint(zone_management_ep_ids[1], capabilities.zoneManagement.supportedFeatures(supported_features)) + device:emit_event_for_endpoint(zone_management_ep_ids[1], capabilities.zoneManagement.supportedFeatures(build_zone_management_supported_features(device))) end end local function init_local_media_storage(device) if device:supports_capability(capabilities.localMediaStorage) then - local supported_attributes = {} - if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then - table.insert(supported_attributes, "localVideoRecording") - end - if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then - table.insert(supported_attributes, "localSnapshotRecording") - end local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.localMediaStorage.supportedAttributes(supported_attributes)) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.localMediaStorage.supportedAttributes(build_local_media_storage_supported_attributes(device))) end end @@ -235,33 +347,16 @@ end local function init_video_stream_settings(device) if device:supports_capability(capabilities.videoStreamSettings) then - local supported_features = {} - if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then - table.insert(supported_features, "liveStreaming") - table.insert(supported_features, "clipRecording") - table.insert(supported_features, "perStreamViewports") - end - if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then - table.insert(supported_features, "watermark") - end - if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then - table.insert(supported_features, "onScreenDisplay") - end local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.videoStreamSettings.supportedFeatures(supported_features)) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.videoStreamSettings.supportedFeatures(build_video_stream_settings_supported_features(device))) end end local function init_camera_privacy_mode(device) if device:supports_capability(capabilities.cameraPrivacyMode) then - local supported_attributes, supported_commands = {}, {} - table.insert(supported_attributes, "softRecordingPrivacyMode") - table.insert(supported_attributes, "softLivestreamPrivacyMode") - table.insert(supported_commands, "setSoftRecordingPrivacyMode") - table.insert(supported_commands, "setSoftLivestreamPrivacyMode") 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(supported_attributes)) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(supported_commands)) + 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())) end end @@ -275,6 +370,89 @@ function CameraDeviceConfiguration.initialize_camera_capabilities(device) init_camera_privacy_mode(device) end +function CameraDeviceConfiguration.initialize_camera_capabilities_and_subscriptions(device) + CameraDeviceConfiguration.initialize_camera_capabilities(device) + device:subscribe() + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then + button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) + end +end + +local function initialize_selected_camera_capabilities(device, capabilities_to_reinit) + local reinit_targets = capabilities_to_reinit or {} + + if reinit_targets.webrtc then + init_webrtc(device) + end + if reinit_targets.ptz then + init_ptz(device) + end + if reinit_targets.zone_management then + init_zone_management(device) + end + if reinit_targets.local_media_storage then + init_local_media_storage(device) + end + if reinit_targets.audio_recording then + init_audio_recording(device) + end + if reinit_targets.video_stream_settings then + init_video_stream_settings(device) + end + if reinit_targets.camera_privacy_mode then + init_camera_privacy_mode(device) + end +end + +local function profile_capability_set(profile) + local capability_set = {} + for _, component in pairs((profile or {}).components or {}) do + for _, capability in ipairs(component.capabilities or {}) do + if capability.id ~= nil then + capability_set[capability.id] = true + end + end + end + return capability_set +end + +local function changed_capabilities_from_profiles(old_profile, new_profile) + local flags = {} + local old_set = profile_capability_set(old_profile) + local new_set = profile_capability_set(new_profile) + + for _, managed in ipairs(managed_capability_map) do + local id = managed.capability.ID + if old_set[id] ~= new_set[id] and new_set[id] == true then + flags[managed.key] = true + end + end + + return flags +end + +function CameraDeviceConfiguration.reconcile_profile_and_capabilities(device) + local profile_update_requested = CameraDeviceConfiguration.match_profile(device) + if not profile_update_requested then + local capabilities_to_reinit = capabilities_needing_reinit(device) + initialize_selected_camera_capabilities(device, capabilities_to_reinit) + end + return profile_update_requested +end + +function CameraDeviceConfiguration.update_status_light_attribute_presence(device, status_light_enabled_present, status_light_brightness_present) + set_status_light_presence(device, status_light_enabled_present, status_light_brightness_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) + device:subscribe() + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then + button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) + end +end + function CameraDeviceConfiguration.update_doorbell_component_map(device, ep) local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} component_map.doorbell = ep 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 000008fa51..c88f177707 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 @@ -14,6 +14,12 @@ CameraFields.MAX_RESOLUTION = "__max_resolution" CameraFields.MIN_RESOLUTION = "__min_resolution" 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.CameraAVSMFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.CameraAvStreamManagement.ID } +CameraFields.CameraAVSULMFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.CameraAvSettingsUserLevelManagement.ID } +CameraFields.ZoneManagementFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.ZoneManagement.ID } CameraFields.PAN_IDX = "PAN" CameraFields.TILT_IDX = "TILT" 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 5793c1d9fc..f3b92aa3b6 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 @@ -6,6 +6,7 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" +local cluster_base = require "st.matter.cluster_base" local CameraUtils = {} @@ -297,10 +298,19 @@ function CameraUtils.subscribe(device) local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) local devices_seen, capabilities_seen, attributes_seen, events_seen = {}, {}, {}, {} + local additional_attributes = {} if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) > 0 then - local ib = im.InteractionInfoBlock(nil, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.attributes.AttributeList.ID) - subscribe_request:with_info_block(ib) + table.insert(additional_attributes, clusters.CameraAvStreamManagement.attributes.AttributeList) + table.insert(additional_attributes, camera_fields.CameraAVSMFeatureMapAttr) + end + + if #device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID) > 0 then + table.insert(additional_attributes, camera_fields.CameraAVSULMFeatureMapAttr) + end + + if #device:get_endpoints(clusters.ZoneManagement.ID) > 0 then + table.insert(additional_attributes, camera_fields.ZoneManagementFeatureMapAttr) end for _, endpoint_info in ipairs(device.endpoints) do @@ -313,6 +323,17 @@ function CameraUtils.subscribe(device) end end + for _, attr in ipairs(additional_attributes) do + local cluster_id = attr.cluster or attr._cluster.ID + local attr_id = attr.ID or attr.attribute + if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib + end + end + if #subscribe_request.info_blocks > 0 then device:send(subscribe_request) end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index 91ebe03ebc..a72aa0b234 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -6,7 +6,6 @@ ------------------------------------------------------------------------------------- local attribute_handlers = require "sub_drivers.camera.camera_handlers.attribute_handlers" -local button_cfg = require("switch_utils.device_configuration").ButtonCfg local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" local camera_fields = require "sub_drivers.camera.camera_utils.fields" local camera_utils = require "sub_drivers.camera.camera_utils.utils" @@ -33,7 +32,7 @@ end function CameraLifecycleHandlers.do_configure(driver, device) camera_utils.update_camera_component_map(device) if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then - camera_cfg.match_profile(device, false, false) + camera_cfg.match_profile(device) end camera_cfg.create_child_devices(driver, device) camera_cfg.initialize_camera_capabilities(device) @@ -42,24 +41,19 @@ end function CameraLifecycleHandlers.driver_switched(driver, device) camera_utils.update_camera_component_map(device) if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then - camera_cfg.match_profile(device, false, false) + camera_cfg.match_profile(device) end end function CameraLifecycleHandlers.info_changed(driver, device, event, args) local software_version_changed = device.matter_version ~= nil and args.old_st_store.matter_version ~= nil and device.matter_version.software ~= args.old_st_store.matter_version.software + local profile_changed = not switch_utils.deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) if software_version_changed then - camera_cfg.match_profile(device, false, false) - end - - if not switch_utils.deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) then - camera_cfg.initialize_camera_capabilities(device) - device:subscribe() - if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then - button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) - end + camera_cfg.reconcile_profile_and_capabilities(device) + elseif profile_changed then + camera_cfg.reinitialize_changed_camera_capabilities_and_subscriptions(device, args.old_st_store.profile, device.profile) end end @@ -105,7 +99,8 @@ local camera_handler = { [clusters.CameraAvStreamManagement.attributes.Viewport.ID] = attribute_handlers.viewport_handler, [clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.localMediaStorage.localSnapshotRecording), [clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.localMediaStorage.localVideoRecording), - [clusters.CameraAvStreamManagement.attributes.AttributeList.ID] = attribute_handlers.camera_av_stream_management_attribute_list_handler + [clusters.CameraAvStreamManagement.attributes.AttributeList.ID] = attribute_handlers.camera_av_stream_management_attribute_list_handler, + [camera_fields.CameraAVSMFeatureMapAttr.ID] = attribute_handlers.camera_feature_map_handler }, [clusters.CameraAvSettingsUserLevelManagement.ID] = { [clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition.ID] = attribute_handlers.ptz_position_handler, @@ -116,14 +111,16 @@ local camera_handler = { [clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.panRange, camera_fields.pt_range_fields[camera_fields.PAN_IDX].min), [clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.tiltRange, camera_fields.pt_range_fields[camera_fields.TILT_IDX].max), [clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.tiltRange, camera_fields.pt_range_fields[camera_fields.TILT_IDX].min), - [clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams.ID] = attribute_handlers.dptz_streams_handler + [clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams.ID] = attribute_handlers.dptz_streams_handler, + [camera_fields.CameraAVSULMFeatureMapAttr.ID] = attribute_handlers.camera_feature_map_handler }, [clusters.ZoneManagement.ID] = { [clusters.ZoneManagement.attributes.MaxZones.ID] = attribute_handlers.max_zones_handler, [clusters.ZoneManagement.attributes.Zones.ID] = attribute_handlers.zones_handler, [clusters.ZoneManagement.attributes.Triggers.ID] = attribute_handlers.triggers_handler, [clusters.ZoneManagement.attributes.SensitivityMax.ID] = attribute_handlers.sensitivity_max_handler, - [clusters.ZoneManagement.attributes.Sensitivity.ID] = attribute_handlers.sensitivity_handler + [clusters.ZoneManagement.attributes.Sensitivity.ID] = attribute_handlers.sensitivity_handler, + [camera_fields.ZoneManagementFeatureMapAttr.ID] = attribute_handlers.camera_feature_map_handler }, [clusters.Chime.ID] = { [clusters.Chime.attributes.InstalledChimeSounds.ID] = attribute_handlers.installed_chime_sounds_handler, 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 631060f79b..7f1f72b108 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -2,7 +2,9 @@ -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" +local cluster_base = require "st.matter.cluster_base" local clusters = require "st.matter.clusters" +local camera_fields = require "sub_drivers.camera.camera_utils.fields" local t_utils = require "integration_test.utils" local test = require "integration_test" local uint32 = require "st.matter.data_types.Uint32" @@ -154,6 +156,9 @@ local function test_init() parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP) }) subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + subscribe_request:merge(cluster_base.subscribe(mock_device, nil, camera_fields.CameraAVSMFeatureMapAttr.cluster, camera_fields.CameraAVSMFeatureMapAttr.ID)) + subscribe_request:merge(cluster_base.subscribe(mock_device, nil, camera_fields.CameraAVSULMFeatureMapAttr.cluster, camera_fields.CameraAVSULMFeatureMapAttr.ID)) + subscribe_request:merge(cluster_base.subscribe(mock_device, nil, camera_fields.ZoneManagementFeatureMapAttr.cluster, camera_fields.ZoneManagementFeatureMapAttr.ID)) for i, attr in ipairs(subscribed_attributes) do if i > 1 then subscribe_request:merge(attr:subscribe(mock_device)) end end @@ -405,6 +410,99 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Software version change should initialize camera capabilities when profile is unchanged", + function() + local camera_handler = require "sub_drivers.camera" + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + local button_cfg = require("switch_utils.device_configuration").ButtonCfg + + local match_profile_called = false + local init_called = false + local subscribe_called = false + local configure_buttons_called = false + + local fake_device = { + matter_version = { hardware = 1, software = 3 }, + profile = { id = "camera" }, + endpoints = { + { + endpoint_id = CAMERA_EP, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + }, + { + endpoint_id = DOORBELL_EP, + device_types = { + {device_type_id = 0x0143, device_type_revision = 1} -- Doorbell + } + } + }, + subscribe = function() subscribe_called = true end, + supports_capability = function() return false end, + get_endpoints = function() return { DOORBELL_EP } end, + } + + local original_match_profile = camera_cfg.match_profile + local original_init = camera_cfg.initialize_camera_capabilities + local original_configure_buttons = button_cfg.configure_buttons + + camera_cfg.match_profile = function() + match_profile_called = true + return false + end + camera_cfg.initialize_camera_capabilities = function() init_called = true end + button_cfg.configure_buttons = function() configure_buttons_called = true end + + camera_handler.lifecycle_handlers.infoChanged(nil, fake_device, nil, { + old_st_store = { + matter_version = { hardware = 1, software = 1 }, + profile = fake_device.profile, + } + }) + + camera_cfg.match_profile = original_match_profile + camera_cfg.initialize_camera_capabilities = original_init + button_cfg.configure_buttons = original_configure_buttons + + assert(match_profile_called, "match_profile should be called on software version change") + assert(not init_called, "initialize_camera_capabilities should not be called when capability state is unchanged") + assert(not subscribe_called, "subscribe should not be called when capability state is unchanged") + assert(not configure_buttons_called, "configure_buttons should not be called when capability state is unchanged") + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Camera FeatureMap change should reinitialize capabilities when profile is unchanged", + function() + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + + local reconcile_called = false + local original_reconcile = camera_cfg.reconcile_profile_and_capabilities + + camera_cfg.reconcile_profile_and_capabilities = function(_) + reconcile_called = true + return false + end + + test.socket.matter:__queue_receive({ + mock_device.id, + cluster_base.build_test_report_data(mock_device, CAMERA_EP, camera_fields.CameraAVSMFeatureMapAttr.cluster, camera_fields.CameraAVSMFeatureMapAttr.ID, uint32(0)) + }) + test.wait_for_events() + + camera_cfg.reconcile_profile_and_capabilities = original_reconcile + assert(reconcile_called, "reconcile_profile_and_capabilities should be called") + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Reports mapping to EnabledState capability data type should generate appropriate events", function() @@ -2799,6 +2897,11 @@ test.register_coroutine_test( function() update_device_profile() test.wait_for_events() + + local camera_cfg = require("sub_drivers.camera.camera_utils.device_configuration") + local original_reconcile = camera_cfg.reconcile_profile_and_capabilities + camera_cfg.reconcile_profile_and_capabilities = function(...) return false end + test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { @@ -2806,6 +2909,9 @@ test.register_coroutine_test( uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) }) }) + test.wait_for_events() + + camera_cfg.reconcile_profile_and_capabilities = original_reconcile end, { min_api_version = 17