diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b1c6a8df8b..2ece06cbbf 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -38,12 +38,15 @@ function SwitchLifecycleHandlers.device_added(driver, device) device:send(clusters.OnOff.attributes.OnOff:read(device)) end - -- call device init in case init is not called after added due to device caching - SwitchLifecycleHandlers.device_init(driver, device) + -- The device init event is guaranteed in FW versions 58+, so this is only needed for older hubs + if version.rpc < 10 then + -- call device init in case init is not called after added due to device caching + SwitchLifecycleHandlers.device_init(driver, device) + end end function SwitchLifecycleHandlers.do_configure(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + if device.network_type == device_lib.NETWORK_TYPE_MATTER then switch_cfg.set_device_control_options(device) device_cfg.match_profile(driver, device) elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then @@ -54,7 +57,7 @@ function SwitchLifecycleHandlers.do_configure(driver, device) end function SwitchLifecycleHandlers.driver_switched(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + if device.network_type == device_lib.NETWORK_TYPE_MATTER then device_cfg.match_profile(driver, device) end end @@ -66,12 +69,16 @@ function SwitchLifecycleHandlers.info_changed(driver, device, event, args) button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) ) + if version.api >= 16 and version.rpc >= 10 then + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + camera_cfg.initialize_select_camera_capabilities(device) + end elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then device:get_parent_device():subscribe() -- parent device required to send subscription requests end end - if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + if device.network_type == device_lib.NETWORK_TYPE_MATTER then if device.matter_version.software ~= args.old_st_store.matter_version.software then device_cfg.match_profile(driver, device) end @@ -95,11 +102,16 @@ function SwitchLifecycleHandlers.device_init(driver, device) switch_utils.check_field_name_updates(device) device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + device:extend_device("emit_event_for_endpoint", switch_utils.emit_event_for_endpoint) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) end if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then - device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + switch_utils.set_preprofiling_data(device, fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY) + end + if version.api < 16 or version.rpc < 10 or #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then + switch_utils.set_preprofiling_data(device, fields.profiling_data.STATUS_LIGHT_ENABLED_PRESENT, false) + switch_utils.set_preprofiling_data(device, fields.profiling_data.STATUS_LIGHT_BRIGHTNESS_PRESENT, false) end switch_utils.handle_electrical_sensor_info(device) device:extend_device("subscribe", switch_utils.subscribe) 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 b32b1dd55c..69753f58ed 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 @@ -1,13 +1,11 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local camera_fields = require "sub_drivers.camera.camera_utils.fields" -local camera_utils = require "sub_drivers.camera.camera_utils.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" -local fields = require "switch_utils.fields" -local utils = require "st.utils" +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local st_utils = require "st.utils" local CameraAttributeHandlers = {} @@ -37,7 +35,7 @@ CameraAttributeHandlers.night_vision_factory = function(attribute) end function CameraAttributeHandlers.image_rotation_handler(driver, device, ib, response) - local degrees = utils.clamp_value(ib.data.value, 0, 359) + local degrees = st_utils.clamp_value(ib.data.value, 0, 359) device:emit_event_for_endpoint(ib, capabilities.imageControl.imageRotation(degrees)) camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageRotation") end @@ -63,7 +61,7 @@ function CameraAttributeHandlers.volume_level_handler(driver, device, ib, respon 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 - local normalized_volume = utils.round((ib.data.value - min_volume) * 100.0 / limited_range) + local normalized_volume = st_utils.round((ib.data.value - min_volume) * 100.0 / limited_range) device:emit_event_for_endpoint(ib, capabilities.audioVolume.volume(normalized_volume)) end @@ -296,7 +294,7 @@ function CameraAttributeHandlers.ptz_position_handler(driver, device, ib, respon local emit_event = function(idx, value) if value ~= ptz_map[idx].current then device:emit_event_for_endpoint(ib, ptz_map[idx].attribute( - utils.clamp_value(value, ptz_map[idx].range.minimum, ptz_map[idx].range.maximum) + st_utils.clamp_value(value, ptz_map[idx].range.minimum, ptz_map[idx].range.maximum) )) end end @@ -447,31 +445,35 @@ function CameraAttributeHandlers.selected_chime_handler(driver, device, ib, resp end function CameraAttributeHandlers.camera_av_stream_management_attribute_list_handler(driver, device, ib, response) - if not ib.data.elements then return end local status_light_enabled_present, status_light_brightness_present = false, false - local attribute_ids = {} - for _, attr in ipairs(ib.data.elements) do + local status_light_attribute_ids = {} + for _, attr in ipairs(ib.data.elements or {}) do if attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID then status_light_enabled_present = true - table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) + table.insert(status_light_attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) elseif attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID then status_light_brightness_present = true - table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + table.insert(status_light_attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) end end - local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} - component_map.statusLed = { - endpoint_id = ib.endpoint_id, - cluster_id = ib.cluster_id, - attribute_ids = attribute_ids, - } - device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true}) - camera_cfg.update_status_light_attribute_presence(device, status_light_enabled_present, status_light_brightness_present) - camera_cfg.reconcile_profile_and_capabilities(device) + if #status_light_attribute_ids > 0 then + camera_utils.update_component_to_endpoint_map(device, camera_fields.profile_components.statusLed, { + endpoint_id = ib.endpoint_id, + cluster_id = ib.cluster_id, + attribute_ids = status_light_attribute_ids, + }) + end + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + local utils = require "switch_utils.utils" + local fields = require "switch_utils.fields" + utils.set_preprofiling_data(device, fields.profiling_data.STATUS_LIGHT_ENABLED_PRESENT, status_light_enabled_present) + utils.set_preprofiling_data(device, fields.profiling_data.STATUS_LIGHT_BRIGHTNESS_PRESENT, status_light_brightness_present) + camera_cfg.reconcile_profile_and_capabilities(driver, device) end function CameraAttributeHandlers.camera_feature_map_handler(driver, device, ib, response) - camera_cfg.reconcile_profile_and_capabilities(device) + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + camera_cfg.reconcile_profile_and_capabilities(driver, 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 398150d063..be585e71f9 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 @@ -1,38 +1,16 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local button_cfg = require("switch_utils.device_configuration").ButtonCfg local camera_fields = require "sub_drivers.camera.camera_utils.fields" local camera_utils = require "sub_drivers.camera.camera_utils.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local st_utils = require "st.utils" -local device_cfg = require "switch_utils.device_configuration" local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" +local st_utils = require "st.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, @@ -108,93 +86,11 @@ local function build_camera_privacy_supported_commands() return { "setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode" } end -local function capabilities_needing_reinit(device) - local capabilities_to_reinit = {} - - local function should_init(capability, attribute, expected) - if device:supports_capability(capability) then - local current = st_utils.deep_copy(device:get_latest_state( - camera_fields.profile_components.main, - capability.ID, - attribute.NAME, - {} - )) - return not switch_utils.deep_equals(current, expected) - end - return false - end - - if should_init(capabilities.webrtc, capabilities.webrtc.supportedFeatures, build_webrtc_supported_features()) then - capabilities_to_reinit.webrtc = true - end - - if should_init(capabilities.mechanicalPanTiltZoom, capabilities.mechanicalPanTiltZoom.supportedAttributes, build_ptz_supported_attributes(device)) then - capabilities_to_reinit.ptz = true - end - - if should_init(capabilities.zoneManagement, capabilities.zoneManagement.supportedFeatures, build_zone_management_supported_features(device)) then - capabilities_to_reinit.zone_management = true - end - - if should_init(capabilities.localMediaStorage, capabilities.localMediaStorage.supportedAttributes, 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( - camera_fields.profile_components.main, - capabilities.audioRecording.ID, - capabilities.audioRecording.audioRecording.NAME - ) - if audio_enabled_state == nil then - capabilities_to_reinit.audio_recording = true - end - end - - if should_init(capabilities.videoStreamSettings, capabilities.videoStreamSettings.supportedFeatures, build_video_stream_settings_supported_features(device)) then - capabilities_to_reinit.video_stream_settings = true - end - - if should_init(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedAttributes, build_camera_privacy_supported_attributes()) or - should_init(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedCommands, build_camera_privacy_supported_commands()) then - capabilities_to_reinit.camera_privacy_mode = true - end - - return capabilities_to_reinit +local function build_audio_recording() + return "enabled" end -function CameraDeviceConfiguration.create_child_devices(driver, device) - local num_floodlight_eps = 0 - local parent_child_device = false - for _, ep in ipairs(device.endpoints or {}) do - if device:supports_server_cluster(clusters.OnOff.ID, ep.endpoint_id) then - local child_profile = device_cfg.SwitchCfg.assign_profile_for_onoff_ep(device, ep.endpoint_id) - if child_profile then - num_floodlight_eps = num_floodlight_eps + 1 - local name = string.format("%s %d", "Floodlight", num_floodlight_eps) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep.endpoint_id), - vendor_provided_label = name - } - ) - parent_child_device = true - end - end - end - if parent_child_device then - device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) - device:set_find_child(switch_utils.find_child) - end -end - -function CameraDeviceConfiguration.match_profile(device) - local status_light_enabled_present, status_light_brightness_present = get_status_light_presence(device) - local profile_update_requested = false +function CameraDeviceConfiguration.assign_profile_for_camera_ep(device, camera_ep_id) local optional_supported_component_capabilities = {} local main_component_capabilities = {} local status_led_component_capabilities = {} @@ -206,9 +102,8 @@ function CameraDeviceConfiguration.match_profile(device) return cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH" end - local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) - if #camera_endpoints > 0 then - local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) + if camera_ep_id then + local camera_ep = switch_utils.get_endpoint_info(device, camera_ep_id) for _, ep_cluster in pairs(camera_ep.clusters or {}) do if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID and has_server_cluster_type(ep_cluster) then local clus_has_feature = function(feature_bitmap) @@ -276,10 +171,10 @@ function CameraDeviceConfiguration.match_profile(device) if #doorbell_endpoints > 0 then table.insert(doorbell_component_capabilities, capabilities.button.ID) end - if status_light_enabled_present then + if device:get_field(fields.profiling_data.STATUS_LIGHT_ENABLED_PRESENT) then table.insert(status_led_component_capabilities, capabilities.switch.ID) end - if status_light_brightness_present then + if device:get_field(fields.profiling_data.STATUS_LIGHT_BRIGHTNESS_PRESENT) then table.insert(status_led_component_capabilities, capabilities.mode.ID) end @@ -298,162 +193,156 @@ function CameraDeviceConfiguration.match_profile(device) 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 + CameraDeviceConfiguration.update_camera_component_map(device) + return "camera", optional_supported_component_capabilities end - - return profile_update_requested end -local function init_webrtc(device) - if device:supports_capability(capabilities.webrtc) then - local transport_provider_ep_ids = device:get_endpoints(clusters.WebRTCTransportProvider.ID) - device:emit_event_for_endpoint(transport_provider_ep_ids[1], capabilities.webrtc.supportedFeatures(build_webrtc_supported_features())) - end +local function init_webrtc(device, supported_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(supported_features)) end -local function init_ptz(device) - if device:supports_capability(capabilities.mechanicalPanTiltZoom) then - local av_settings_ep_ids = device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID) - device:emit_event_for_endpoint(av_settings_ep_ids[1], capabilities.mechanicalPanTiltZoom.supportedAttributes(build_ptz_supported_attributes(device))) - end +local function init_ptz(device, supported_attributes) + 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)) end -local function init_zone_management(device) - if device:supports_capability(capabilities.zoneManagement) then - local zone_management_ep_ids = device:get_endpoints(clusters.ZoneManagement.ID) - device:emit_event_for_endpoint(zone_management_ep_ids[1], capabilities.zoneManagement.supportedFeatures(build_zone_management_supported_features(device))) - end +local function init_zone_management(device, supported_features) + 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)) end -local function init_local_media_storage(device) - if device:supports_capability(capabilities.localMediaStorage) then - local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.localMediaStorage.supportedAttributes(build_local_media_storage_supported_attributes(device))) - end +local function init_local_media_storage(device, supported_attributes) + 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)) end -local function init_audio_recording(device) - if device:supports_capability(capabilities.audioRecording) then - local audio_enabled_state = device:get_latest_state( - camera_fields.profile_components.main, capabilities.audioRecording.ID, capabilities.audioRecording.audioRecording.NAME - ) - if audio_enabled_state == nil then - -- Initialize with enabled default if state is unset - local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.audioRecording.audioRecording("enabled")) - end +local function init_audio_recording(device, initial_audio_recording_state) + local audio_enabled_state = device:get_latest_state( + camera_fields.profile_components.main, capabilities.audioRecording.ID, capabilities.audioRecording.audioRecording.NAME + ) + if audio_enabled_state == nil then + -- Initialize with enabled default if state is unset + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.audioRecording.audioRecording(initial_audio_recording_state)) end end -local function init_video_stream_settings(device) - if device:supports_capability(capabilities.videoStreamSettings) then - local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.videoStreamSettings.supportedFeatures(build_video_stream_settings_supported_features(device))) - end +local function init_video_stream_settings(device, supported_features) + 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)) end -local function init_camera_privacy_mode(device) - if device:supports_capability(capabilities.cameraPrivacyMode) then - local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedAttributes(build_camera_privacy_supported_attributes())) - device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(build_camera_privacy_supported_commands())) - end +local function init_camera_privacy_mode_attributes(device, supported_attributes) + 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)) end -function CameraDeviceConfiguration.initialize_camera_capabilities(device) - init_webrtc(device) - init_ptz(device) - init_zone_management(device) - init_local_media_storage(device) - init_audio_recording(device) - init_video_stream_settings(device) - init_camera_privacy_mode(device) +local function init_camera_privacy_mode_commands(device, supported_commands) + 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.supportedCommands(supported_commands)) end -local function initialize_selected_camera_capabilities(device, capabilities_to_reinit) - local reinit_targets = capabilities_to_reinit or {} +function CameraDeviceConfiguration.initialize_select_camera_capabilities(device) + local ordered_managed_capabilities = { + capabilities.webrtc, + capabilities.mechanicalPanTiltZoom, + capabilities.zoneManagement, + capabilities.localMediaStorage, + capabilities.audioRecording, + capabilities.videoStreamSettings, + capabilities.cameraPrivacyMode + } - 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 managed_capabilities = { + [capabilities.webrtc] = { + [capabilities.webrtc.supportedFeatures] = { init_webrtc, build_webrtc_supported_features} + }, + [capabilities.mechanicalPanTiltZoom] = { + [capabilities.mechanicalPanTiltZoom.supportedAttributes] = { init_ptz, build_ptz_supported_attributes} + }, + [capabilities.zoneManagement] = { + [capabilities.zoneManagement.supportedFeatures] = { init_zone_management, build_zone_management_supported_features} + }, + [capabilities.localMediaStorage] = { + [capabilities.localMediaStorage.supportedAttributes] = { init_local_media_storage, build_local_media_storage_supported_attributes} + }, + [capabilities.audioRecording] = { + [capabilities.audioRecording.audioRecording] = { init_audio_recording, build_audio_recording} + }, + [capabilities.videoStreamSettings] = { + [capabilities.videoStreamSettings.supportedFeatures] = { init_video_stream_settings, build_video_stream_settings_supported_features} + }, + [capabilities.cameraPrivacyMode] = { + [capabilities.cameraPrivacyMode.supportedAttributes] = { init_camera_privacy_mode_attributes, build_camera_privacy_supported_attributes}, + [capabilities.cameraPrivacyMode.supportedCommands] = { init_camera_privacy_mode_commands, build_camera_privacy_supported_commands} + } + } -local function profile_capability_set(profile) - local capability_set = {} - for _, component in pairs((profile or {}).components or {}) do - for _, capability in pairs(component.capabilities or {}) do - if capability.id ~= nil then - capability_set[capability.id] = true - end + local function should_init(capability, attribute, expected) + if device:supports_capability(capability) then + local current = st_utils.deep_copy(device:get_latest_state(camera_fields.profile_components.main, capability.ID, attribute.NAME, {})) + return not switch_utils.deep_equals(current, expected) end + return false 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 + for _, capability in ipairs(ordered_managed_capabilities) do + for attribute, functions in pairs(managed_capabilities[capability]) do + local init_function = functions[1] + local build_function = functions[2] + local expected = build_function(device) + if should_init(capability, attribute, expected) then + init_function(device, expected) + end end end - - return flags end -function CameraDeviceConfiguration.reconcile_profile_and_capabilities(device) - local profile_update_requested = CameraDeviceConfiguration.match_profile(device) +function CameraDeviceConfiguration.reconcile_profile_and_capabilities(driver, device) + local device_cfg = require "switch_utils.device_configuration".DeviceCfg + local profile_update_requested = device_cfg.match_profile(driver, device) if not profile_update_requested then - local capabilities_to_reinit = capabilities_needing_reinit(device) - initialize_selected_camera_capabilities(device, capabilities_to_reinit) + CameraDeviceConfiguration.initialize_select_camera_capabilities(device) 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})) +function CameraDeviceConfiguration.update_camera_component_map(device) + -- An assumption here: there is only 1 CameraAvStreamManagement cluster on the device (which is all our profile supports) + local audio_camera_av_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID, {feature_bitmap=clusters.CameraAvStreamManagement.types.Feature.AUDIO}) + if #audio_camera_av_ep_ids > 0 then + camera_utils.update_component_to_endpoint_map(device, camera_fields.profile_components.microphone, { + endpoint_id = audio_camera_av_ep_ids[1], + cluster_id = clusters.CameraAvStreamManagement.ID, + attribute_ids = { + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID, + }, + }) + end + + local video_camera_av_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID, {feature_bitmap=clusters.CameraAvStreamManagement.types.Feature.VIDEO}) + if #video_camera_av_ep_ids > 0 then + camera_utils.update_component_to_endpoint_map(device, camera_fields.profile_components.speaker, { + endpoint_id = video_camera_av_ep_ids[1], + cluster_id = clusters.CameraAvStreamManagement.ID, + attribute_ids = { + clusters.CameraAvStreamManagement.attributes.SpeakerMuted.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID, + }, + }) 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 - device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) + local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) + if #doorbell_endpoints > 0 then + camera_utils.update_component_to_endpoint_map(device, camera_fields.profile_components.doorbell, doorbell_endpoints[1]) + end end return CameraDeviceConfiguration 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 c88f177707..6d935afb38 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,8 +14,6 @@ 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 } diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/subscriptions.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/subscriptions.lua new file mode 100644 index 0000000000..3f201a7da0 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/subscriptions.lua @@ -0,0 +1,106 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local camera_fields = require "sub_drivers.camera.camera_utils.fields" + +local SubscriptionMap = { + subscribed_attributes = { + [capabilities.audioMute.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted + }, + [capabilities.audioVolume.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel + }, + [capabilities.cameraPrivacyMode.ID] = { + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn + }, + [capabilities.cameraViewportSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.Viewport + }, + [capabilities.hdr.ID] = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation + }, + [capabilities.imageControl.ID] = { + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical + }, + [capabilities.localMediaStorage.ID] = { + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled + }, + [capabilities.mechanicalPanTiltZoom.ID] = { + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin + }, + [capabilities.mode.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness + }, + [capabilities.nightVision.ID] = { + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum + }, + [capabilities.sounds.ID] = { + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime + }, + [capabilities.switch.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + }, + [capabilities.videoStreamSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams + }, + [capabilities.webrtc.ID] = { + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport + }, + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity + }, + }, + subscribed_events = { + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped + } + }, + conditional_subscriptions = { + [function(device) return #device:get_endpoints(clusters.CameraAvStreamManagement.ID) > 0 end] = { + clusters.CameraAvStreamManagement.attributes.AttributeList, + camera_fields.CameraAVSMFeatureMapAttr + }, + [function(device) return #device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID) > 0 end] = { + camera_fields.CameraAVSULMFeatureMapAttr + }, + [function(device) return #device:get_endpoints(clusters.ZoneManagement.ID) > 0 end] = { + camera_fields.ZoneManagementFeatureMapAttr, + } + } +} + +return SubscriptionMap 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..aef9fd7eba 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 @@ -3,56 +3,10 @@ local camera_fields = require "sub_drivers.camera.camera_utils.fields" 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 CameraUtils = {} -function CameraUtils.component_to_endpoint(device, component) - local camera_eps = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - table.sort(camera_eps) - for _, ep in ipairs(camera_eps or {}) do - if ep ~= 0 then -- 0 is the matter RootNode endpoint - return ep - end - end - return nil -end - -function CameraUtils.update_camera_component_map(device) - local camera_av_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) - if #camera_av_ep_ids > 0 then - -- An assumption here: there is only 1 CameraAvStreamManagement cluster on the device (which is all our profile supports) - local component_map = {} - if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.AUDIO) then - component_map.microphone = { - endpoint_id = camera_av_ep_ids[1], - cluster_id = clusters.CameraAvStreamManagement.ID, - attribute_ids = { - clusters.CameraAvStreamManagement.attributes.MicrophoneMuted.ID, - clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel.ID, - clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID, - clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID, - }, - } - end - if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then - component_map.speaker = { - endpoint_id = camera_av_ep_ids[1], - cluster_id = clusters.CameraAvStreamManagement.ID, - attribute_ids = { - clusters.CameraAvStreamManagement.attributes.SpeakerMuted.ID, - clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel.ID, - clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID, - clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID, - }, - } - end - device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) - end -end - function CameraUtils.get_ptz_map(device) local mechanicalPanTiltZoom = capabilities.mechanicalPanTiltZoom local ptz_map = { @@ -180,162 +134,11 @@ function CameraUtils.optional_capabilities_list_changed(new_component_capability return false end -function CameraUtils.subscribe(device) - local camera_subscribed_attributes = { - [capabilities.hdr.ID] = { - clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, - clusters.CameraAvStreamManagement.attributes.ImageRotation - }, - [capabilities.nightVision.ID] = { - clusters.CameraAvStreamManagement.attributes.NightVision, - clusters.CameraAvStreamManagement.attributes.NightVisionIllum - }, - [capabilities.imageControl.ID] = { - clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, - clusters.CameraAvStreamManagement.attributes.ImageFlipVertical - }, - [capabilities.cameraPrivacyMode.ID] = { - clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, - clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, - clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn - }, - [capabilities.webrtc.ID] = { - clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport - }, - [capabilities.mechanicalPanTiltZoom.ID] = { - clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, - clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, - clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, - clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, - clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, - clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, - clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, - clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin - }, - [capabilities.audioMute.ID] = { - clusters.CameraAvStreamManagement.attributes.SpeakerMuted, - clusters.CameraAvStreamManagement.attributes.MicrophoneMuted - }, - [capabilities.audioVolume.ID] = { - clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, - clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, - clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel - }, - [capabilities.mode.ID] = { - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness - }, - [capabilities.switch.ID] = { - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, - clusters.OnOff.attributes.OnOff - }, - [capabilities.videoStreamSettings.ID] = { - clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, - clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, - clusters.CameraAvStreamManagement.attributes.VideoSensorParams, - clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, - clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams - }, - [capabilities.zoneManagement.ID] = { - clusters.ZoneManagement.attributes.MaxZones, - clusters.ZoneManagement.attributes.Zones, - clusters.ZoneManagement.attributes.Triggers, - clusters.ZoneManagement.attributes.SensitivityMax, - clusters.ZoneManagement.attributes.Sensitivity - }, - [capabilities.sounds.ID] = { - clusters.Chime.attributes.InstalledChimeSounds, - clusters.Chime.attributes.SelectedChime - }, - [capabilities.localMediaStorage.ID] = { - clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, - clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled - }, - [capabilities.cameraViewportSettings.ID] = { - clusters.CameraAvStreamManagement.attributes.MinViewportResolution, - clusters.CameraAvStreamManagement.attributes.VideoSensorParams, - clusters.CameraAvStreamManagement.attributes.Viewport - }, - [capabilities.motionSensor.ID] = { - clusters.OccupancySensing.attributes.Occupancy - }, - [capabilities.switchLevel.ID] = { - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - }, - [capabilities.colorControl.ID] = { - clusters.ColorControl.attributes.ColorMode, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - }, - [capabilities.colorTemperature.ID] = { - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - }, - } - - local camera_subscribed_events = { - [capabilities.zoneManagement.ID] = { - clusters.ZoneManagement.events.ZoneTriggered, - clusters.ZoneManagement.events.ZoneStopped - }, - [capabilities.button.ID] = { - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete - } - } - - local im = require "st.matter.interaction_model" - - 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 - 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 - local checked_device = switch_utils.find_child(device, endpoint_info.endpoint_id) or device - if not devices_seen[checked_device.id] then - switch_utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, - camera_subscribed_attributes, camera_subscribed_events - ) - devices_seen[checked_device.id] = true -- only loop through any device once - 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 +function CameraUtils.update_component_to_endpoint_map(device, component, endpoint_mapping) + local fields = require "switch_utils.fields" + local component_endpoint_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + component_endpoint_map[component] = endpoint_mapping + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_endpoint_map, { persist = true }) end return CameraUtils 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 a72aa0b234..d39fb349a3 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -5,69 +5,15 @@ -- Matter Camera Sub Driver ------------------------------------------------------------------------------------- -local attribute_handlers = require "sub_drivers.camera.camera_handlers.attribute_handlers" -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" local capabilities = require "st.capabilities" -local capability_handlers = require "sub_drivers.camera.camera_handlers.capability_handlers" local clusters = require "st.matter.clusters" +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local attribute_handlers = require "sub_drivers.camera.camera_handlers.attribute_handlers" +local capability_handlers = require "sub_drivers.camera.camera_handlers.capability_handlers" local event_handlers = require "sub_drivers.camera.camera_handlers.event_handlers" -local fields = require "switch_utils.fields" -local switch_utils = require "switch_utils.utils" - -local CameraLifecycleHandlers = {} - -function CameraLifecycleHandlers.device_init(driver, device) - device:set_component_to_endpoint_fn(camera_utils.component_to_endpoint) - device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) - device:extend_device("emit_event_for_endpoint", switch_utils.emit_event_for_endpoint) - if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then - device:set_find_child(switch_utils.find_child) - end - device:extend_device("subscribe", camera_utils.subscribe) - device:subscribe() -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) - end - camera_cfg.create_child_devices(driver, device) - camera_cfg.initialize_camera_capabilities(device) -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) - 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.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 - -function CameraLifecycleHandlers.added() end local camera_handler = { NAME = "Camera Handler", - lifecycle_handlers = { - init = CameraLifecycleHandlers.device_init, - infoChanged = CameraLifecycleHandlers.info_changed, - doConfigure = CameraLifecycleHandlers.do_configure, - driverSwitched = CameraLifecycleHandlers.driver_switched, - added = CameraLifecycleHandlers.added - }, matter_handlers = { attr = { [clusters.CameraAvStreamManagement.ID] = { diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index 20bd92881e..801a609061 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -340,7 +340,7 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo end end if #set_topology_eps == 0 then -- in other words, all AvailableEndpoints attribute responses have been handled - device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY, {persist=true}) + switch_utils.set_preprofiling_data(device, fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY) device_cfg.match_profile(driver, device) end end @@ -373,7 +373,7 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response) end end if #tree_topology_eps == 0 then -- in other words, all PartsList attribute responses for TREE Electrical Sensor EPs have been handled - device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, {persist=true}) + switch_utils.set_preprofiling_data(device, fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.TREE_TOPOLOGY) device_cfg.match_profile(driver, device) end end @@ -398,18 +398,20 @@ function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response end function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) - local previous_battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) - device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) + local latest_battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) for _, attr in ipairs(ib.data.elements or {}) do if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist=true}) - break - elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID and - device:get_field(fields.profiling_data.BATTERY_SUPPORT) ~= fields.battery_support.BATTERY_PERCENTAGE then -- don't overwrite if percentage support is already detected + break -- BATTERY_PERCENTAGE is highest priority. break early if found + elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID then device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist=true}) end end - if not previous_battery_support or previous_battery_support ~= device:get_field(fields.profiling_data.BATTERY_SUPPORT) then + -- in the case that 1) no battery has been set, and 2) the returned ib does not include battery attributes, ignore battery + if latest_battery_support == nil and not device:get_field(fields.profiling_data.BATTERY_SUPPORT) then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) + end + if latest_battery_support == nil or latest_battery_support ~= device:get_field(fields.profiling_data.BATTERY_SUPPORT) then device_cfg.match_profile(driver, device) end end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua index 8f1f1311fa..fbd312bf57 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -21,15 +21,15 @@ local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} local FanDeviceConfiguration = {} -function ChildConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) +function ChildConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn, label_prefix) + table.sort(server_cluster_ep_ids) if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created return end - table.sort(server_cluster_ep_ids) for device_num, ep_id in ipairs(server_cluster_ep_ids) do if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint - local label_and_name = string.format("%s %d", device.label, device_num) + local label_and_name = string.format("%s %d", label_prefix or device.label, device_num) local child_profile, _ = assign_profile_fn(device, ep_id, true) local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id) if not existing_child_device then @@ -205,15 +205,26 @@ end function DeviceConfiguration.match_profile(driver, device) if profiling_data_still_required(device) then return end - local default_endpoint_id = switch_utils.find_default_endpoint(device) + local default_endpoint_id, default_device_type = switch_utils.find_default_endpoint(device) local optional_component_capabilities local updated_profile + -- TODO: we only check this early for "Floodlight" labeling purposes, + -- but ideally that should be identified with a Floodlight Camera device type check + local camera_ep_ids = {} + if version.api >= 16 and version.rpc >= 10 then + camera_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) + end + local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH if #server_onoff_ep_ids > 0 then - ChildConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) + local label_prefix = #camera_ep_ids > 0 and "Floodlight" or nil -- if there's a camera EP, it's likely this OnOff EP is for a floodlight, so label the child device accordingly + ChildConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep, label_prefix) end + -- Allow Bridges to create child on/off devices, but don't attempt to re-profile the main Bridge device. + if default_device_type == fields.DEVICE_TYPE_ID.AGGREGATOR then return end + if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end @@ -245,13 +256,23 @@ function DeviceConfiguration.match_profile(driver, device) -- initialize the main device card with buttons if applicable local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momentary_switch_ep_ids) then - updated_profile = ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momentary_switch_ep_ids) - -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. - ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momentary_switch_ep_ids) + if #camera_ep_ids == 0 then + -- the Doorbell device type, which is a superset of the Generic Switch device type, is handled later on as a subset of the the camera profiling logic. + updated_profile = ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momentary_switch_ep_ids) + -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. + ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momentary_switch_ep_ids) + end ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) end + if #camera_ep_ids > 0 then + local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" + updated_profile, optional_component_capabilities = camera_cfg.assign_profile_for_camera_ep(device, camera_ep_ids[1]) + if not updated_profile then return false end + end + device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities }) + return updated_profile ~= nil end return { diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 39a60e5eaa..f26461e037 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -34,7 +34,7 @@ SwitchFields.DEVICE_TYPE_ID = { CAMERA = 0x0142, CHIME = 0x0146, DIMMABLE_PLUG_IN_UNIT = 0x010B, - DOORBELL = 0x0143, + DOORBELL = 0x0148, ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, @@ -158,6 +158,8 @@ SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", BATTERY_SUPPORT = "__battery_support", + STATUS_LIGHT_ENABLED_PRESENT = "__status_light_enabled_present", + STATUS_LIGHT_BRIGHTNESS_PRESENT = "__status_light_brightness_present", } SwitchFields.battery_support = { diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index e7f5a96187..12edfe53ea 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -146,7 +146,7 @@ end --- find_default_endpoint is a helper function to handle situations where --- device does not have endpoint ids in sequential order from 1 function utils.find_default_endpoint(device) - -- Buttons should not be set on the main component for the Aqara Climate Sensor W100, + -- Buttons should not be set on the main component for the Aqara Climate Sensor W100 if utils.get_product_override_field(device, "is_climate_sensor_w100") then return device.MATTER_DEFAULT_ENDPOINT end @@ -161,16 +161,30 @@ function utils.find_default_endpoint(device) return nil end + -- Return the first camera endpoint as the default endpoint if any is found + if version.rpc >= 10 and version.api >= 16 then + local camera_eps = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + if #camera_eps > 0 then + return get_first_non_zero_endpoint(camera_eps), fields.DEVICE_TYPE_ID.CAMERA + end + end + + -- After camera, use aggregator as default if it is found + local aggregator_ep_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) + if #aggregator_ep_ids > 0 then + return aggregator_ep_ids[1], fields.DEVICE_TYPE_ID.AGGREGATOR + end + -- Return the first fan endpoint as the default endpoint if any is found local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) if #fan_endpoint_ids > 0 then - return get_first_non_zero_endpoint(fan_endpoint_ids) + return get_first_non_zero_endpoint(fan_endpoint_ids), fields.DEVICE_TYPE_ID.FAN end -- Return the first water valve endpoint as the default endpoint if any is found local water_valve_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) if #water_valve_endpoint_ids > 0 then - return get_first_non_zero_endpoint(water_valve_endpoint_ids) + return get_first_non_zero_endpoint(water_valve_endpoint_ids), fields.DEVICE_TYPE_ID.WATER_VALVE end -- If both onoff and momentary switch endpoints are present, check the device type on the first onoff @@ -183,25 +197,26 @@ function utils.find_default_endpoint(device) if #onoff_ep_ids > 0 and #momentary_switch_ep_ids > 0 then local default_endpoint_id = get_first_non_zero_endpoint(onoff_ep_ids) if utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then - return default_endpoint_id + return default_endpoint_id, fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE else device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") - return get_first_non_zero_endpoint(momentary_switch_ep_ids) + return get_first_non_zero_endpoint(momentary_switch_ep_ids), fields.DEVICE_TYPE_ID.GENERIC_SWITCH end elseif #onoff_ep_ids > 0 then - return get_first_non_zero_endpoint(onoff_ep_ids) + return get_first_non_zero_endpoint(onoff_ep_ids), fields.DEVICE_TYPE_ID.LIGHT.ON_OFF elseif #momentary_switch_ep_ids > 0 then - return get_first_non_zero_endpoint(momentary_switch_ep_ids) + return get_first_non_zero_endpoint(momentary_switch_ep_ids), fields.DEVICE_TYPE_ID.GENERIC_SWITCH end device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return device.MATTER_DEFAULT_ENDPOINT + return device.MATTER_DEFAULT_ENDPOINT, utils.find_primary_device_type(utils.get_endpoint_info(device, device.MATTER_DEFAULT_ENDPOINT)) end function utils.component_to_endpoint(device, component) local map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} if map[component] then - return map[component] + -- if it's not a number, it should be a table with at least an endpoint_id fields + return (type(map[component]) == "number") and map[component] or map[component].endpoint_id end return utils.find_default_endpoint(device) end @@ -371,10 +386,6 @@ function utils.deep_equals(a, b, opts, seen) return utils.deep_equals(mt_a, mt_b, opts, seen) end -function utils.detect_bridge(device) - return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 -end - --- Generalizes the 'get_latest_state' function to be callable with extra endpoint information, described below, --- without directly specifying the expected component. See the 'get_latest_state' definition for more --- information about parameters and expected functionality otherwise. @@ -440,7 +451,7 @@ function utils.handle_electrical_sensor_info(device) local electrical_sensor_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR, { with_info = true }) if #electrical_sensor_eps == 0 then -- no Electrical Sensor EPs are supported. Set profiling data to false and return - device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, {persist=true}) + utils.set_preprofiling_data(device, fields.profiling_data.POWER_TOPOLOGY, false) return end @@ -465,7 +476,7 @@ function utils.handle_electrical_sensor_info(device) device:set_field(fields.ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) elseif clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, endpoint_power_topology_feature_map) then -- EP has a NODE topology, so there is only ONE Electrical Sensor EP - device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, {persist=true}) + utils.set_preprofiling_data(device, fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.NODE_TOPOLOGY) if utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sensor_eps[1], device:get_endpoints(clusters.OnOff.ID)) == false then device.log.warn("Electrical Sensor EP with NODE topology found, but no OnOff EPs exist. Electrical Sensor capabilities will not be exposed.") end @@ -473,6 +484,10 @@ function utils.handle_electrical_sensor_info(device) end end +function utils.set_preprofiling_data(device, profiling_field, value) + device:set_field(profiling_field, value, {persist=true}) +end + function utils.lazy_load(sub_driver_name) if version.api >= 16 then return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) @@ -492,34 +507,43 @@ end --- helper for the switch subscribe override, which adds to a subscribed request for a checked device --- --- @param checked_device any a Matter device object, either a parent or child device, so not necessarily the same as device +--- @param parent_device any the parent Matter device object; could be the same as checked_device if checked_device is the parent device --- @param subscribe_request table a subscribe request that will be appended to as needed for the device --- @param capabilities_seen table a list of capabilities that have already been checked by previously handled devices --- @param attributes_seen table a list of attributes that have already been checked --- @param events_seen table a list of events that have already been checked --- @param subscribed_attributes table key-value pairs mapping capability ids to subscribed attributes --- @param subscribed_events table key-value pairs mapping capability ids to subscribed events -function utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, subscribed_attributes, subscribed_events) - for _, component in pairs(checked_device.st_store.profile.components) do +local function populate_subscribe_request_for_device(checked_device, parent_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, subscribed_attributes, subscribed_events) + for _, component in pairs(checked_device.st_store.profile.components) do for _, capability in pairs(component.capabilities) do if not capabilities_seen[capability.id] then for _, attr in ipairs(subscribed_attributes[capability.id] or {}) 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 + if #parent_device:get_endpoints(cluster_id) > 0 then + 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 + else + log.warn_with({ hub_logs = true }, string.format("Device does not support cluster 0x%04X, not adding subscribed attribute", cluster_id)) end end for _, event in ipairs(subscribed_events[capability.id] or {}) do local cluster_id = event.cluster or event._cluster.ID - local event_id = event.ID or event.event - if not events_seen[cluster_id] or not events_seen[cluster_id][event_id] then - local ib = im.InteractionInfoBlock(nil, cluster_id, nil, event_id) - subscribe_request:with_info_block(ib) - events_seen[cluster_id] = events_seen[cluster_id] or {} - events_seen[cluster_id][event_id] = ib + if #parent_device:get_endpoints(cluster_id) > 0 then + local event_id = event.ID or event.event + if not events_seen[cluster_id] or not events_seen[cluster_id][event_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, nil, event_id) + subscribe_request:with_info_block(ib) + events_seen[cluster_id] = events_seen[cluster_id] or {} + events_seen[cluster_id][event_id] = ib + end + else + log.warn_with({ hub_logs = true }, string.format("Device does not support cluster 0x%04X, not adding subscribed event", cluster_id)) end end capabilities_seen[capability.id] = true -- only loop through any capability once @@ -528,6 +552,53 @@ function utils.populate_subscribe_request_for_device(checked_device, subscribe_r end end +--- aggregate the subscribed_attributes and subscribed_events tables with capability-subscription tables found in sub-drivers +--- +--- @param device any a Matter device object +--- @param checked_device any a Matter device object, either a parent or child device, so not necessarily the same as device +--- @param subscribe_request any a subscribe request that will be appended to as needed for the device +--- @param subscribed_attributes any table key-value pairs mapping capability ids to subscribed attributes, which will be appended to as needed for the device +--- @param subscribed_events any table key-value pairs mapping capability ids to subscribed events, which will be appended to as needed for the device +local function aggregate_sub_driver_subscriptions(device, checked_device, subscribe_request, subscribed_attributes, subscribed_events) + for _, sub_driver in ipairs(device.driver.sub_drivers) do + if sub_driver.can_handle({}, device.driver, checked_device) then + local sub_driver_subscriptions = require(string.format("%s.%s_utils.subscriptions", sub_driver.NAME, string.gsub(sub_driver.NAME, "sub_drivers.", ""))) + for capability, cluster_attributes in pairs(sub_driver_subscriptions.subscribed_attributes or {}) do + if subscribed_attributes[capability] then + for _, attr in pairs(cluster_attributes or {}) do + if not utils.tbl_contains(subscribed_attributes[capability], attr) then + table.insert(subscribed_attributes[capability], attr) + end + end + else + subscribed_attributes[capability] = cluster_attributes + end + end + for capability, cluster_events in pairs(sub_driver_subscriptions.subscribed_events or {}) do + if subscribed_events[capability] then + for _, event in pairs(cluster_events or {}) do + if not utils.tbl_contains(subscribed_events[capability], event) then + table.insert(subscribed_events[capability], event) + end + end + else + subscribed_events[capability] = cluster_events + end + end + for condition_fn, cluster_attributes in pairs(sub_driver_subscriptions.conditional_subscriptions or {}) do + if condition_fn(checked_device) then + for _, cluster_attribute in pairs(cluster_attributes or {}) do + local cluster_id = cluster_attribute.cluster or cluster_attribute._cluster.ID + local attr_id = cluster_attribute.ID or cluster_attribute.attribute + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + end + end + end + end + end +end + --- create and send a subscription request by checking all devices, accounting for both parent and child devices --- --- @param device any a Matter device object @@ -538,8 +609,10 @@ function utils.subscribe(device) for _, endpoint_info in ipairs(device.endpoints) do local checked_device = utils.find_child(device, endpoint_info.endpoint_id) or device if not devices_seen[checked_device.id] then - utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, - device.driver.subscribed_attributes, device.driver.subscribed_events + local subscribed_attributes, subscribed_events = device.driver.subscribed_attributes, device.driver.subscribed_events + aggregate_sub_driver_subscriptions(device, checked_device, subscribe_request, subscribed_attributes, subscribed_events) + populate_subscribe_request_for_device(checked_device, device, subscribe_request, capabilities_seen, attributes_seen, events_seen, + subscribed_attributes, subscribed_events ) devices_seen[checked_device.id] = true -- only loop through any device once end diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index 305c7e682e..1e23ffff4a 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua @@ -130,7 +130,6 @@ local function test_init() end test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua index 667e68ec16..9b29c85bfb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -181,7 +181,6 @@ local function test_init() -- Test added -> doConfigure logic test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua index 180604431b..1eaca84c36 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -183,7 +183,6 @@ local function test_init() end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) end test.set_test_init_function(test_init) @@ -199,7 +198,6 @@ local function test_init_periodic() test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "init" }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) end test.register_message_test( diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua index f618389ba6..86a2c34d22 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua @@ -130,7 +130,6 @@ local function test_init() end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua index 5fa58ce8d3..e5c3b5dfc5 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -127,7 +127,6 @@ local function test_init_electrical_sensor() end test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "added" }) - test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "init" }) test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua index ccf952a824..a7d34b7cd8 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua @@ -107,7 +107,6 @@ end local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -118,7 +117,6 @@ test.set_test_init_function(test_init) local function test_init_x_y_color_mode() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua index 6cb679796d..0f108494df 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua @@ -38,7 +38,7 @@ local mock_bridge = test.mock_device.build_test_matter_device({ {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} }, device_types = { - {device_type_id = 0x0100, device_type_revision = 1} -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 1} -- Dimmable Light } }, { @@ -53,7 +53,7 @@ local mock_bridge = test.mock_device.build_test_matter_device({ {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} }, device_types = { - {device_type_id = 0x0100, device_type_revision = 1} -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 1} -- Dimmable Light } } } @@ -83,6 +83,22 @@ local function test_init_mock_bridge() test.mock_device.add_test_device(mock_bridge) test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "init" }) + test.socket.matter:__expect_send({ mock_bridge.id, clusters.LevelControl.attributes.Options:write(mock_bridge, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) }) + test.socket.matter:__expect_send({ mock_bridge.id, clusters.LevelControl.attributes.Options:write(mock_bridge, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) }) + mock_bridge:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 1", + profile = "light-level", + parent_device_id = mock_bridge.id, + parent_assigned_child_key = string.format("%d", 1) + }) + mock_bridge:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "light-level", + parent_device_id = mock_bridge.id, + parent_assigned_child_key = string.format("%d", 2) + }) test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "doConfigure" }) mock_bridge:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua index 86ce8185bf..9d1a3e1698 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -104,7 +104,6 @@ local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -133,7 +132,6 @@ local function test_init_battery() test.disable_startup_messages() test.mock_device.add_test_device(mock_device_battery) test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) - test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) 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 fceaa4ccf5..30c66cea6c 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -5,171 +5,78 @@ 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 switch_fields = require "switch_utils.fields" local t_utils = require "integration_test.utils" local test = require "integration_test" local uint32 = require "st.matter.data_types.Uint32" test.disable_startup_messages() -local CAMERA_EP, FLOODLIGHT_EP, CHIME_EP, DOORBELL_EP = 1, 2, 3, 4 +local CAMERA_EP = 1 -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("camera.yml"), - manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - matter_version = {hardware = 1, software = 1}, - endpoints = { - { - endpoint_id = 0, - clusters = { - { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } - }, - device_types = { - { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode - } +local endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - { - endpoint_id = CAMERA_EP, - clusters = { - { - cluster_id = clusters.CameraAvStreamManagement.ID, - feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO | - clusters.CameraAvStreamManagement.types.Feature.PRIVACY | - clusters.CameraAvStreamManagement.types.Feature.AUDIO | - clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE | - clusters.CameraAvStreamManagement.types.Feature.PRIVACY | - clusters.CameraAvStreamManagement.types.Feature.SPEAKER | - clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL | - clusters.CameraAvStreamManagement.types.Feature.SPEAKER | - clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE | - clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION | - clusters.CameraAvStreamManagement.types.Feature.WATERMARK | - clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY, - cluster_type = "SERVER" - }, - { - cluster_id = clusters.CameraAvSettingsUserLevelManagement.ID, - feature_map = clusters.CameraAvSettingsUserLevelManagement.types.Feature.DIGITALPTZ | - clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN | - clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT | - clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM | - clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS, - cluster_type = "SERVER" - }, - { - cluster_id = clusters.PushAvStreamTransport.ID, - cluster_type = "SERVER" - }, - { - cluster_id = clusters.ZoneManagement.ID, - feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | - clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY, - cluster_type = "SERVER" - }, - { - cluster_id = clusters.WebRTCTransportProvider.ID, - cluster_type = "SERVER" - }, - { - cluster_id = clusters.OccupancySensing.ID, - cluster_type = "SERVER" - } + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = CAMERA_EP, + clusters = { + { + cluster_id = clusters.CameraAvStreamManagement.ID, + feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO | + clusters.CameraAvStreamManagement.types.Feature.PRIVACY | + clusters.CameraAvStreamManagement.types.Feature.AUDIO | + clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE | + clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION | + clusters.CameraAvStreamManagement.types.Feature.WATERMARK | + clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY, + cluster_type = "SERVER" }, - device_types = { - {device_type_id = 0x0142, device_type_revision = 1} -- Camera - } - }, - { - endpoint_id = FLOODLIGHT_EP, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30} + { + cluster_id = clusters.CameraAvSettingsUserLevelManagement.ID, + feature_map = clusters.CameraAvSettingsUserLevelManagement.types.Feature.DIGITALPTZ | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS, + cluster_type = "SERVER" }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - { - endpoint_id = CHIME_EP, - clusters = { - { - cluster_id = clusters.Chime.ID, - cluster_type = "SERVER" - }, + { + cluster_id = clusters.PushAvStreamTransport.ID, + cluster_type = "SERVER" }, - device_types = { - {device_type_id = 0x0146, device_type_revision = 1} -- Chime - } - }, - { - endpoint_id = DOORBELL_EP, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, - cluster_type = "SERVER", - } + { + cluster_id = clusters.ZoneManagement.ID, + feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | + clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.WebRTCTransportProvider.ID, + cluster_type = "SERVER" }, - device_types = { - {device_type_id = 0x0143, device_type_revision = 1} -- Doorbell + { + cluster_id = clusters.OccupancySensing.ID, + cluster_type = "SERVER" } + }, + device_types = { + {device_type_id = switch_fields.DEVICE_TYPE_ID.CAMERA, device_type_revision = 1} } } -}) - -local subscribe_request -local subscribed_attributes = { - clusters.CameraAvStreamManagement.attributes.AttributeList, - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, } -local function test_init() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - local floodlight_child_device_data = { - profile = t_utils.get_profile_definition("light-color-level.yml"), - device_network_id = string.format("%s:%d", mock_device.id, FLOODLIGHT_EP), - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP) - } - test.mock_device.add_test_device(test.mock_device.build_test_child_device(floodlight_child_device_data)) - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Floodlight 1", - profile = "light-color-level", - parent_device_id = mock_device.id, - 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 - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -test.set_test_init_function(test_init) - -local additional_subscribed_attributes = { +local additional_subscriptions = { clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, clusters.CameraAvStreamManagement.attributes.ImageRotation, clusters.CameraAvStreamManagement.attributes.NightVision, @@ -188,8 +95,6 @@ local additional_subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel, - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled, @@ -208,8 +113,6 @@ local additional_subscribed_attributes = { clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin, clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams, - clusters.Chime.attributes.InstalledChimeSounds, - clusters.Chime.attributes.SelectedChime, clusters.ZoneManagement.attributes.MaxZones, clusters.ZoneManagement.attributes.Zones, clusters.ZoneManagement.attributes.Triggers, @@ -217,193 +120,74 @@ local additional_subscribed_attributes = { clusters.ZoneManagement.attributes.Sensitivity, clusters.ZoneManagement.events.ZoneTriggered, clusters.ZoneManagement.events.ZoneStopped, - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, clusters.OccupancySensing.attributes.Occupancy, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete } +local function create_subscription(device) + local subscribe_request = clusters.CameraAvStreamManagement.attributes.AttributeList:subscribe(device) + subscribe_request:merge(cluster_base.subscribe(device, nil, camera_fields.CameraAVSMFeatureMapAttr.cluster, + camera_fields.CameraAVSMFeatureMapAttr.ID)) + subscribe_request:merge(cluster_base.subscribe(device, nil, camera_fields.CameraAVSULMFeatureMapAttr.cluster, + camera_fields.CameraAVSULMFeatureMapAttr.ID)) + subscribe_request:merge(cluster_base.subscribe(device, nil, camera_fields.ZoneManagementFeatureMapAttr.cluster, + camera_fields.ZoneManagementFeatureMapAttr.ID)) + for _, attr in ipairs(additional_subscriptions) do + subscribe_request:merge(attr:subscribe(device)) + end + return subscribe_request +end + local expected_metadata = { optional_component_capabilities = { - { - "main", - { - "videoCapture2", - "cameraViewportSettings", - "videoStreamSettings", - "localMediaStorage", - "audioRecording", - "cameraPrivacyMode", - "imageControl", - "hdr", - "nightVision", - "mechanicalPanTiltZoom", - "zoneManagement", - "webrtc", + {"main", { + "videoCapture2", "cameraViewportSettings", "videoStreamSettings", + "localMediaStorage", "audioRecording", "cameraPrivacyMode", + "imageControl", "hdr", "nightVision", + "mechanicalPanTiltZoom", "zoneManagement", "webrtc", "motionSensor", - "sounds", - } - }, - { - "statusLed", - { - "switch", - "mode" - } - }, - { - "speaker", - { - "audioMute", - "audioVolume" - } - }, - { - "microphone", - { - "audioMute", - "audioVolume" } }, - { - "doorbell", - { - "button" - } - } + {"speaker", {"audioMute", "audioVolume"}}, + {"microphone", {"audioMute", "audioVolume"}} }, profile = "camera" } -local function update_device_profile() - 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 mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml", { enabled_optional_capabilities = expected_metadata.optional_component_capabilities }), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = endpoints +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) + mock_device:set_field(switch_fields.profiling_data.STATUS_LIGHT_BRIGHTNESS_PRESENT, false, {persist=true}) + mock_device:set_field(switch_fields.profiling_data.STATUS_LIGHT_ENABLED_PRESENT, false, {persist=true}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, create_subscription(mock_device) }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update(expected_metadata) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) - test.wait_for_events() - local updated_device_profile = t_utils.get_profile_definition( - "camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} - ) - test.wait_for_events() - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_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"} - )) - ) - for _, attr in ipairs(additional_subscribed_attributes) do - subscribe_request:merge(attr:subscribe(mock_device)) - 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)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end --- Matter Handler UTs +test.set_test_init_function(test_init) test.register_coroutine_test( - "Software version change should trigger camera reprofiling when camera endpoint is present", + "Software version change should initialize camera capabilities when profile is unchanged", function() + mock_device:set_field(switch_fields.profiling_data.STATUS_LIGHT_BRIGHTNESS_PRESENT, false) + mock_device:set_field(switch_fields.profiling_data.STATUS_LIGHT_ENABLED_PRESENT, false) + local camera_utils = require "sub_drivers.camera.camera_utils.utils" + camera_utils.optional_capabilities_list_changed = function () return false end -- integration profile ref logic makes this fn inaccurate + + local unchanged_profile = t_utils.get_profile_definition("camera.yml", { enabled_optional_capabilities = expected_metadata.optional_component_capabilities }) + unchanged_profile.id = "00000000-1111-2222-3333-000000000002" + unchanged_profile.preferences = nil test.socket.device_lifecycle:__queue_receive( - mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } }) + mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 }, profile = unchanged_profile }) ) - - mock_device:expect_metadata_update({ - optional_component_capabilities = { - { - "main", - { - "videoCapture2", - "cameraViewportSettings", - "videoStreamSettings", - "localMediaStorage", - "audioRecording", - "cameraPrivacyMode", - "imageControl", - "hdr", - "nightVision", - "mechanicalPanTiltZoom", - "zoneManagement", - "webrtc", - "motionSensor", - "sounds" - } - }, - { - "speaker", - { - "audioMute", - "audioVolume" - } - }, - { - "microphone", - { - "audioMute", - "audioVolume" - } - }, - { - "doorbell", - { - "button" - } - } - }, - profile = "camera" - }) - - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) end, { min_api_version = 17 @@ -411,65 +195,14 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Software version change should initialize camera capabilities when profile is unchanged", + "Software version change should trigger camera reprofiling when camera endpoint is present", 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") + mock_device:set_field(switch_fields.profiling_data.STATUS_LIGHT_BRIGHTNESS_PRESENT, false) + mock_device:set_field(switch_fields.profiling_data.STATUS_LIGHT_ENABLED_PRESENT, false) + test.socket.device_lifecycle:__queue_receive( + mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } }) + ) + mock_device:expect_metadata_update(expected_metadata) end, { min_api_version = 17 @@ -480,21 +213,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, @@ -503,109 +232,9 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "Camera privacy mode state compare should ignore table metatable differences", - function() - local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" - - local init_event_count = 0 - local original_match_profile = camera_cfg.match_profile - - camera_cfg.match_profile = function() - return false - end - - local fake_device = { - supports_capability = function(_, capability) - return capability == capabilities.cameraPrivacyMode - end, - get_latest_state = function(_, _, _, attribute_name) - if attribute_name == capabilities.cameraPrivacyMode.supportedAttributes.NAME then - return { "softRecordingPrivacyMode", "softLivestreamPrivacyMode" } - elseif attribute_name == capabilities.cameraPrivacyMode.supportedCommands.NAME then - local commands = { "setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode" } - setmetatable(commands, { - __index = function() - return nil - end - }) - return commands - end - return nil - end, - get_endpoints = function() - return { CAMERA_EP } - end, - emit_event_for_endpoint = function() - init_event_count = init_event_count + 1 - end - } - - camera_cfg.reconcile_profile_and_capabilities(fake_device) - camera_cfg.match_profile = original_match_profile - - assert(init_event_count == 0, "cameraPrivacyMode should not be reinitialized for equal values with metatable differences") - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Reports mapping to EnabledState capability data type should generate appropriate events", - function() - update_device_profile() - test.wait_for_events() - local cluster_to_capability_map = { - {cluster = clusters.CameraAvStreamManagement.server.attributes.HDRModeEnabled, capability = capabilities.hdr.hdr}, - {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipHorizontal, capability = capabilities.imageControl.imageFlipHorizontal}, - {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipVertical, capability = capabilities.imageControl.imageFlipVertical}, - {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftRecordingPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softRecordingPrivacyMode}, - {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftLivestreamPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softLivestreamPrivacyMode}, - {cluster = clusters.CameraAvStreamManagement.server.attributes.HardPrivacyModeOn, capability = capabilities.cameraPrivacyMode.hardPrivacyMode}, - {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalSnapshotRecordingEnabled, capability = capabilities.localMediaStorage.localSnapshotRecording}, - {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalVideoRecordingEnabled, capability = capabilities.localMediaStorage.localVideoRecording} - } - for _, v in ipairs(cluster_to_capability_map) do - test.socket.matter:__queue_receive({ - mock_device.id, - v.cluster:build_test_report_data(mock_device, CAMERA_EP, true) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", v.capability("enabled")) - ) - if v.capability == capabilities.imageControl.imageFlipHorizontal then - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal"})) - ) - elseif v.capability == capabilities.imageControl.imageFlipVertical then - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal", "imageFlipVertical"})) - ) - elseif v.capability == capabilities.cameraPrivacyMode.hardPrivacyMode then - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes({"softRecordingPrivacyMode", "softLivestreamPrivacyMode", "hardPrivacyMode"})) - ) - end - test.socket.matter:__queue_receive({ - mock_device.id, - v.cluster:build_test_report_data(mock_device, CAMERA_EP, false) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", v.capability("disabled")) - ) - end - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Night Vision reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() local cluster_to_capability_map = { {cluster = clusters.CameraAvStreamManagement.server.attributes.NightVision, capability = capabilities.nightVision.nightVision}, {cluster = clusters.CameraAvStreamManagement.server.attributes.NightVisionIllum, capability = capabilities.nightVision.illumination} @@ -648,8 +277,6 @@ test.register_coroutine_test( "Image Rotation reports should generate appropriate events", function() local utils = require "st.utils" - update_device_profile() - test.wait_for_events() local first_value = true for angle = 0, 400, 50 do test.socket.matter:__queue_receive({ @@ -676,8 +303,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Two Way Talk Support reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvStreamManagement.server.attributes.TwoWayTalkSupport:build_test_report_data( @@ -720,8 +345,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Muted reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() local cluster_to_component_map = { {cluster = clusters.CameraAvStreamManagement.server.attributes.SpeakerMuted, component = "speaker"}, {cluster = clusters.CameraAvStreamManagement.server.attributes.MicrophoneMuted, component = "microphone"} @@ -751,8 +374,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Volume Level reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() local max_vol = 200 local min_vol = 0 test.socket.matter:__queue_receive({ @@ -798,84 +419,6 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "Status Light Enabled reports should generate appropriate events", - function() - update_device_profile() - test.wait_for_events() - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP, true) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.switch.switch.on()) - ) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP, false) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.switch.switch.off()) - ) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Status Light Brightness reports should generate appropriate events", - function() - update_device_profile() - test.wait_for_events() - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( - mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.LOW) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.mode.supportedModes( - {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.mode.supportedArguments( - {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.mode.mode("low")) - ) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( - mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.MEDIUM) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.mode.mode("medium")) - ) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( - mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.HIGH) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.mode.mode("high")) - ) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( - mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.AUTO) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("statusLed", capabilities.mode.mode("auto")) - ) - end, - { - min_api_version = 17 - } -) - local function receive_rate_distortion_trade_off_points() test.socket.matter:__queue_receive({ mock_device.id, @@ -985,8 +528,6 @@ end test.register_coroutine_test( "Rate Distortion Trade Off Points, MaxEncodedPixelRate, MinViewport, VideoSensorParams reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() receive_rate_distortion_trade_off_points() receive_max_encoded_pixel_rate() receive_min_viewport() @@ -1003,8 +544,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Rate Distortion Trade Off Points, MinViewport, VideoSensorParams, MaxEncodedPixelRate reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() receive_rate_distortion_trade_off_points() receive_min_viewport() emit_min_viewport() @@ -1021,8 +560,6 @@ test.register_coroutine_test( test.register_coroutine_test( "MaxEncodedPixelRate, MinViewport, VideoSensorParams, Rate Distortion Trade Off Points reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() receive_max_encoded_pixel_rate() receive_min_viewport() emit_min_viewport() @@ -1039,8 +576,6 @@ test.register_coroutine_test( test.register_coroutine_test( "PTZ Position reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax:build_test_report_data(mock_device, CAMERA_EP, 150) @@ -1094,8 +629,6 @@ test.register_coroutine_test( test.register_coroutine_test( "PTZ Presets reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets:build_test_report_data( @@ -1118,8 +651,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Max Presets reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets:build_test_report_data(mock_device, CAMERA_EP, 10) @@ -1136,8 +667,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Max Zones reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.ZoneManagement.attributes.MaxZones:build_test_report_data(mock_device, CAMERA_EP, 10) @@ -1154,8 +683,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Zones reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.ZoneManagement.attributes.Zones:build_test_report_data( @@ -1202,8 +729,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Triggers reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.ZoneManagement.attributes.Triggers:build_test_report_data( @@ -1240,8 +765,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Sensitivity reports should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.ZoneManagement.attributes.SensitivityMax:build_test_report_data(mock_device, CAMERA_EP, 7) @@ -1263,42 +786,11 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "Chime reports should generate appropriate events", - function() - update_device_profile() - test.wait_for_events() - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Chime.attributes.InstalledChimeSounds:build_test_report_data(mock_device, CAMERA_EP, { - clusters.Chime.types.ChimeSoundStruct({chime_id = 1, name = "Sound 1"}), - clusters.Chime.types.ChimeSoundStruct({chime_id = 2, name = "Sound 2"}) - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.sounds.supportedSounds({ - {id = 1, label = "Sound 1"}, - {id = 2, label = "Sound 2"}, - }, {visibility = {displayed = false}})) - ) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Chime.attributes.SelectedChime:build_test_report_data(mock_device, CAMERA_EP, 2) - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.sounds.selectedSound(2))) - end, - { - min_api_version = 17 - } -) - -- Event Handler UTs test.register_coroutine_test( "Zone events should generate appropriate events", function() - update_device_profile() - test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, { @@ -1335,39 +827,11 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "Button events should generate appropriate events", - function() - update_device_profile() - test.wait_for_events() - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.server.events.InitialPress:build_test_event_report(mock_device, DOORBELL_EP, {new_position = 1}) - }) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.server.events.MultiPressComplete:build_test_event_report(mock_device, DOORBELL_EP, { - new_position = 1, - total_number_of_presses_counted = 2, - previous_position = 0 - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("doorbell", capabilities.button.button.double({state_change = true})) - ) - end, - { - min_api_version = 17 - } -) - -- Capability Handler UTs test.register_coroutine_test( "Set night vision commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() local command_to_attribute_map = { ["setNightVision"] = clusters.CameraAvStreamManagement.attributes.NightVision, ["setIllumination"] = clusters.CameraAvStreamManagement.attributes.NightVisionIllum @@ -1404,8 +868,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Set enabled commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() local command_to_attribute_map = { ["setHdr"] = { capability = "hdr", attr = clusters.CameraAvStreamManagement.attributes.HDRModeEnabled}, ["setImageFlipHorizontal"] = { capability = "imageControl", attr = clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal}, @@ -1440,8 +902,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Set image rotation command should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, { capability = "imageControl", component = "main", command = "setImageRotation", args = { 10 } }, @@ -1465,8 +925,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Set mute commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, { capability = "audioMute", component = "speaker", command = "setMute", args = { "muted" } }, @@ -1532,8 +990,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Set Volume command should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() local max_vol = 200 local min_vol = 5 test.socket.matter:__queue_receive({ @@ -1637,62 +1093,9 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "Set Mode command should send the appropriate commands", - function() - update_device_profile() - test.wait_for_events() - local mode_to_enum_map = { - ["low"] = clusters.Global.types.ThreeLevelAutoEnum.LOW, - ["medium"] = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM, - ["high"] = clusters.Global.types.ThreeLevelAutoEnum.HIGH, - ["auto"] = clusters.Global.types.ThreeLevelAutoEnum.AUTO - } - for i, v in pairs(mode_to_enum_map) do - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "mode", component = "speaker", command = "setMode", args = { i } }, - }) - test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:write(mock_device, CAMERA_EP, v) - }) - end - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Set Status LED commands should send the appropriate commands", - function() - update_device_profile() - test.wait_for_events() - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "switch", component = "statusLed", command = "on", args = { } }, - }) - test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP, true) - }) - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "switch", component = "statusLed", command = "off", args = { } }, - }) - test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP, false) - }) - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Set Relative PTZ commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, { capability = "mechanicalPanTiltZoom", component = "main", command = "panRelative", args = { 10 } }, @@ -1723,8 +1126,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Set PTZ commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, { capability = "mechanicalPanTiltZoom", component = "main", command = "setPanTiltZoom", args = { 10, 20, 30 } }, @@ -1804,8 +1205,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Preset commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, { capability = "mechanicalPanTiltZoom", component = "main", command = "savePreset", args = { 1, "Preset 1" } }, @@ -1833,36 +1232,10 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "Sound commands should send the appropriate commands", - function() - update_device_profile() - test.wait_for_events() - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "sounds", component = "main", command = "setSelectedSound", args = { 1 } }, - }) - test.socket.matter:__expect_send({ - mock_device.id, clusters.Chime.attributes.SelectedChime:write(mock_device, CAMERA_EP, 1) - }) - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "sounds", component = "main", command = "playSound", args = {} }, - }) - test.socket.matter:__expect_send({ - mock_device.id, clusters.Chime.server.commands.PlayChimeSound(mock_device, CAMERA_EP) - }) - end, - { - min_api_version = 17 - } -) test.register_coroutine_test( "Zone Management zone commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() local use_map = { ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, ["focus"] = clusters.ZoneManagement.types.ZoneUseEnum.FOCUS, @@ -1935,8 +1308,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Zone Management zone commands should send the appropriate commands - missing optional color argument", function() - update_device_profile() - test.wait_for_events() local use_map = { ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, ["focus"] = clusters.ZoneManagement.types.ZoneUseEnum.FOCUS, @@ -2008,9 +1379,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Zone Management trigger commands should send the appropriate commands", function() - update_device_profile() - test.wait_for_events() - -- Create the trigger test.socket.capability:__queue_receive({ mock_device.id, @@ -2084,9 +1452,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Removing a zone with an existing trigger should send RemoveTrigger followed by RemoveZone", function() - update_device_profile() - test.wait_for_events() - -- Create a zone test.socket.capability:__queue_receive({ mock_device.id, @@ -2176,8 +1541,6 @@ test.register_coroutine_test( test.register_coroutine_test( "setStream with label and viewport changes should emit capability event", function() - update_device_profile() - test.wait_for_events() -- Set up an existing stream test.socket.matter:__queue_receive({ mock_device.id, @@ -2282,8 +1645,6 @@ test.register_coroutine_test( test.register_coroutine_test( "setStream with only watermark/OSD changes should use VideoStreamModify", function() - update_device_profile() - test.wait_for_events() -- Set up an existing stream test.socket.matter:__queue_receive({ mock_device.id, @@ -2358,8 +1719,7 @@ test.register_coroutine_test( test.register_coroutine_test( "setStream with only label change should emit capability event", function() - update_device_profile() - test.wait_for_events() + -- Set up existing stream test.socket.matter:__queue_receive({ mock_device.id, @@ -2449,8 +1809,7 @@ test.register_coroutine_test( test.register_coroutine_test( "setStream with only viewport change should send DPTZSetViewport command", function() - update_device_profile() - test.wait_for_events() + -- Set up existing stream test.socket.matter:__queue_receive({ mock_device.id, @@ -2551,8 +1910,7 @@ test.register_coroutine_test( test.register_coroutine_test( "setStream with resolution change should trigger reallocation", function() - update_device_profile() - test.wait_for_events() + -- Set up existing stream test.socket.matter:__queue_receive({ mock_device.id, @@ -2704,8 +2062,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Stream label should persist across attribute reports", function() - update_device_profile() - test.wait_for_events() + -- Set up existing stream test.socket.matter:__queue_receive({ mock_device.id, @@ -2820,8 +2177,7 @@ test.register_coroutine_test( test.register_coroutine_test( "DPTZStreams attribute should update viewports in capability", function() - update_device_profile() - test.wait_for_events() + -- Set up multiple existing streams test.socket.matter:__queue_receive({ mock_device.id, @@ -2940,73 +2296,4 @@ test.register_coroutine_test( end ) -test.register_coroutine_test( - "Camera profile should not update for an unchanged Status Light AttributeList report", - function() - update_device_profile() - test.wait_for_events() - - 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, { - uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), - uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) - }) - }) - test.wait_for_events() - - camera_cfg.reconcile_profile_and_capabilities = original_reconcile - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Camera profile should update for a changed Status Light AttributeList report", - function() - update_device_profile() - test.wait_for_events() - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { - uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) - }) - }) - local updated_expected_metadata = { - optional_component_capabilities = { - { "main", - { "videoCapture2", "cameraViewportSettings", "videoStreamSettings", "localMediaStorage", "audioRecording", - "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "zoneManagement", - "webrtc", "motionSensor", "sounds", } - }, - { "statusLed", - { "switch" } -- only switch capability remains - }, - { "speaker", - { "audioMute", "audioVolume" } - }, - { "microphone", - { "audioMute", "audioVolume" } - }, - { "doorbell", - { "button" } - } - }, - profile = "camera" - } - mock_device:expect_metadata_update(updated_expected_metadata) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) - end, - { - min_api_version = 17 - } -) - --- run the tests test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera_configuration.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera_configuration.lua new file mode 100644 index 0000000000..3c7676e8e4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera_configuration.lua @@ -0,0 +1,738 @@ +-- Copyright © 2025 SmartThings, Inc. +-- 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 switch_fields = require "switch_utils.fields" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local uint32 = require "st.matter.data_types.Uint32" + +test.disable_startup_messages() + +local CAMERA_EP_ID = 1 +local CAMERA_EP = { + endpoint_id = CAMERA_EP_ID, + clusters = { + { + cluster_id = clusters.CameraAvStreamManagement.ID, + feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO | + clusters.CameraAvStreamManagement.types.Feature.PRIVACY | + clusters.CameraAvStreamManagement.types.Feature.AUDIO | + clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE | + clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION | + clusters.CameraAvStreamManagement.types.Feature.WATERMARK | + clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.CameraAvSettingsUserLevelManagement.ID, + feature_map = clusters.CameraAvSettingsUserLevelManagement.types.Feature.DIGITALPTZ | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.PushAvStreamTransport.ID, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.ZoneManagement.ID, + feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | + clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.WebRTCTransportProvider.ID, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = switch_fields.DEVICE_TYPE_ID.CAMERA, device_type_revision = 1} + } + } + +local CHIME_EP_ID = 2 +local CHIME_EP = { + endpoint_id = CHIME_EP_ID, + clusters = { + { + cluster_id = clusters.Chime.ID, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = switch_fields.DEVICE_TYPE_ID.CHIME, device_type_revision = 1} -- Chime + } +} + +local DOORBELL_EP_ID = 3 +local DOORBELL_EP = { + endpoint_id = DOORBELL_EP_ID, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER", + } + }, + device_types = { + {device_type_id = switch_fields.DEVICE_TYPE_ID.DOORBELL, device_type_revision = 1} + } +} + +local FLOODLIGHT_EP_ID = 4 +local FLOODLIGHT_EP = { + endpoint_id = FLOODLIGHT_EP_ID, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + + device_types = { + {device_type_id = switch_fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE, device_type_revision = 2} + } +} + +local chime_subscriptions = { + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime +} + +local doorbell_subscriptions = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete +} + +local floodlight_subscriptions = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, -- also required due to switch cluster catching it +} + +local status_led_subscriptions = { + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness +} + +local camera_subscriptions = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation, + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum, + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical, + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn, + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport, + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvStreamManagement.attributes.Viewport, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.AttributeList, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin, + clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams, + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity, + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped, +} + +local function create_subscription(device, with_camera, with_status_led, with_floodlight, with_doorbell, with_chime) + local subscribe_request = clusters.CameraAvStreamManagement.attributes.AttributeList:subscribe(device) + subscribe_request:merge(cluster_base.subscribe(device, nil, camera_fields.CameraAVSMFeatureMapAttr.cluster, camera_fields.CameraAVSMFeatureMapAttr.ID)) + subscribe_request:merge(cluster_base.subscribe(device, nil, camera_fields.CameraAVSULMFeatureMapAttr.cluster, camera_fields.CameraAVSULMFeatureMapAttr.ID)) + subscribe_request:merge(cluster_base.subscribe(device, nil, camera_fields.ZoneManagementFeatureMapAttr.cluster, camera_fields.ZoneManagementFeatureMapAttr.ID)) + local merge_subscriptions = function(cluster_list) + for _, attr in ipairs(cluster_list) do + subscribe_request:merge(attr:subscribe(device)) + end + end + if with_camera then merge_subscriptions(camera_subscriptions) end + if with_status_led then merge_subscriptions(status_led_subscriptions) end + if with_floodlight then merge_subscriptions(floodlight_subscriptions) end + if with_doorbell then merge_subscriptions(doorbell_subscriptions) end + if with_chime then merge_subscriptions(chime_subscriptions) end + return subscribe_request +end + +local expected_metadata = { + optional_component_capabilities = { + {"main", { + "videoCapture2", "cameraViewportSettings", "videoStreamSettings", + "localMediaStorage", "audioRecording", "cameraPrivacyMode", + "imageControl", "hdr", "nightVision", + "mechanicalPanTiltZoom", "zoneManagement", "webrtc", + } + }, + {"speaker", {"audioMute", "audioVolume"}}, + {"microphone", {"audioMute", "audioVolume"}} + }, + profile = "camera" +} + +local expected_metadata_with_status_led = { + optional_component_capabilities = { + {"main", { + "videoCapture2", "cameraViewportSettings", "videoStreamSettings", + "localMediaStorage", "audioRecording", "cameraPrivacyMode", + "imageControl", "hdr", "nightVision", + "mechanicalPanTiltZoom", "zoneManagement", "webrtc", + } + }, + {"statusLed", {"switch", "mode"}}, + {"speaker", {"audioMute", "audioVolume"}}, + {"microphone", {"audioMute", "audioVolume"}} + }, + profile = "camera" +} + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { CAMERA_EP } +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, create_subscription(mock_device, false) }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +local function mock_initial_camera_update(device, updated_optional_component_capabilities, updated_subscription, with_doorbell_events) + local updated_device_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = updated_optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(device:generate_info_changed({ profile = updated_device_profile })) + if with_doorbell_events then + test.socket.capability:__expect_send(device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) + end + test.socket.capability:__expect_send( + 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( + device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.supportedAttributes( + {"pan", "panRange", "tilt", "tiltRange", "zoom", "zoomRange", "presets", "maxPresets"} + )) + ) + test.socket.capability:__expect_send( + device:generate_test_message("main", capabilities.zoneManagement.supportedFeatures( + {"triggerAugmentation", "perZoneSensitivity"} + )) + ) + test.socket.capability:__expect_send( + device:generate_test_message("main", capabilities.localMediaStorage.supportedAttributes( + {"localVideoRecording"} + )) + ) + test.socket.capability:__expect_send( + device:generate_test_message("main", capabilities.audioRecording.audioRecording("enabled")) + ) + test.socket.capability:__expect_send( + device:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports", "watermark", "onScreenDisplay"} + )) + ) + test.socket.capability:__expect_send( + device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes( + {"softRecordingPrivacyMode", "softLivestreamPrivacyMode"} + )) + ) + test.socket.capability:__expect_send( + device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedCommands( + {"setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode"} + )) + ) + test.socket.matter:__expect_send({device.id, updated_subscription}) + if with_doorbell_events then + test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, DOORBELL_EP_ID)}) + end + test.wait_for_events() +end + +test.register_coroutine_test( + "Initial profile update should trigger appropriate capability updates and subscriptions", + function () + mock_initial_camera_update(mock_device, expected_metadata.optional_component_capabilities, create_subscription(mock_device, true)) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Reports mapping to EnabledState capability data type should generate appropriate events", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP_ID, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device:expect_metadata_update(expected_metadata_with_status_led) + test.wait_for_events() + mock_initial_camera_update(mock_device, expected_metadata_with_status_led.optional_component_capabilities, create_subscription(mock_device, true, true)) + local cluster_to_capability_map = { + {cluster = clusters.CameraAvStreamManagement.server.attributes.HDRModeEnabled, capability = capabilities.hdr.hdr}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipHorizontal, capability = capabilities.imageControl.imageFlipHorizontal}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipVertical, capability = capabilities.imageControl.imageFlipVertical}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftRecordingPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softRecordingPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftLivestreamPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softLivestreamPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.HardPrivacyModeOn, capability = capabilities.cameraPrivacyMode.hardPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalSnapshotRecordingEnabled, capability = capabilities.localMediaStorage.localSnapshotRecording}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalVideoRecordingEnabled, capability = capabilities.localMediaStorage.localVideoRecording} + } + for _, v in ipairs(cluster_to_capability_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP_ID, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("enabled")) + ) + if v.capability == capabilities.imageControl.imageFlipHorizontal then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal"})) + ) + elseif v.capability == capabilities.imageControl.imageFlipVertical then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal", "imageFlipVertical"})) + ) + elseif v.capability == capabilities.cameraPrivacyMode.hardPrivacyMode then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes({"softRecordingPrivacyMode", "softLivestreamPrivacyMode", "hardPrivacyMode"})) + ) + end + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP_ID, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("disabled")) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Status Light Enabled reports should generate appropriate events", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP_ID, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device:expect_metadata_update(expected_metadata_with_status_led) + test.wait_for_events() + mock_initial_camera_update(mock_device, expected_metadata_with_status_led.optional_component_capabilities, create_subscription(mock_device, true, true)) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP_ID, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.switch.switch.on()) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP_ID, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.switch.switch.off()) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Status Light Brightness reports should generate appropriate events", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP_ID, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device:expect_metadata_update(expected_metadata_with_status_led) + test.wait_for_events() + mock_initial_camera_update(mock_device, expected_metadata_with_status_led.optional_component_capabilities, create_subscription(mock_device, true, true)) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP_ID, clusters.Global.types.ThreeLevelAutoEnum.LOW) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.supportedModes( + {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.supportedArguments( + {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("low")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP_ID, clusters.Global.types.ThreeLevelAutoEnum.MEDIUM) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("medium")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP_ID, clusters.Global.types.ThreeLevelAutoEnum.HIGH) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("high")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP_ID, clusters.Global.types.ThreeLevelAutoEnum.AUTO) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("auto")) + ) + end, + { + min_api_version = 17 + } +) + + +test.register_coroutine_test( + "Set Mode command should send the appropriate commands", + function() + mock_initial_camera_update(mock_device, expected_metadata_with_status_led.optional_component_capabilities, create_subscription(mock_device, true, true)) + local mode_to_enum_map = { + ["low"] = clusters.Global.types.ThreeLevelAutoEnum.LOW, + ["medium"] = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM, + ["high"] = clusters.Global.types.ThreeLevelAutoEnum.HIGH, + ["auto"] = clusters.Global.types.ThreeLevelAutoEnum.AUTO + } + for i, v in pairs(mode_to_enum_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "speaker", command = "setMode", args = { i } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:write(mock_device, CAMERA_EP_ID, v) + }) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Set Status LED commands should send the appropriate commands", + function() + mock_initial_camera_update(mock_device, expected_metadata_with_status_led.optional_component_capabilities, create_subscription(mock_device, true, true)) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "statusLed", command = "on", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP_ID, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "statusLed", command = "off", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP_ID, false) + }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Camera profile should not update for an unchanged Status Light AttributeList report", + function() + 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_ID, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + test.wait_for_events() + camera_cfg.reconcile_profile_and_capabilities = original_reconcile + end, + { + min_api_version = 17 + } +) + + +local expected_metadata_with_doorbell_chime = { + optional_component_capabilities = { + {"main", { + "videoCapture2", "cameraViewportSettings", "videoStreamSettings", + "localMediaStorage", "audioRecording", "cameraPrivacyMode", + "imageControl", "hdr", "nightVision", + "mechanicalPanTiltZoom", "zoneManagement", "webrtc", + "sounds" -- chime specific capability + } + }, + {"speaker", {"audioMute", "audioVolume"}}, + {"microphone", {"audioMute", "audioVolume"}}, + {"doorbell", {"button"}} -- doorbell specific component and capability + }, + profile = "camera" +} + +local mock_device_doorbell_chime_camera = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { CAMERA_EP, DOORBELL_EP, CHIME_EP } +}) + +local function test_init_doorbell_chime_camera() + test.mock_device.add_test_device(mock_device_doorbell_chime_camera) + test.socket.device_lifecycle:__queue_receive({ mock_device_doorbell_chime_camera.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_doorbell_chime_camera.id, "init" }) + test.socket.matter:__expect_send({ mock_device_doorbell_chime_camera.id, create_subscription(mock_device_doorbell_chime_camera) }) + test.socket.device_lifecycle:__queue_receive({ mock_device_doorbell_chime_camera.id, "doConfigure" }) + mock_device_doorbell_chime_camera:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + + +test.register_coroutine_test( + "Button events should generate appropriate events", + function() + test.socket.matter:__queue_receive({ + mock_device_doorbell_chime_camera.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_doorbell_chime_camera, CAMERA_EP_ID, {uint32(0)}) + }) + mock_device_doorbell_chime_camera:expect_metadata_update(expected_metadata_with_doorbell_chime) + test.socket.matter:__expect_send({mock_device_doorbell_chime_camera.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_doorbell_chime_camera, DOORBELL_EP_ID)}) + test.wait_for_events() + mock_initial_camera_update(mock_device_doorbell_chime_camera, expected_metadata_with_doorbell_chime.optional_component_capabilities, + create_subscription(mock_device_doorbell_chime_camera, true, false, false, true, true), true + ) + + test.socket.matter:__queue_receive({ + mock_device_doorbell_chime_camera.id, + clusters.Switch.server.events.InitialPress:build_test_event_report(mock_device_doorbell_chime_camera, DOORBELL_EP_ID, {new_position = 1}) + }) + test.socket.matter:__queue_receive({ + mock_device_doorbell_chime_camera.id, + clusters.Switch.server.events.MultiPressComplete:build_test_event_report(mock_device_doorbell_chime_camera, DOORBELL_EP_ID, { + new_position = 1, + total_number_of_presses_counted = 2, + previous_position = 0 + }) + }) + test.socket.capability:__expect_send( + mock_device_doorbell_chime_camera:generate_test_message("doorbell", capabilities.button.button.double({state_change = true})) + ) + end, + { + test_init = test_init_doorbell_chime_camera + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Sound commands should send the appropriate commands", + function() + test.socket.matter:__queue_receive({ + mock_device_doorbell_chime_camera.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_doorbell_chime_camera, CAMERA_EP_ID, {uint32(0)}) + }) + mock_device_doorbell_chime_camera:expect_metadata_update(expected_metadata_with_doorbell_chime) + test.socket.matter:__expect_send({mock_device_doorbell_chime_camera.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_doorbell_chime_camera, DOORBELL_EP_ID)}) + test.wait_for_events() + mock_initial_camera_update(mock_device_doorbell_chime_camera, expected_metadata_with_doorbell_chime.optional_component_capabilities, + create_subscription(mock_device_doorbell_chime_camera, true, false, false, true, true), true + ) + + test.socket.capability:__queue_receive({ + mock_device_doorbell_chime_camera.id, + { capability = "sounds", component = "main", command = "setSelectedSound", args = { 1 } }, + }) + test.socket.matter:__expect_send({ + mock_device_doorbell_chime_camera.id, clusters.Chime.attributes.SelectedChime:write(mock_device_doorbell_chime_camera, CAMERA_EP_ID, 1) + }) + test.socket.capability:__queue_receive({ + mock_device_doorbell_chime_camera.id, + { capability = "sounds", component = "main", command = "playSound", args = {} }, + }) + test.socket.matter:__expect_send({ + mock_device_doorbell_chime_camera.id, clusters.Chime.server.commands.PlayChimeSound(mock_device_doorbell_chime_camera, CAMERA_EP_ID) + }) + end, + { + test_init = test_init_doorbell_chime_camera + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Chime reports should generate appropriate events", + function() + test.socket.matter:__queue_receive({ + mock_device_doorbell_chime_camera.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_doorbell_chime_camera, CAMERA_EP_ID, {uint32(0)}) + }) + mock_device_doorbell_chime_camera:expect_metadata_update(expected_metadata_with_doorbell_chime) + test.socket.matter:__expect_send({mock_device_doorbell_chime_camera.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_doorbell_chime_camera, DOORBELL_EP_ID)}) + test.wait_for_events() + mock_initial_camera_update(mock_device_doorbell_chime_camera, expected_metadata_with_doorbell_chime.optional_component_capabilities, + create_subscription(mock_device_doorbell_chime_camera, true, false, false, true, true), true + ) + + test.socket.matter:__queue_receive({ + mock_device_doorbell_chime_camera.id, + clusters.Chime.attributes.InstalledChimeSounds:build_test_report_data(mock_device_doorbell_chime_camera, CAMERA_EP_ID, { + clusters.Chime.types.ChimeSoundStruct({chime_id = 1, name = "Sound 1"}), + clusters.Chime.types.ChimeSoundStruct({chime_id = 2, name = "Sound 2"}) + }) + }) + test.socket.capability:__expect_send( + mock_device_doorbell_chime_camera:generate_test_message("main", capabilities.sounds.supportedSounds({ + {id = 1, label = "Sound 1"}, + {id = 2, label = "Sound 2"}, + }, {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive({ + mock_device_doorbell_chime_camera.id, + clusters.Chime.attributes.SelectedChime:build_test_report_data(mock_device_doorbell_chime_camera, CAMERA_EP_ID, 2) + }) + test.socket.capability:__expect_send(mock_device_doorbell_chime_camera:generate_test_message("main", capabilities.sounds.selectedSound(2))) + end, + { + test_init = test_init_doorbell_chime_camera + }, + { + min_api_version = 17 + } +) + + +local mock_device_floodlight_camera = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { CAMERA_EP, FLOODLIGHT_EP } +}) + +local floodlight_child_device_data = { + profile = t_utils.get_profile_definition("light-level.yml"), + device_network_id = string.format("%s:%d", mock_device_floodlight_camera.id, FLOODLIGHT_EP_ID), + parent_device_id = mock_device_floodlight_camera.id, + parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP_ID) +} +local mock_floodlight_child = test.mock_device.build_test_child_device(floodlight_child_device_data) + +local function test_init_floodlight_camera() + test.mock_device.add_test_device(mock_device_floodlight_camera) + test.socket.device_lifecycle:__queue_receive({ mock_device_floodlight_camera.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_floodlight_camera.id, "init" }) + test.socket.matter:__expect_send({ mock_device_floodlight_camera.id, create_subscription(mock_device_floodlight_camera) }) + test.socket.matter:__expect_send({ mock_device_floodlight_camera.id, clusters.LevelControl.attributes.Options:write(mock_device_floodlight_camera, FLOODLIGHT_EP_ID, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) }) + test.socket.device_lifecycle:__queue_receive({ mock_device_floodlight_camera.id, "doConfigure" }) + mock_device_floodlight_camera:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + + +test.register_coroutine_test( + "Child Floodlight device should be created when OnOff cluster is present on a separate endpoint", + function() + test.mock_device.add_test_device(mock_floodlight_child) + test.socket.matter:__queue_receive({ + mock_device_floodlight_camera.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_floodlight_camera, CAMERA_EP_ID, {uint32(0)}) + }) + mock_device_floodlight_camera:expect_device_create({ + type = "EDGE_CHILD", + label = "Floodlight 1", + profile = "light-level", + parent_device_id = mock_device_floodlight_camera.id, + parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP_ID) + }) + mock_device_floodlight_camera:expect_metadata_update(expected_metadata) + test.wait_for_events() + mock_initial_camera_update(mock_device_floodlight_camera, expected_metadata.optional_component_capabilities, create_subscription(mock_device_floodlight_camera, true, false, true)) + end, + { + test_init = test_init_floodlight_camera + }, + { + min_api_version = 17 + } +) + + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua index 6a8ab5657a..3f7402471d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua @@ -131,7 +131,6 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -- since all fan capabilities are optional, nothing is initially subscribed to test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index 2225ed129b..4333721ebb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -221,8 +221,6 @@ local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) -- make sure the cache is populated - -- added sets a bunch of fields on the device, and calls init - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) -- init results in subscription interaction @@ -254,7 +252,6 @@ local function test_init_battery() test.disable_startup_messages() test.mock_device.add_test_device(mock_device_battery) - test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua index 68fc7e8339..f271bd906c 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua @@ -155,7 +155,6 @@ local function test_init() for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) -- init results in subscription interaction diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index 0c4b314791..b551dd7b21 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -216,7 +216,6 @@ local function test_init() for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_CHILD) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -412,7 +411,6 @@ test.register_coroutine_test( subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) - test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua index 3b3cefd8e3..929937db14 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua @@ -42,17 +42,10 @@ local function test_init() test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete, - clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, - clusters.PowerSource.attributes.BatPercentRemaining } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) @@ -63,7 +56,6 @@ local function test_init() end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index a1b60be357..3c9f0b9713 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -154,7 +154,6 @@ local function test_init() end end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -- note that since disable_startup_messages is not explicitly called here, -- the following subscribe is due to the init event sent by the test framework. @@ -172,7 +171,6 @@ local function test_init_x_y_color_mode() end end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) @@ -187,7 +185,6 @@ local function test_init_no_hue_sat() end end test.socket.device_lifecycle:__queue_receive({ mock_device_no_hue_sat.id, "added" }) - test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) test.mock_device.add_test_device(mock_device_no_hue_sat) @@ -215,7 +212,6 @@ local function test_init_color_temp() end test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "added" }) - test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "init" }) test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) @@ -241,7 +237,6 @@ local function test_init_extended_color() subscribe_request:merge(cluster:subscribe(mock_device_extended_color)) end end - test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "init" }) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 37b5ec8ca5..d5e6bfadcb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -513,7 +513,6 @@ local function test_init_switch_vendor_override() test.mock_device.add_test_device(mock_device_switch_vendor_override) local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_switch_vendor_override) test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "added" }) - test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "init" }) test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "doConfigure" }) @@ -533,7 +532,6 @@ local function test_init_mounted_on_off_control() end end test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "added" }) - test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "init" }) test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) @@ -562,7 +560,6 @@ local function test_init_mounted_dimmable_load_control() end end test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "added" }) - test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "init" }) test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) @@ -592,7 +589,6 @@ local function test_init_parent_child_different_types() } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_different_types) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "added" }) - test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) @@ -656,7 +652,6 @@ local function test_init_light_level_motion() end test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "added" }) - test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "init" }) test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua index 338acd58c7..b7420675ab 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua @@ -58,7 +58,6 @@ local function test_init() end end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua index 7976b27a4a..5dc9945ddc 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua @@ -146,7 +146,6 @@ local function test_init_mock_3switch() test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch) test.socket.device_lifecycle:__queue_receive({ mock_3switch.id, "added" }) - test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) @@ -162,7 +161,6 @@ local function test_init_mock_2switch() test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_2switch) test.socket.device_lifecycle:__queue_receive({ mock_2switch.id, "added" }) - test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) test.mock_device.add_test_device(mock_2switch) @@ -177,7 +175,6 @@ local function test_init_mock_3switch_non_sequential() test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch_non_sequential) test.socket.device_lifecycle:__queue_receive({ mock_3switch_non_sequential.id, "added" }) - test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) test.mock_device.add_test_device(mock_3switch_non_sequential) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 8102c61865..e6313964a2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -177,7 +177,6 @@ local function test_init() end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -255,7 +254,6 @@ local function test_init_parent_child_endpoints_non_sequential() end test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) - test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 9beed1805e..1c7a86d410 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -173,7 +173,6 @@ local function test_init() end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -249,7 +248,6 @@ local function test_init_parent_child_endpoints_non_sequential() end test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request})