diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 20a57fc8f0..5f53181bae 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -4046,11 +4046,6 @@ matterGeneric: - id: 0x0101 # Dimmable Light - id: 0x0107 # Occupancy Sensor deviceProfileName: light-level-motion - - id: "matter/camera" - deviceLabel: Matter Camera - deviceTypes: - - id: 0x0142 # Camera - deviceProfileName: camera - id: "matter/on-off/fan/light" deviceLabel: Matter OnOff Fan Light deviceTypes: @@ -4099,6 +4094,50 @@ matterGeneric: - id: 0x002B # Fan - id: 0x0110 # Mounted Dimmable Load Control deviceProfileName: fan-modular + - id: "matter/camera" + deviceLabel: Matter Camera + deviceTypes: + - id: 0x0142 # Camera + deviceProfileName: camera + - id: "matter/intercom" + deviceLabel: Matter Intercom + deviceTypes: + - id: 0x0140 # Intercom + deviceProfileName: camera + - id: "matter/audio/doorbell" + deviceLabel: Matter Audio DoorBell + deviceTypes: + - id: 0x0141 # Audio DoorBell + - id: 0x0142 # Camera + - id: 0x0148 # DoorBell + deviceProfileName: av-doorbell + - id: "matter/video/doorbell" + deviceLabel: Matter Video DoorBell + deviceTypes: + - id: 0x0143 # Video DoorBell + - id: 0x0142 # Camera + - id: 0x0148 # DoorBell + deviceProfileName: av-doorbell + - id: "matter/floodlight/camera" + deviceLabel: Matter Floodlight Camera + deviceTypes: + - id: 0x0144 # Floodlight Camera + deviceProfileName: camera + - id: "matter/snapshot/camera" + deviceLabel: Matter Snapshot Camera + deviceTypes: + - id: 0x0145 # Snapshot Camera + deviceProfileName: camera + - id: "matter/chime" + deviceLabel: Matter Chime + deviceTypes: + - id: 0x0146 # Chime + deviceProfileName: chime + - id: "matter/doorbell" + deviceLabel: Matter DoorBell + deviceTypes: + - id: 0x0148 # DoorBell + deviceProfileName: doorbell matterThing: - id: SmartThings/MatterThing diff --git a/drivers/SmartThings/matter-switch/profiles/av-doorbell.yml b/drivers/SmartThings/matter-switch/profiles/av-doorbell.yml new file mode 100644 index 0000000000..31be650580 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/av-doorbell.yml @@ -0,0 +1,159 @@ +name: av-doorbell +components: + - id: main + capabilities: + - id: webrtc + version: 1 + optional: true + - id: videoCapture2 + version: 1 + optional: true + - id: videoStreamSettings + version: 1 + optional: true + - id: imageCapture + version: 1 + optional: true + - id: mechanicalPanTiltZoom + version: 1 + optional: true + - id: hdr + version: 1 + optional: true + - id: nightVision + version: 1 + optional: true + - id: imageControl + version: 1 + optional: true + - id: audioRecording + version: 1 + optional: true + - id: sounds + version: 1 + optional: true + - id: cameraPrivacyMode + version: 1 + optional: true + - id: zoneManagement + version: 1 + optional: true + - id: localMediaStorage + version: 1 + optional: true + - id: cameraViewportSettings + version: 1 + optional: true + - id: motionSensor + version: 1 + optional: true + - id: vision.clipAnalysis + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: DoorBell + - id: statusLed + optional: true + capabilities: + - id: switch + version: 1 + optional: true + - id: mode + version: 1 + optional: true + - id: speaker + optional: true + capabilities: + - id: audioMute + version: 1 + optional: true + - id: audioVolume + version: 1 + optional: true + - id: microphone + optional: true + capabilities: + - id: audioMute + version: 1 + optional: true + - id: audioVolume + version: 1 + optional: true + - id: doorbell + optional: true + capabilities: + - id: button + version: 1 + optional: true +deviceConfig: + dashboard: + states: + - component: main + capability: imageCapture + version: 1 + values: + - label: "{{___PO_CODE_SAMSUNGELECTRONICS.IM_DEFAULT_IMAGE_CAPTURE}}" + visibleCondition: + component: main + capability: imageCapture + version: 1 + value: captureTime.value + valueType: string + operator: CONTAINS + operand: T + isOffline: false + basicPlus: + - displayType: camera + camera: + image: + component: main + capability: imageCapture + version: 1 + value: image.value + overlayIcons: + - iconUrl: "res://ic_camera_motion_detected" + visibleCondition: + component: main + capability: motionSensor + version: 1 + value: motion.value + valueType: string + operator: EQUALS + operand: "active" + detailView: + - component: main + capability: webrtc + version: 1 + - component: main + capability: mechanicalPanTiltZoom + version: 1 + - component: main + capability: motionSensor + version: 1 + automation: + conditions: + - component: main + capability: motionSensor + version: 1 + - component: doorbell + capability: button + version: 1 + actions: + - component: main + capability: videoCapture2 + version: 1 + - component: main + capability: imageCapture + version: 1 + dpInfo: + - os: ios + dpUri: "storyboard://HMVSController/HMVSViewController" + - os: android + dpUri: "plugin://com.samsung.android.plugin.camera" +metadata: + mnmn: SmartThingsEdge + vid: matter-av-doorbell diff --git a/drivers/SmartThings/matter-switch/profiles/chime.yml b/drivers/SmartThings/matter-switch/profiles/chime.yml new file mode 100644 index 0000000000..b5e6fc876b --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/chime.yml @@ -0,0 +1,14 @@ +name: chime +components: + - id: main + capabilities: + - id: sounds + version: 1 + - id: audioMute + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: DoorBell diff --git a/drivers/SmartThings/matter-switch/profiles/doorbell.yml b/drivers/SmartThings/matter-switch/profiles/doorbell.yml new file mode 100644 index 0000000000..18ade115a2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/doorbell.yml @@ -0,0 +1,12 @@ +name: doorbell +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: DoorBell diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua index f709d25420..e330a29a77 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 @@ -48,106 +48,122 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr local speaker_component_capabilities = {} local microphone_component_capabilities = {} local doorbell_component_capabilities = {} + local profile = "camera" - local function has_server_cluster_type(cluster) - return cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH" - end + local av_stream_endpoints = device:get_endpoints(clusters.CameraAvStreamManagement.ID, {cluster_type = "SERVER"}) + local av_settings_endpoints = device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID, {cluster_type = "SERVER"}) + local chime_endpoints = device:get_endpoints(clusters.Chime.ID, {cluster_type = "SERVER"}) + local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) - 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]) - 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) - return clusters.CameraAvStreamManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.VIDEO) then - if switch_utils.find_cluster_on_ep(camera_ep, clusters.PushAvStreamTransport.ID, "SERVER") then - table.insert(main_component_capabilities, capabilities.videoCapture2.ID) - end - table.insert(main_component_capabilities, capabilities.cameraViewportSettings.ID) - table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE) then - table.insert(main_component_capabilities, capabilities.localMediaStorage.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.AUDIO) then - if switch_utils.find_cluster_on_ep(camera_ep, clusters.PushAvStreamTransport.ID, "SERVER") then - table.insert(main_component_capabilities, capabilities.audioRecording.ID) - end - table.insert(microphone_component_capabilities, capabilities.audioMute.ID) - table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then - table.insert(main_component_capabilities, capabilities.imageCapture.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.PRIVACY) then - table.insert(main_component_capabilities, capabilities.cameraPrivacyMode.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then - table.insert(speaker_component_capabilities, capabilities.audioMute.ID) - table.insert(speaker_component_capabilities, capabilities.audioVolume.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL) then - table.insert(main_component_capabilities, capabilities.imageControl.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE) then - table.insert(main_component_capabilities, capabilities.hdr.ID) - end - if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION) then - table.insert(main_component_capabilities, capabilities.nightVision.ID) - end - elseif ep_cluster.cluster_id == clusters.CameraAvSettingsUserLevelManagement.ID and has_server_cluster_type(ep_cluster) then - local clus_has_feature = function(feature_bitmap) - return clusters.CameraAvSettingsUserLevelManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) - end - if clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN) or - clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT) or - clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then - table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) - end - elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID and has_server_cluster_type(ep_cluster) then - table.insert(main_component_capabilities, capabilities.zoneManagement.ID) - elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID and has_server_cluster_type(ep_cluster) then - table.insert(main_component_capabilities, capabilities.motionSensor.ID) - elseif ep_cluster.cluster_id == clusters.WebRTCTransportProvider.ID and has_server_cluster_type(ep_cluster) then - table.insert(main_component_capabilities, capabilities.webrtc.ID) + if #av_stream_endpoints > 0 then + local av_stream_ep = switch_utils.get_endpoint_info(device, av_stream_endpoints[1]) + local av_stream_cluster = switch_utils.find_cluster_on_ep(av_stream_ep, clusters.CameraAvStreamManagement.ID, "SERVER") + local clus_has_feature = function(feature_bitmap) + return clusters.CameraAvStreamManagement.are_features_supported(feature_bitmap, av_stream_cluster.feature_map) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + if #device:get_endpoints(clusters.PushAvStreamTransport.ID, {cluster_type = "SERVER"}) > 0 then + table.insert(main_component_capabilities, capabilities.videoCapture2.ID) end + table.insert(main_component_capabilities, capabilities.cameraViewportSettings.ID) + table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) end - end - local chime_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CHIME) - if #chime_endpoints > 0 then - table.insert(main_component_capabilities, capabilities.sounds.ID) - end - local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) - if #doorbell_endpoints > 0 then - table.insert(doorbell_component_capabilities, capabilities.button.ID) - end - if status_light_enabled_present then - table.insert(status_led_component_capabilities, capabilities.switch.ID) - end - if status_light_brightness_present then - table.insert(status_led_component_capabilities, capabilities.mode.ID) + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE) then + table.insert(main_component_capabilities, capabilities.localMediaStorage.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.AUDIO) then + if #device:get_endpoints(clusters.PushAvStreamTransport.ID, {cluster_type = "SERVER"}) > 0 then + table.insert(main_component_capabilities, capabilities.audioRecording.ID) + end + table.insert(microphone_component_capabilities, capabilities.audioMute.ID) + table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then + table.insert(main_component_capabilities, capabilities.imageCapture.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.PRIVACY) then + table.insert(main_component_capabilities, capabilities.cameraPrivacyMode.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then + table.insert(speaker_component_capabilities, capabilities.audioMute.ID) + table.insert(speaker_component_capabilities, capabilities.audioVolume.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL) then + table.insert(main_component_capabilities, capabilities.imageControl.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE) then + table.insert(main_component_capabilities, capabilities.hdr.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION) then + table.insert(main_component_capabilities, capabilities.nightVision.ID) + end + if #chime_endpoints > 0 then + table.insert(main_component_capabilities, capabilities.sounds.ID) + end + if #doorbell_endpoints > 0 then + table.insert(doorbell_component_capabilities, capabilities.button.ID) + CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1], camera_fields.profile_components.doorbell) + end + if status_light_enabled_present then + table.insert(status_led_component_capabilities, capabilities.switch.ID) + end + if status_light_brightness_present then + table.insert(status_led_component_capabilities, capabilities.mode.ID) + end + + if #status_led_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.statusLed, status_led_component_capabilities}) + end + if #speaker_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.speaker, speaker_component_capabilities}) + end + if #microphone_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.microphone, microphone_component_capabilities}) + end + if #doorbell_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.doorbell, doorbell_component_capabilities}) + end + elseif #chime_endpoints > 0 then + profile = "chime" + elseif #doorbell_endpoints > 0 then + CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1], camera_fields.profile_components.main) + profile = "doorbell" end - table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.main, main_component_capabilities}) - if #status_led_component_capabilities > 0 then - table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.statusLed, status_led_component_capabilities}) + if #av_settings_endpoints > 0 then + local av_settings_ep = switch_utils.get_endpoint_info(device, av_settings_endpoints[1]) + local av_settings_cluster = switch_utils.find_cluster_on_ep(av_settings_ep, clusters.CameraAvSettingsUserLevelManagement.ID, "SERVER") + local clus_has_feature = function(feature_bitmap) + return clusters.CameraAvSettingsUserLevelManagement.are_features_supported(feature_bitmap, av_settings_cluster.feature_map) + end + if clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN) or + clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT) or + clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then + table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) + end end - if #speaker_component_capabilities > 0 then - table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.speaker, speaker_component_capabilities}) + + if #device:get_endpoints(clusters.ZoneManagement.ID, {cluster_type = "SERVER"}) > 0 then + table.insert(main_component_capabilities, capabilities.zoneManagement.ID) end - if #microphone_component_capabilities > 0 then - table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.microphone, microphone_component_capabilities}) + + if #device:get_endpoints(clusters.OccupancySensing.ID, {cluster_type = "SERVER"}) > 0 then + table.insert(main_component_capabilities, capabilities.motionSensor.ID) end - if #doorbell_component_capabilities > 0 then - table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.doorbell, doorbell_component_capabilities}) + + if #device:get_endpoints(clusters.WebRTCTransportProvider.ID, {cluster_type = "SERVER"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) end + table.insert(optional_supported_component_capabilities, 1, {camera_fields.profile_components.main, main_component_capabilities}) + if camera_utils.optional_capabilities_list_changed(optional_supported_component_capabilities, device.profile.components) then - device:try_update_metadata({profile = "camera", optional_component_capabilities = optional_supported_component_capabilities}) + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA_VIDEO_DOORBELL) > 0 or + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA_AUDIO_DOORBELL) > 0 then + profile = "av-doorbell" + end + device:try_update_metadata({profile = profile, optional_component_capabilities = optional_supported_component_capabilities}) if #doorbell_endpoints > 0 then - CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1]) button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) end end @@ -275,9 +291,13 @@ function CameraDeviceConfiguration.initialize_camera_capabilities(device) init_camera_privacy_mode(device) end -function CameraDeviceConfiguration.update_doorbell_component_map(device, ep) +function CameraDeviceConfiguration.update_doorbell_component_map(device, ep, component) local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} - component_map.doorbell = ep + if component == camera_fields.profile_components.main then + component_map.main = ep + elseif component == camera_fields.profile_components.doorbell then + component_map.doorbell = ep + end device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index 12341f493e..f6ace2890a 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 @@ -199,6 +199,8 @@ function CameraUtils.optional_capabilities_list_changed(new_component_capability end function CameraUtils.subscribe(device) + local im = require "st.matter.interaction_model" + local camera_subscribed_attributes = { [capabilities.hdr.ID] = { clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, @@ -232,7 +234,8 @@ function CameraUtils.subscribe(device) }, [capabilities.audioMute.ID] = { clusters.CameraAvStreamManagement.attributes.SpeakerMuted, - clusters.CameraAvStreamManagement.attributes.MicrophoneMuted + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted, + clusters.Chime.attributes.Enabled }, [capabilities.audioVolume.ID] = { clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, @@ -311,8 +314,6 @@ function CameraUtils.subscribe(device) } } - 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 = {}, {}, {}, {} @@ -324,7 +325,7 @@ function CameraUtils.subscribe(device) 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, + switch_utils.populate_subscribe_request_for_device(checked_device, 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 diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua index 25a441d641..339fa2fe9a 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua @@ -2,13 +2,16 @@ -- Licensed under the Apache License, Version 2.0 return function(opts, driver, device, ...) + local clusters = require "st.matter.clusters" local device_lib = require "st.device" local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" if device.network_type == device_lib.NETWORK_TYPE_MATTER then local version = require "version" if version.rpc >= 10 and version.api >= 16 and - #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) > 0 then + #device:get_endpoints(clusters.CameraAvStreamManagement.ID, {cluster_type = "SERVER"}) > 0 or + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CHIME) > 0 or + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then return true, require("sub_drivers.camera") end end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 2b315c6528..8bec90e8ae 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -26,9 +26,14 @@ SwitchFields.DEVICE_TYPE_ID = { AGGREGATOR = 0x000E, BRIDGED_NODE = 0x0013, CAMERA = 0x0142, + CAMERA_AUDIO_DOORBELL = 0x0141, + CAMERA_FLOODLIGHT = 0x0144, + CAMERA_INTERCOM = 0x0140, + CAMERA_SNAPSHOT = 0x0145, + CAMERA_VIDEO_DOORBELL = 0x0143, CHIME = 0x0146, DIMMABLE_PLUG_IN_UNIT = 0x010B, - DOORBELL = 0x0143, + DOORBELL = 0x0148, ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 9fd4c77c84..8f368302af 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -296,18 +296,29 @@ function utils.matter_handler(driver, device, response_block) device.log.info(string.format("Fallback handler for %s", response_block)) end --- get a list of endpoints for a specified device type. +--- Get a list of endpoints for a specified device type or list of device types. +--- +--- @param device table a Matter device object +--- @param device_type_id number|table a single device type ID or a list of device type IDs to match against +--- @param opts table|nil optional parameters: +--- - `with_info` boolean: if true, returns full endpoint info tables instead of just endpoint IDs +--- @return table a list of endpoint IDs by default, or a list of endpoint info tables if `opts.with_info` is true function utils.get_endpoints_by_device_type(device, device_type_id, opts) + if device_type_id == nil then return end + device_type_id = type(device_type_id) == "table" and device_type_id or { device_type_id } opts = opts or {} local dt_eps = {} + local function insert_ep(ep) + if opts.with_info then + table.insert(dt_eps, ep) + else + table.insert(dt_eps, ep.endpoint_id) + end + end for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == device_type_id then - if opts.with_info then - table.insert(dt_eps, ep) - else - table.insert(dt_eps, ep.endpoint_id) - end + if utils.tbl_contains(device_type_id, dt.device_type_id) then + insert_ep(ep) break end end @@ -447,34 +458,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 +function utils.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 @@ -493,7 +513,7 @@ 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, + utils.populate_subscribe_request_for_device(checked_device, device, subscribe_request, capabilities_seen, attributes_seen, events_seen, device.driver.subscribed_attributes, device.driver.subscribed_events ) devices_seen[checked_device.id] = true -- only loop through any device once 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 2eee0b778e..49a76377b4 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -111,7 +111,186 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, device_types = { - {device_type_id = 0x0143, device_type_revision = 1} -- Doorbell + {device_type_id = 0x0148, device_type_revision = 1} -- DoorBell + } + } + } +}) + +local mock_device_chime = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("chime.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 + } + }, + { + endpoint_id = CHIME_EP, + clusters = { + { + cluster_id = clusters.Chime.ID, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x0146, device_type_revision = 1} -- Chime + } + } + } +}) + +local mock_device_doorbell = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("doorbell.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 + } + }, + { + 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", + } + }, + device_types = { + {device_type_id = 0x0148, device_type_revision = 1} -- DoorBell + } + } + } +}) + +local VD_VIDEO_DOORBELL_EP, VD_CAMERA_EP, VD_FLOODLIGHT_EP, VD_CHIME_EP, VD_DOORBELL_EP = 1, 2, 3, 4, 5 + +local mock_device_video_doorbell = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("av-doorbell.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 + } + }, + { + endpoint_id = VD_VIDEO_DOORBELL_EP, + clusters = {}, + device_types = { + { device_type_id = 0x0143, device_type_revision = 1 } -- Video Doorbell + } + }, + { + endpoint_id = VD_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.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.WebRTCTransportRequestor.ID, + cluster_type = "CLIENT" + }, + { + cluster_id = clusters.OccupancySensing.ID, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + }, + { + endpoint_id = VD_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} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = VD_CHIME_EP, + clusters = { + { + cluster_id = clusters.Chime.ID, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x0146, device_type_revision = 1} -- Chime + } + }, + { + endpoint_id = VD_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", + } + }, + device_types = { + {device_type_id = 0x0148, device_type_revision = 1} -- DoorBell } } } @@ -164,6 +343,100 @@ end test.set_test_init_function(test_init) +local function test_init_chime() + test.mock_device.add_test_device(mock_device_chime) + test.socket.device_lifecycle:__queue_receive({ mock_device_chime.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_chime.id, "init" }) + local subscribed_attributes_chime = { + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime, + clusters.Chime.attributes.Enabled, + } + subscribe_request = subscribed_attributes_chime[1]:subscribe(mock_device_chime) + for i, attr in ipairs(subscribed_attributes_chime) do + if i > 1 then subscribe_request:merge(attr:subscribe(mock_device_chime)) end + end + test.socket.matter:__expect_send({mock_device_chime.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_chime.id, "doConfigure" }) + local expected_metadata = { optional_component_capabilities = { { "main", {} } }, profile = "chime" } + mock_device_chime:expect_metadata_update(expected_metadata) + local updated_device_profile = t_utils.get_profile_definition("chime.yml") + test.socket.device_lifecycle:__queue_receive(mock_device_chime:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({mock_device_chime.id, subscribe_request}) + mock_device_chime:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_doorbell() + test.mock_device.add_test_device(mock_device_doorbell) + test.socket.device_lifecycle:__queue_receive({ mock_device_doorbell.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_doorbell.id, "init" }) + local subscribed_attributes_doorbell = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + subscribe_request = subscribed_attributes_doorbell[1]:subscribe(mock_device_doorbell) + for i, attr in ipairs(subscribed_attributes_doorbell) do + if i > 1 then subscribe_request:merge(attr:subscribe(mock_device_doorbell)) end + end + test.socket.matter:__expect_send({mock_device_doorbell.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_doorbell.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device_doorbell.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_doorbell, DOORBELL_EP)}) + test.socket.capability:__expect_send(mock_device_doorbell:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + local expected_metadata = { optional_component_capabilities = { { "main", {} } }, profile = "doorbell" } + mock_device_doorbell:expect_metadata_update(expected_metadata) + local updated_device_profile = t_utils.get_profile_definition("doorbell.yml") + test.socket.device_lifecycle:__queue_receive(mock_device_doorbell:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({mock_device_doorbell.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device_doorbell.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_doorbell, DOORBELL_EP)}) + test.socket.capability:__expect_send(mock_device_doorbell:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + mock_device_doorbell:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_video_doorbell() + test.mock_device.add_test_device(mock_device_video_doorbell) + test.socket.device_lifecycle:__queue_receive({ mock_device_video_doorbell.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_video_doorbell.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_video_doorbell.id, VD_FLOODLIGHT_EP), + parent_device_id = mock_device_video_doorbell.id, + parent_assigned_child_key = string.format("%d", VD_FLOODLIGHT_EP) + } + test.mock_device.add_test_device(test.mock_device.build_test_child_device(floodlight_child_device_data)) + mock_device_video_doorbell:expect_device_create({ + type = "EDGE_CHILD", + label = "Floodlight 1", + profile = "light-color-level", + parent_device_id = mock_device_video_doorbell.id, + parent_assigned_child_key = string.format("%d", VD_FLOODLIGHT_EP) + }) + local subscribed_attributes_vd = { + 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, + } + subscribe_request = subscribed_attributes_vd[1]:subscribe(mock_device_video_doorbell) + for i, attr in ipairs(subscribed_attributes_vd) do + if i > 1 then subscribe_request:merge(attr:subscribe(mock_device_video_doorbell)) end + end + test.socket.matter:__expect_send({mock_device_video_doorbell.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_video_doorbell.id, "doConfigure" }) + mock_device_video_doorbell:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local additional_subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, clusters.CameraAvStreamManagement.attributes.ImageRotation, @@ -203,6 +476,7 @@ local additional_subscribed_attributes = { clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin, clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams, + clusters.Chime.attributes.Enabled, clusters.Chime.attributes.InstalledChimeSounds, clusters.Chime.attributes.SelectedChime, clusters.ZoneManagement.attributes.MaxZones, @@ -244,11 +518,11 @@ local expected_metadata = { "imageControl", "hdr", "nightVision", + "sounds", "mechanicalPanTiltZoom", "zoneManagement", - "webrtc", "motionSensor", - "sounds", + "webrtc", } }, { @@ -2768,8 +3042,8 @@ test.register_coroutine_test( optional_component_capabilities = { { "main", { "videoCapture2", "cameraViewportSettings", "videoStreamSettings", "localMediaStorage", "audioRecording", - "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "zoneManagement", - "webrtc", "motionSensor", "sounds", } + "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "sounds", "mechanicalPanTiltZoom", + "zoneManagement", "motionSensor", "webrtc" } }, { "statusLed", { "switch" } -- only switch capability remains @@ -2795,5 +3069,137 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Test init for chime device type", + function() + end, + { test_init = test_init_chime } +) + +test.register_coroutine_test( + "Test init for doorbell device type", + function() + end, + { test_init = test_init_doorbell } +) + +test.register_coroutine_test( + "Test init for video doorbell device type", + function() + local expected_metadata_av_doorbell = { + optional_component_capabilities = { + { + "main", + { + "videoCapture2", + "cameraViewportSettings", + "videoStreamSettings", + "localMediaStorage", + "audioRecording", + "cameraPrivacyMode", + "imageControl", + "hdr", + "nightVision", + "sounds", + "mechanicalPanTiltZoom", + "zoneManagement", + "motionSensor", + "webrtc", + } + }, + { + "statusLed", + { + "switch", + "mode" + } + }, + { + "speaker", + { + "audioMute", + "audioVolume" + } + }, + { + "microphone", + { + "audioMute", + "audioVolume" + } + }, + { + "doorbell", + { + "button" + } + } + }, + profile = "av-doorbell" + } + + test.socket.matter:__queue_receive({ + mock_device_video_doorbell.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_video_doorbell, VD_CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device_video_doorbell:expect_metadata_update(expected_metadata_av_doorbell) + test.socket.matter:__expect_send({mock_device_video_doorbell.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_video_doorbell, VD_DOORBELL_EP)}) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition( + "av-doorbell.yml", {enabled_optional_capabilities = expected_metadata_av_doorbell.optional_component_capabilities} + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device_video_doorbell:generate_info_changed({ profile = updated_device_profile })) + test.socket.capability:__expect_send( + mock_device_video_doorbell: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_video_doorbell:generate_test_message("main", capabilities.mechanicalPanTiltZoom.supportedAttributes( + {"pan", "panRange", "tilt", "tiltRange", "zoom", "zoomRange", "presets", "maxPresets"} + )) + ) + test.socket.capability:__expect_send( + mock_device_video_doorbell:generate_test_message("main", capabilities.zoneManagement.supportedFeatures( + {"triggerAugmentation", "perZoneSensitivity"} + )) + ) + test.socket.capability:__expect_send( + mock_device_video_doorbell:generate_test_message("main", capabilities.localMediaStorage.supportedAttributes( + {"localVideoRecording"} + )) + ) + test.socket.capability:__expect_send( + mock_device_video_doorbell:generate_test_message("main", capabilities.audioRecording.audioRecording("enabled")) + ) + test.socket.capability:__expect_send( + mock_device_video_doorbell:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports", "watermark", "onScreenDisplay"} + )) + ) + test.socket.capability:__expect_send( + mock_device_video_doorbell:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes( + {"softRecordingPrivacyMode", "softLivestreamPrivacyMode"} + )) + ) + test.socket.capability:__expect_send( + mock_device_video_doorbell:generate_test_message("main", capabilities.cameraPrivacyMode.supportedCommands( + {"setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode"} + )) + ) + for _, attr in ipairs(additional_subscribed_attributes) do + subscribe_request:merge(attr:subscribe(mock_device_video_doorbell)) + end + test.socket.matter:__expect_send({mock_device_video_doorbell.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device_video_doorbell.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_video_doorbell, VD_DOORBELL_EP)}) + test.socket.capability:__expect_send(mock_device_video_doorbell:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) + end, + { test_init = test_init_video_doorbell } +) + -- run the tests test.run_registered_tests() 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 642610e88a..0407f1d622 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 @@ -9,124 +9,176 @@ local dkjson = require "dkjson" local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), - matter_version = {hardware = 1, software = 1}, - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, + profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), + matter_version = { hardware = 1, software = 1 }, + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 }, -- RootNode + } }, - endpoints = { - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0302, device_type_revision = 1}, - } - }, - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "BOTH"}, - }, - device_types = { - {device_type_id = 0x0307, device_type_revision = 1}, - } - }, - } + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0302, device_type_revision = 1 }, -- Temperature Sensor + } + }, + { + endpoint_id = 2, + clusters = { + { cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "BOTH" }, + }, + device_types = { + { device_type_id = 0x0307, device_type_revision = 1 }, -- Humidity Sensor + } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.Switch.ID, cluster_type = "SERVER", cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH }, + }, + device_types = { + { device_type_id = 0x000F, device_type_revision = 1 }, -- Generic Switch + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.Switch.ID, cluster_type = "SERVER", cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH }, + }, + device_types = { + { device_type_id = 0x000F, device_type_revision = 1 }, -- Generic Switch + } + }, + { + endpoint_id = 5, + clusters = { + { cluster_id = clusters.Switch.ID, cluster_type = "SERVER", cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH }, + }, + device_types = { + { device_type_id = 0x000F, device_type_revision = 1 }, -- Generic Switch + } + }, + { + endpoint_id = 6, + clusters = { + { cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", + feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY }, + }, + device_types = { + { device_type_id = 0x0011, device_type_revision = 1 }, -- Power Source + } + }, + } }) local function test_init() - test.disable_startup_messages() - 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) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end + test.disable_startup_messages() + 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, - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + clusters.PowerSource.attributes.AttributeList, + clusters.PowerSource.attributes.BatPercentRemaining + } - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + 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 }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "3-button-battery-temperature-humidity" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json}) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "3-button-battery-temperature-humidity" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) end -test.register_coroutine_test("Read appropriate attribute values after tempOffset preference change", function() - local report = clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,1, 2000) - mock_device.st_store.preferences = {tempOffset = "0"} +test.register_coroutine_test("Read appropriate attribute values after tempOffset preference change", + function() + local report = clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 2000) + mock_device.st_store.preferences = { tempOffset = "0" } - test.socket.matter:__queue_receive({mock_device.id, report}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ - value = 20.0, - unit = "C" + test.socket.matter:__queue_receive({ mock_device.id, report }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ + value = 20.0, + unit = "C" }))) - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {tempOffset = "5"}})) - test.socket.matter:__expect_send({mock_device.id, clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device)}) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = { tempOffset = "5" } })) + test.socket.matter:__expect_send({ mock_device.id, clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) test.wait_for_events() test.socket.matter:__queue_receive({mock_device.id, report}) test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ - value = 20.0, - unit = "C" + value = 20.0, + unit = "C" }))) -end, -{ - min_api_version = 19 -} + end, + { + min_api_version = 19 + } ) -test.register_coroutine_test("Read appropriate attribute values after humidityOffset preference change", function() - local report = clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,2, 2000) - mock_device.st_store.preferences = {humidityOffset = "0"} +test.register_coroutine_test("Read appropriate attribute values after humidityOffset preference change", + function() + local report = clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device, 2, 2000) + mock_device.st_store.preferences = { humidityOffset = "0" } - test.socket.matter:__queue_receive({mock_device.id, report}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ - value = 20 + test.socket.matter:__queue_receive({ mock_device.id, report }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ + value = 20 }))) - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {humidityOffset = "5"}})) - test.socket.matter:__expect_send({mock_device.id, clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(mock_device)}) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = { humidityOffset = "5" } })) + test.socket.matter:__expect_send({ mock_device.id, clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(mock_device) }) test.wait_for_events() test.socket.matter:__queue_receive({mock_device.id, report}) test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ - value = 20 + value = 20 }))) -end, -{ - min_api_version = 19 -} + end, + { + min_api_version = 19 + } ) test.set_test_init_function(test_init)