diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index c67eec7bf2..ec4fec228e 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -3073,6 +3073,41 @@ matterGeneric: 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 + deviceProfileName: camera + - id: "matter/video/doorbell" + deviceLabel: Matter Video DoorBell + deviceTypes: + - id: 0x0143 # Video DoorBell + deviceProfileName: camera + - 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/chime.yml b/drivers/SmartThings/matter-switch/profiles/chime.yml new file mode 100644 index 0000000000..ec5de4c07e --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/chime.yml @@ -0,0 +1,10 @@ +name: chime +components: + - id: main + capabilities: + - id: sounds + version: 1 + optional: true + - id: audioMute + version: 1 + optional: true diff --git a/drivers/SmartThings/matter-switch/profiles/doorbell.yml b/drivers/SmartThings/matter-switch/profiles/doorbell.yml new file mode 100644 index 0000000000..db25318575 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/doorbell.yml @@ -0,0 +1,7 @@ +name: doorbell +components: + - id: main + capabilities: + - id: button + version: 1 + optional: true 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 90c5ee3498..fe2e4b3b2a 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,13 +48,28 @@ 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 camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) + local camera_profile_device_types = { + fields.DEVICE_TYPE_ID.CAMERA.INTERCOM, + fields.DEVICE_TYPE_ID.CAMERA.AUDIO_DOORBELL, + fields.DEVICE_TYPE_ID.CAMERA.CAMERA, + fields.DEVICE_TYPE_ID.CAMERA.VIDEO_DOORBELL, + fields.DEVICE_TYPE_ID.CAMERA.FLOODLIGHT_CAMERA, + fields.DEVICE_TYPE_ID.CAMERA.SNAPSHOT_CAMERA, + } + + local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, camera_profile_device_types) + local chime_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA.CHIME) + local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA.DOORBELL) + if #camera_endpoints > 0 then + -- get full list of camera endpoints, including chime and doorbell + camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) 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 @@ -111,40 +126,47 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr table.insert(main_component_capabilities, capabilities.webrtc.ID) end end - end - local chime_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CHIME) - if #chime_endpoints > 0 then + 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) + button_cfg.configure_buttons(device) + end + if #chime_endpoints > 0 then + table.insert(main_component_capabilities, capabilities.sounds.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) + 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 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) - CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1]) + table.insert(main_component_capabilities, capabilities.audioMute.ID) + profile = "chime" + elseif #doorbell_endpoints > 0 then + table.insert(main_component_capabilities, capabilities.button.ID) + CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1], camera_fields.profile_components.main) button_cfg.configure_buttons(device) - 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) + 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}) - 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 + 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}) + device:try_update_metadata({profile = profile, optional_component_capabilities = optional_supported_component_capabilities}) end end @@ -270,9 +292,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/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index 8244f4fd62..c736611a7d 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -42,7 +42,7 @@ end function CameraLifecycleHandlers.info_changed(driver, device, event, args) if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then camera_cfg.initialize_camera_capabilities(device) - if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA.DOORBELL) > 0 then button_cfg.configure_buttons(device) end device:subscribe() diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 3a0bdc9fdb..4a1267b34b 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -37,10 +37,17 @@ SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 SwitchFields.DEVICE_TYPE_ID = { AGGREGATOR = 0x000E, BRIDGED_NODE = 0x0013, - CAMERA = 0x0142, - CHIME = 0x0146, + CAMERA = { + INTERCOM = 0x0140, + AUDIO_DOORBELL = 0x0141, + CAMERA = 0x0142, + VIDEO_DOORBELL = 0x0143, + FLOODLIGHT_CAMERA = 0x0144, + SNAPSHOT_CAMERA = 0x0145, + CHIME = 0x0146, + DOORBELL = 0x0148, + }, DIMMABLE_PLUG_IN_UNIT = 0x010B, - DOORBELL = 0x0143, ELECTRICAL_SENSOR = 0x0510, GENERIC_SWITCH = 0x000F, MOUNTED_ON_OFF_CONTROL = 0x010F, diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index b6cc09e007..07216808a5 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -257,13 +257,19 @@ 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 function utils.get_endpoints_by_device_type(device, device_type_id) local dt_eps = {} 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 type(device_type_id) == "table" then + if utils.tbl_contains(device_type_id, dt.device_type_id) then + table.insert(dt_eps, ep.endpoint_id) + break + end + elseif dt.device_type_id == device_type_id then table.insert(dt_eps, ep.endpoint_id) + break end end end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index b9804a2a76..1548556bb7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -108,7 +108,66 @@ 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}, + 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}, + 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 } } } @@ -148,6 +207,81 @@ 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, + } + 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", + { + "sounds", + "audioMute" + } + } + }, + profile = "chime" + } + mock_device_chime:expect_metadata_update(expected_metadata) + local updated_device_profile = t_utils.get_profile_definition( + "chime.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + 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", + { + "button" + } + } + }, + profile = "doorbell" + } + mock_device_doorbell:expect_metadata_update(expected_metadata) + local updated_device_profile = t_utils.get_profile_definition( + "doorbell.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + 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, 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}))) + test.socket.matter:__expect_send({mock_device_doorbell.id, subscribe_request}) + mock_device_doorbell:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local function update_device_profile() test.socket.matter:__set_channel_ordering("relaxed") local uint32 = require "st.matter.data_types.Uint32" @@ -1810,5 +1944,19 @@ test.register_coroutine_test( end ) +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 } +) + -- run the tests test.run_registered_tests()