From a2397f0f4e59671598c7569910d84438dab6bdcb Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 28 May 2025 16:08:14 -0500 Subject: [PATCH 1/7] add support for re-profiling parent switch devices and for profiling all types of energy plug devices --- .../SmartThings/matter-switch/src/init.lua | 120 +++++++++++++----- .../src/test/test_aqara_light_switch_h2.lua | 1 + .../src/test/test_electrical_sensor.lua | 10 +- .../test/test_matter_switch_device_types.lua | 3 + .../test_multi_switch_parent_child_lights.lua | 2 + .../test_multi_switch_parent_child_plugs.lua | 41 ++++++ 6 files changed, 141 insertions(+), 36 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 8d99e62ccb..b0ea25ea4e 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -480,11 +480,11 @@ local function check_field_name_updates(device) end end -local function assign_child_profile(device, child_ep) +local function assign_switch_profile(device, switch_ep, is_child_device) local profile - + local electrical_tags = "" for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == child_ep then + if ep.endpoint_id == switch_ep then -- Some devices report multiple device types which are a subset of -- a superset device type (For example, Dimmable Light is a superset of -- On/Off light). This mostly applies to the four light types, so we will want @@ -495,28 +495,88 @@ local function assign_child_profile(device, child_ep) id = math.max(id, dt.device_type_id) end profile = device_type_profile_map[id] + if profile == "plug-binary" or profile == "plug-level" then + local power_cluster_found, energy_cluster_found = false, false + for _, cluster in ipairs(ep.clusters) do + if cluster.cluster_id == clusters.ElectricalPowerMeasurement.ID then + power_cluster_found = true + elseif cluster.cluster_id == clusters.ElectricalEnergyMeasurement.ID then + energy_cluster_found = true + end + end + if power_cluster_found then + electrical_tags = electrical_tags .. "-power" + end + if energy_cluster_found then + electrical_tags = electrical_tags .."-energy-powerConsumption" + end + if electrical_tags ~= "" then + profile = string.gsub(profile, "-binary", "") -- remove the "-binary" in the plug-binary profile name + profile = profile .. electrical_tags + end + end break end end - -- Check if device has an overridden child profile that differs from the profile that would match - -- the child's device type for the following two cases: - -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) - -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is - -- determined in the "for" loop above (e.g., light-binary) - -- 2. The selected profile for the child device matches the initial profile defined in - -- child_device_profile_overrides - for id, vendor in pairs(child_device_profile_overrides_per_vendor_id) do - for _, fingerprint in ipairs(vendor) do - if device.manufacturer_info.product_id == fingerprint.product_id and - ((device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile + -- If the device supports the Electrical Sensor Device Type + if electrical_tags == "" then + local electrical_sensor_found = false + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == ELECTRICAL_SENSOR_ID then + electrical_sensor_found = true + end + end + end + if electrical_sensor_found then + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(switch_eps) + local main_switch_ep = switch_eps[1] + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == main_switch_ep and ep.endpoint_id == switch_ep then + -- Some devices report multiple device types which are a subset of + -- a superset device type (For example, Dimmable Light is a superset of + -- On/Off light). This mostly applies to the four light types, so we will want + -- to match the profile for the superset device type. This can be done by + -- matching to the device type with the highest ID + local id = 0 + for _, dt in ipairs(ep.device_types) do + id = math.max(id, dt.device_type_id) + end + profile = device_type_profile_map[id] + if profile == "plug-binary" or profile == "plug-level" or profile == "light-binary" then + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then + electrical_tags = electrical_tags .. "-power" + end + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then + electrical_tags = electrical_tags .."-energy-powerConsumption" + end + if electrical_tags ~= "" then + profile = string.gsub(profile, "-binary", "") -- remove the "-binary" in the plug-binary/light-binary profile name + profile = profile .. electrical_tags + end + end + break + end end end end - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" + if is_child_device then + -- Check if child device has an overridden child profile that differs from the child's generic device type profile + for _, vendor in pairs(child_device_profile_overrides_per_vendor_id) do + for _, fingerprint in ipairs(vendor) do + if device.manufacturer_info.product_id == fingerprint.product_id and profile == fingerprint.initial_profile then + return fingerprint.target_profile + end + end + end + -- default to "switch-binary" if no child profile is found + return profile or "switch-binary" + end + + return profile end local function configure_buttons(device) @@ -598,7 +658,7 @@ local function build_child_switch_profiles(driver, device, main_endpoint) num_switch_server_eps = num_switch_server_eps + 1 if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = assign_child_profile(device, ep) + local child_profile = assign_switch_profile(device, ep, true) driver:try_create_device( { type = "EDGE_CHILD", @@ -728,22 +788,9 @@ local function match_profile(driver, device) end local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local level_eps = device:get_endpoints(clusters.LevelControl.ID) - local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) local profile_name = nil - local level_support = "" - if #level_eps > 0 then - level_support = "-level" - end - if #energy_eps > 0 and #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" - elseif #energy_eps > 0 then - profile_name = "plug" .. level_support .. "-energy-powerConsumption" - elseif #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power" - elseif #valve_eps > 0 then + if #valve_eps > 0 then profile_name = "water-valve" if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then @@ -754,6 +801,15 @@ local function match_profile(driver, device) end if profile_name then device:try_update_metadata({ profile = profile_name }) + return + end + + -- after doing all previous profiling steps, attempt to re-profile main/parent switch/plug device + profile_name = assign_switch_profile(device, main_endpoint, false) + -- ignore attempts to dynamically profile light-level-colorTemperature devices for now, since + -- these may lose fingerprinted Kelvin ranges when dynamically profiled. + if profile_name and profile_name ~= "light-level-colorTemperature" then + device:try_update_metadata({profile = profile_name}) end end 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 369689e181..3247b309a6 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 @@ -188,6 +188,7 @@ local function test_init() for _, child in pairs(aqara_mock_children) do test.mock_device.add_test_device(child) + print("abcd1") end aqara_mock_device:expect_device_create({ diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 6cca6a8cd0..05eaad4df9 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -51,10 +51,10 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoint_id = 2, clusters = { { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, }, device_types = { - { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug } }, }, @@ -80,16 +80,18 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, }, device_types = { - { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug } }, }, }) local subscribed_attributes_periodic = { + clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } @@ -713,7 +715,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 1) } }, { 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 6debff2163..0b185a1190 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 @@ -427,6 +427,7 @@ local function test_init_mounted_on_off_control() end 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, "doConfigure" }) + mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_mounted_on_off_control) end @@ -443,6 +444,7 @@ local function test_init_mounted_dimmable_load_control() end 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, "doConfigure" }) + mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end @@ -477,6 +479,7 @@ local function test_init_parent_child_different_types() 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, "doConfigure" }) + mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_parent_child_different_types) 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 de62865597..c78960501f 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 @@ -181,6 +181,7 @@ local function test_init() 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({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) @@ -247,6 +248,7 @@ local function test_init_parent_child_endpoints_non_sequential() 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, "doConfigure" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) 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 cbf8fb0afe..84a71bf774 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 @@ -23,6 +23,8 @@ local child_profile_override = t_utils.get_profile_definition("switch-binary.yml local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 +local child3_ep = 40 +local child4_ep = 50 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", @@ -68,6 +70,27 @@ local mock_device = test.mock_device.build_test_matter_device({ {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug } }, + { + endpoint_id = child3_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + } + }, + { + endpoint_id = child4_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + } + } } }) @@ -139,6 +162,7 @@ local function test_init() 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({ profile = "plug-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) @@ -161,6 +185,22 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "plug-power-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child3_ep) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 5", + profile = "plug-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child4_ep) + }) end local mock_children_child_profile_override = {} @@ -184,6 +224,7 @@ local function test_init_child_profile_override() test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) + mock_device_child_profile_override:expect_metadata_update({ profile = "plug-binary" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_child_profile_override) From 255334aefeef97559e917fc517d7a9fa46e01c1a Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 28 May 2025 16:22:04 -0500 Subject: [PATCH 2/7] ignore electrical sensor dt --- .../SmartThings/matter-switch/src/init.lua | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b0ea25ea4e..7b4ebdc6d0 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -481,20 +481,27 @@ local function check_field_name_updates(device) end local function assign_switch_profile(device, switch_ep, is_child_device) + local get_profile_by_device_type = function(ep) + -- Some devices report multiple device types which are a subset of + -- a superset device type (For example, Dimmable Light is a superset of + -- On/Off light). This mostly applies to the four light types, so we will want + -- to match the profile for the superset device type. This can be done by + -- matching to the device type with the highest ID + -- Note: Electrical Sensor does not follow the above logic, so it's ignored + local id = 0 + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id ~= ELECTRICAL_SENSOR_ID then + id = math.max(id, dt.device_type_id) + end + end + return device_type_profile_map[id] + end + local profile local electrical_tags = "" for _, ep in ipairs(device.endpoints) do if ep.endpoint_id == switch_ep then - -- Some devices report multiple device types which are a subset of - -- a superset device type (For example, Dimmable Light is a superset of - -- On/Off light). This mostly applies to the four light types, so we will want - -- to match the profile for the superset device type. This can be done by - -- matching to the device type with the highest ID - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - profile = device_type_profile_map[id] + profile = get_profile_by_device_type(ep) if profile == "plug-binary" or profile == "plug-level" then local power_cluster_found, energy_cluster_found = false, false for _, cluster in ipairs(ep.clusters) do @@ -535,16 +542,7 @@ local function assign_switch_profile(device, switch_ep, is_child_device) local main_switch_ep = switch_eps[1] for _, ep in ipairs(device.endpoints) do if ep.endpoint_id == main_switch_ep and ep.endpoint_id == switch_ep then - -- Some devices report multiple device types which are a subset of - -- a superset device type (For example, Dimmable Light is a superset of - -- On/Off light). This mostly applies to the four light types, so we will want - -- to match the profile for the superset device type. This can be done by - -- matching to the device type with the highest ID - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - profile = device_type_profile_map[id] + profile = get_profile_by_device_type(ep) if profile == "plug-binary" or profile == "plug-level" or profile == "light-binary" then if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-power" From f3f40304c473e026dadfc71c0e89b697520a7878 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 28 May 2025 16:24:59 -0500 Subject: [PATCH 3/7] remove print --- .../matter-switch/src/test/test_aqara_light_switch_h2.lua | 1 - 1 file changed, 1 deletion(-) 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 3247b309a6..369689e181 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 @@ -188,7 +188,6 @@ local function test_init() for _, child in pairs(aqara_mock_children) do test.mock_device.add_test_device(child) - print("abcd1") end aqara_mock_device:expect_device_create({ From 3525fe25a208e03f7606cf69f34d440bcf000b9c Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 28 May 2025 21:02:17 -0500 Subject: [PATCH 4/7] update the logic to be cleaner --- .../SmartThings/matter-switch/src/init.lua | 143 +++++++----------- .../test_multi_switch_parent_child_plugs.lua | 8 +- 2 files changed, 61 insertions(+), 90 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 7b4ebdc6d0..4edfaa22f7 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -321,6 +321,22 @@ local function create_multi_press_values_list(size, supportsHeld) return list end +-- check if device type is found on the device. Optionally specify the endpoint to search on +local function contains_device_type(device, device_type_id, endpoint_id) + 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 endpoint_id and ep.endpoint_id == endpoint_id then + return true + elseif endpoint_id == nil then + return true + end + end + end + end + return false +end + local function tbl_contains(array, value) for _, element in ipairs(array) do if element == value then @@ -385,20 +401,14 @@ end --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. local function device_type_supports_button_switch_combination(device, endpoint_id) - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == endpoint_id then - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then - for _, fingerprint in ipairs(child_device_profile_overrides_per_vendor_id[0x115F]) do - if device.manufacturer_info.product_id == fingerprint.product_id then - return false -- For Aqara Dimmer Switch with Button. - end - end - return true - end - end + for _, fingerprint in ipairs(child_device_profile_overrides_per_vendor_id[AQARA_MANUFACTURER_ID]) do + if device.manufacturer_info.product_id == fingerprint.product_id then + return false -- For Aqara Dimmer Switch with Button. end end + if contains_device_type(device, DIMMABLE_LIGHT_DEVICE_TYPE_ID, endpoint_id) then + return true + end return false end @@ -481,83 +491,51 @@ local function check_field_name_updates(device) end local function assign_switch_profile(device, switch_ep, is_child_device) - local get_profile_by_device_type = function(ep) - -- Some devices report multiple device types which are a subset of - -- a superset device type (For example, Dimmable Light is a superset of - -- On/Off light). This mostly applies to the four light types, so we will want - -- to match the profile for the superset device type. This can be done by - -- matching to the device type with the highest ID - -- Note: Electrical Sensor does not follow the above logic, so it's ignored - local id = 0 - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id ~= ELECTRICAL_SENSOR_ID then - id = math.max(id, dt.device_type_id) - end - end - return device_type_profile_map[id] - end - local profile local electrical_tags = "" for _, ep in ipairs(device.endpoints) do if ep.endpoint_id == switch_ep then - profile = get_profile_by_device_type(ep) - if profile == "plug-binary" or profile == "plug-level" then - local power_cluster_found, energy_cluster_found = false, false - for _, cluster in ipairs(ep.clusters) do - if cluster.cluster_id == clusters.ElectricalPowerMeasurement.ID then - power_cluster_found = true - elseif cluster.cluster_id == clusters.ElectricalEnergyMeasurement.ID then - energy_cluster_found = true - end - end - if power_cluster_found then - electrical_tags = electrical_tags .. "-power" - end - if energy_cluster_found then - electrical_tags = electrical_tags .."-energy-powerConsumption" + -- Some devices report multiple device types which are a subset of + -- a superset device type (For example, Dimmable Light is a superset of + -- On/Off light). This mostly applies to the four light types, so we will want + -- to match the profile for the superset device type. This can be done by + -- matching to the device type with the highest ID + -- Note: Electrical Sensor does not follow the above logic, so it's ignored + local id = 0 + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id ~= ELECTRICAL_SENSOR_ID then + id = math.max(id, dt.device_type_id) end - if electrical_tags ~= "" then - profile = string.gsub(profile, "-binary", "") -- remove the "-binary" in the plug-binary profile name - profile = profile .. electrical_tags + end + profile = device_type_profile_map[id] + local power_tags, energy_tags = "", "" + for _, cluster in ipairs(ep.clusters) do + if cluster.cluster_id == clusters.ElectricalPowerMeasurement.ID then + power_tags = "-power" + elseif cluster.cluster_id == clusters.ElectricalEnergyMeasurement.ID then + energy_tags = "-energy-powerConsumption" end end + electrical_tags = power_tags .. energy_tags + if electrical_tags ~= "" and (profile == "plug-binary" or profile == "plug-level") then + -- remove the "-binary" in the plug-binary profile name + profile = string.gsub(profile, "-binary", "") .. electrical_tags + end break end end - - -- If the device supports the Electrical Sensor Device Type - if electrical_tags == "" then - local electrical_sensor_found = false - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == ELECTRICAL_SENSOR_ID then - electrical_sensor_found = true - end - end + -- Add electrical support to the first switch ep if Electical Sensor is handled on a unique ep + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(switch_eps) + if switch_ep == switch_eps[1] and electrical_tags == "" and contains_device_type(device, ELECTRICAL_SENSOR_ID) then + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then + electrical_tags = electrical_tags .. "-power" end - if electrical_sensor_found then - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - local main_switch_ep = switch_eps[1] - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == main_switch_ep and ep.endpoint_id == switch_ep then - profile = get_profile_by_device_type(ep) - if profile == "plug-binary" or profile == "plug-level" or profile == "light-binary" then - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then - electrical_tags = electrical_tags .. "-power" - end - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then - electrical_tags = electrical_tags .."-energy-powerConsumption" - end - if electrical_tags ~= "" then - profile = string.gsub(profile, "-binary", "") -- remove the "-binary" in the plug-binary/light-binary profile name - profile = profile .. electrical_tags - end - end - break - end - end + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then + electrical_tags = electrical_tags .."-energy-powerConsumption" + end + if electrical_tags ~= "" and (profile == "plug-binary" or profile == "plug-level" or profile == "light-binary") then + profile = string.gsub(profile, "-binary", "") .. electrical_tags end end @@ -733,14 +711,7 @@ local function initialize_buttons_and_switches(driver, device, main_endpoint) end local function detect_bridge(device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == AGGREGATOR_DEVICE_TYPE_ID then - return true - end - end - end - return false + contains_device_type(device, AGGREGATOR_DEVICE_TYPE_ID) end local function device_init(driver, device) 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 84a71bf774..ece501a0e6 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 @@ -47,6 +47,8 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoint_id = parent_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER"}, }, device_types = { {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug @@ -74,8 +76,6 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoint_id = child3_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER"}, }, device_types = { {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug @@ -162,7 +162,7 @@ local function test_init() 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({ profile = "plug-binary" }) + mock_device:expect_metadata_update({ profile = "plug-power-energy-powerConsumption" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) @@ -189,7 +189,7 @@ local function test_init() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 4", - profile = "plug-power-energy-powerConsumption", + profile = "plug-binary", parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child3_ep) }) From aef53f3d04d9a35150de33676c9120dfc256719a Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 29 May 2025 14:31:08 -0500 Subject: [PATCH 5/7] gate another profile type --- drivers/SmartThings/matter-switch/src/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 4edfaa22f7..c3b92473e7 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -777,7 +777,7 @@ local function match_profile(driver, device) profile_name = assign_switch_profile(device, main_endpoint, false) -- ignore attempts to dynamically profile light-level-colorTemperature devices for now, since -- these may lose fingerprinted Kelvin ranges when dynamically profiled. - if profile_name and profile_name ~= "light-level-colorTemperature" then + if profile_name and profile_name ~= "light-level-colorTemperature" and profile_name ~= "light-color-level" then device:try_update_metadata({profile = profile_name}) end end From 40a8983121f990fd31aae55786e0858db38bac21 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Sun, 1 Jun 2025 22:34:32 -0500 Subject: [PATCH 6/7] extend energy profiling logic --- .../SmartThings/matter-switch/src/init.lua | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bc9c22baa7..3aa9973865 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -524,15 +524,24 @@ local function assign_switch_profile(device, switch_ep, is_child_device) break end end - -- Add electrical support to the first switch ep if Electical Sensor is handled on a unique ep - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - if switch_ep == switch_eps[1] and electrical_tags == "" and contains_device_type(device, ELECTRICAL_SENSOR_ID) then - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then - electrical_tags = electrical_tags .. "-power" - end - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then - electrical_tags = electrical_tags .."-energy-powerConsumption" + -- Add electrical support to the switch ep if Electical Sensor is handled on a unique ep + if electrical_tags == "" and contains_device_type(device, ELECTRICAL_SENSOR_ID) then + local electrical_energy_eps = device:get_endpoints(clusters.ElectricalEnergyMeasurement.ID) + local electrical_power_eps = device:get_endpoints(clusters.ElectricalPowerMeasurement.ID) + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(electrical_energy_eps) + table.sort(electrical_power_eps) + table.sort(switch_eps) + for i, ep in ipairs(switch_eps) do + if ep == switch_ep then + if electrical_energy_eps[i] then + electrical_tags = electrical_tags .. "-power" + end + if electrical_power_eps[i] then + electrical_tags = electrical_tags .. "-energy-powerConsumption" + end + break + end end if electrical_tags ~= "" and (profile == "plug-binary" or profile == "plug-level" or profile == "light-binary") then profile = string.gsub(profile, "-binary", "") .. electrical_tags From f4892dff13d462550dc21bb187d3a2fa39776e9e Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Sun, 1 Jun 2025 22:38:27 -0500 Subject: [PATCH 7/7] fix small issue, add test --- .../SmartThings/matter-switch/src/init.lua | 4 +-- .../src/test/test_electrical_sensor.lua | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 3aa9973865..67d740d627 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -534,10 +534,10 @@ local function assign_switch_profile(device, switch_ep, is_child_device) table.sort(switch_eps) for i, ep in ipairs(switch_eps) do if ep == switch_ep then - if electrical_energy_eps[i] then + if electrical_power_eps[i] then electrical_tags = electrical_tags .. "-power" end - if electrical_power_eps[i] then + if electrical_energy_eps[i] then electrical_tags = electrical_tags .. "-energy-powerConsumption" end break diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 70dba47fc8..9b740c0a9f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -56,6 +56,25 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } }, }, }) @@ -647,6 +666,13 @@ test.register_coroutine_test( function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { test_init = test_init }