diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua index ad8a74a00f..704ee19c97 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -22,6 +22,9 @@ local fields = require "utils.switch_fields" local switch_utils = require "utils.switch_utils" local color_utils = require "utils.color_utils" +local cfg = require "utils.device_configuration" +local device_cfg = cfg.DeviceCfg + local AttributeHandlers = {} -- [[ ON OFF CLUSTER ATTRIBUTES ]] -- @@ -247,15 +250,10 @@ end function AttributeHandlers.active_power_handler(driver, device, ib, response) if ib.data.value then local watt_value = ib.data.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - end - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") end end @@ -279,17 +277,37 @@ end -- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- +local function report_power_consumption_to_st_energy(device, endpoint_id, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + + -- Ensure that the previous report was sent at least 15 minutes ago + if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then + return + end + device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + local state_device = switch_utils.find_child(device, endpoint_id) or device + local previous_imported_report = state_device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME, { energy = latest_total_imported_energy_wh }) + local energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) -- Calculate the energy delta between reports + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC + device:emit_event_for_endpoint(endpoint_id, capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) +end + function AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT device:set_field(fields.TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - end - switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + report_power_consumption_to_st_energy(device, ib.endpoint_id, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end @@ -299,8 +317,8 @@ function AttributeHandlers.per_energy_imported_handler(driver, device, ib, respo local latest_energy_report = device:get_field(fields.TOTAL_IMPORTED_ENERGY) or 0 local summed_energy_report = latest_energy_report + watt_hour_value device:set_field(fields.TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) - device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) - switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) + report_power_consumption_to_st_energy(device, ib.endpoint_id, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end @@ -322,8 +340,35 @@ function AttributeHandlers.energy_imported_factory(is_cumulative_report) end --- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- +-- [[ POWER TOPOLOGY CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.available_endpoints_handler(driver, device, ib, response) + local set_topology_eps = device:get_field(fields.SET_TOPOLOGY_EPS) + for i, ep in pairs(set_topology_eps or {}) do + if ep.endpoint_id == ib.endpoint_id then + set_topology_eps[i] = nil -- seen, remove from list + local tags = "" + if ep[clusters.ElectricalPowerMeasurement.ID] then tags = tags.."-power" end + if ep[clusters.ElectricalEnergyMeasurement.ID] then tags = tags.."-energy-powerConsumption" end + table.sort(ib.data.elements) + local primary_available_ep = ib.data.elements[1].value -- for consistency, associate data with first listed EP + switch_utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_available_ep, tags) + switch_utils.set_field_for_endpoint(device, fields.PRIMARY_CHILD_EP, ib.endpoint_id, primary_available_ep, { persist = true }) + break + end + end + + if #set_topology_eps ~= 0 then -- we have not handled all eps + device:set_field(fields.SET_TOPOLOGY_EPS, set_topology_eps) -- permanently remove deleted ep + return + end + + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY) + device_cfg.match_profile(driver, device) +end + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) if ib.data.value then diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bbf9ca24ee..8aad650cf7 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -24,7 +24,6 @@ local fields = require "utils.switch_fields" local switch_utils = require "utils.switch_utils" local cfg = require "utils.device_configuration" local device_cfg = cfg.DeviceCfg -local switch_cfg = cfg.SwitchCfg local button_cfg = cfg.ButtonCfg local attribute_handlers = require "generic_handlers.attribute_handlers" @@ -45,6 +44,8 @@ function SwitchLifecycleHandlers.device_added(driver, device) -- was created after the initial subscription report if device.network_type == device_lib.NETWORK_TYPE_CHILD then device:send(clusters.OnOff.attributes.OnOff:read(device)) + elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then + switch_utils.collect_and_set_electrical_sensor_info(device) end -- call device init in case init is not called after added due to device caching @@ -87,17 +88,15 @@ function SwitchLifecycleHandlers.device_init(driver, device) end local main_endpoint = switch_utils.find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices - for idx, ep in ipairs(device.endpoints) do + for _, ep in ipairs(device.endpoints) do if ep.endpoint_id ~= main_endpoint then - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - local child_profile = switch_cfg.assign_child_profile(device, ep) - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end local id = 0 for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == fields.ELECTRICAL_SENSOR_ID then + for _, attr in pairs(fields.device_type_attribute_map[fields.ELECTRICAL_SENSOR_ID]) do + device:add_subscribed_attribute(attr) + end + end id = math.max(id, dt.device_type_id) end for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do @@ -180,6 +179,9 @@ local matter_driver_template = { [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, + [clusters.PowerTopology.ID] = { + [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = attribute_handlers.available_endpoints_handler, + }, [clusters.RelativeHumidityMeasurement.ID] = { [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler }, 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 27fe47f11a..d014d61ed7 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 @@ -35,7 +35,8 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ clusters = { {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2 }, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 } + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 }, + {cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1 } -- NODE_TOPOLOGY }, device_types = { {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode @@ -271,10 +272,8 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - -- don't use "aqara_mock_children[aqara_child1_ep].id," - -- because energy management is at the root endpoint. aqara_mock_device.id, - clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000) + clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 0, 17000) } ) @@ -288,7 +287,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_19) } ) @@ -308,7 +307,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_29) } ) @@ -325,7 +324,7 @@ test.register_coroutine_test( { aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - aqara_mock_device, 1, cumulative_report_val_39 + aqara_mock_device, 0, cumulative_report_val_39 ) } ) @@ -338,7 +337,7 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ start = "1970-01-01T00:15:01Z", ["end"] = "1970-01-01T00:40:00Z", - deltaEnergy = 0.0, + deltaEnergy = 20.0, energy = 39.0 })) ) 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 d981af3110..41f2336433 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -16,6 +16,7 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" local version = require "version" if version.api < 11 then @@ -44,6 +45,7 @@ local mock_device = test.mock_device.build_test_matter_device({ clusters = { { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY }, device_types = { { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor @@ -56,9 +58,29 @@ local mock_device = test.mock_device.build_test_matter_device({ {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 } - } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY + }, + 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 + } + }, }, }) @@ -82,18 +104,22 @@ 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, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, } -- SET_TOPOLOGY }, device_types = { - { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor } }, }, }) local subscribed_attributes_periodic = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, } local subscribed_attributes = { clusters.OnOff.attributes.OnOff, @@ -138,14 +164,19 @@ local periodic_report_val_23 = { } local function test_init() + test.mock_device.add_test_device(mock_device) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 1) + read_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) @@ -157,11 +188,13 @@ local function test_init_periodic() subscribe_request:merge(cluster:subscribe(mock_device_periodic)) end end - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device_periodic.id, 1) + test.socket.matter:__expect_send({ mock_device_periodic.id, read_req }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "init" }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) end test.register_message_test( @@ -400,9 +433,20 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", 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_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + 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) + }) end, { test_init = test_init } ) @@ -411,8 +455,10 @@ test.register_coroutine_test( "Test profile change on init for only Periodic Electrical Sensor device type", function() test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) - mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device_periodic.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device_periodic, 1, {uint32(1)})}) + mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) end, { test_init = test_init_periodic } ) 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 0046244bcd..f2274e6555 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 @@ -473,6 +473,8 @@ end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -505,6 +507,7 @@ local function test_init_mounted_on_off_control() 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" }) end @@ -526,6 +529,7 @@ local function test_init_mounted_dimmable_load_control() 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" }) end @@ -566,6 +570,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" }) mock_device_parent_child_different_types:expect_device_create({ @@ -617,6 +622,7 @@ local function test_init_light_level_motion() test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -704,4 +710,4 @@ test.register_coroutine_test( { test_init = test_init_light_level_motion } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file 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 21c9e1087d..15ba79e061 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 @@ -189,6 +189,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" }) for _, child in pairs(mock_children) do @@ -260,6 +261,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" }) for _, child in pairs(mock_children_non_sequential) do @@ -687,4 +689,4 @@ test.register_coroutine_test( { test_init = test_init_parent_child_endpoints_non_sequential } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file 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 03c898b7f5..f09ab3c858 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 @@ -24,6 +24,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", @@ -69,6 +71,24 @@ 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"}, + }, + 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"}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + } + } } }) @@ -146,6 +166,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" }) for _, child in pairs(mock_children) do @@ -167,6 +188,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-binary", + 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-binary", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child4_ep) + }) end local mock_children_child_profile_override = {} @@ -196,6 +233,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" }) for _, child in pairs(mock_children_child_profile_override) do @@ -389,4 +427,4 @@ test.register_coroutine_test( { test_init = test_init_child_profile_override } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index feb21ac193..e8bc890d63 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -31,43 +31,44 @@ local DeviceConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} -function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) +function SwitchDeviceConfiguration.assign_switch_profile(device, switch_ep, opts) local profile 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 -- 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 - id = math.max(id, dt.device_type_id) + if dt.device_type_id ~= fields.ELECTRICAL_SENSOR_ID then + id = math.max(id, dt.device_type_id) + end end profile = fields.device_type_profile_map[id] 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(fields.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 == fields.AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile + local electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS, switch_ep) + if electrical_tags ~= nil and (profile == "plug-binary" or profile == "plug-level" or profile == "light-binary") then + profile = string.gsub(profile, "-binary", "") .. electrical_tags + end + + if opts and opts.is_child_device then + -- Check if child device has an overridden child profile that differs from the child's generic device type profile + for _, fingerprint in ipairs(fields.device_overrides_per_vendor[device.manufacturer_info.vendor_id] or {}) do + if device.manufacturer_info.product_id == fingerprint.product_id and profile == fingerprint.initial_profile then + return fingerprint.target_profile end end + -- default to "switch-binary" if no child profile is found + return profile or "switch-binary" end - - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" + return profile end function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) @@ -75,12 +76,12 @@ function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, m local parent_child_device = false local switch_eps = device:get_endpoints(clusters.OnOff.ID) table.sort(switch_eps) - for idx, ep in ipairs(switch_eps) do + for _, ep in ipairs(switch_eps) do if device:supports_server_cluster(clusters.OnOff.ID, ep) then 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 = SwitchDeviceConfiguration.assign_child_profile(device, ep) + local child_profile = SwitchDeviceConfiguration.assign_switch_profile(device, ep, { is_child_device = true }) driver:try_create_device( { type = "EDGE_CHILD", @@ -92,10 +93,6 @@ function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, m } ) parent_child_device = true - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end end end end @@ -225,34 +222,37 @@ function DeviceConfiguration.initialize_buttons_and_switches(driver, device, mai return profile_found end +local function profiling_data_still_required(device) + for _, field in pairs(fields.profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + function DeviceConfiguration.match_profile(driver, device) + if profiling_data_still_required(device) then return end + local main_endpoint = switch_utils.find_default_endpoint(device) -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. local profile_found = DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) + else + for _, ep in ipairs(device.endpoints) do + -- this may be persist-set during initial device interview. If not PARENT/CHILD, we can drop this data. + switch_utils.set_field_for_endpoint(device, fields.PRIMARY_CHILD_EP, ep.endpoint_id, nil) + end end if profile_found then return 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 @@ -263,6 +263,18 @@ function DeviceConfiguration.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 = SwitchDeviceConfiguration.assign_switch_profile(device, main_endpoint) + -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since + -- these may lose fingerprinted Kelvin ranges when dynamically profiled. + if profile_name and profile_name ~= "light-level-colorTemperature" and profile_name ~= "light-color-level" then + if profile_name == "light-level" and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then + profile_name = "light-level-motion" + end + device:try_update_metadata({profile = profile_name}) end end diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 2244eab661..ace96d2ac6 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -86,7 +86,6 @@ SwitchFields.CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000t -- table for devices that joined prior to this transition, and is also used for -- button devices that require component mapping. SwitchFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -SwitchFields.ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" SwitchFields.IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" SwitchFields.COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" SwitchFields.COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" @@ -99,27 +98,36 @@ SwitchFields.COLOR_MODE = "__color_mode" SwitchFields.updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil } + { current_field_name = "__switch_intialized", updated_field_name = nil }, + { current_field_name = "__energy_management_endpoint", updated_field_name = nil } } SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION SwitchFields.X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY -SwitchFields.child_device_profile_overrides_per_vendor_id = { +SwitchFields.device_overrides_per_vendor = { [0x1321] = { { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, }, [0x115F] = { - { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1003, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1004, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x1005, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) + { product_id = 0x1006, combo_switch_button = false }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1008, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1009, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x100A, combo_switch_button = false }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) } + +} + +SwitchFields.SET_TOPOLOGY_EPS = "__SET_TOPOLOGY_EPS" +SwitchFields.PRIMARY_CHILD_EP = "__PRIMARY_CHILD_EP" +SwitchFields.ELECTRICAL_TAGS = "__ELECTRICAL_TAGS" +SwitchFields.profiling_data = { + POWER_TOPOLOGY = "__POWER_TOPOLOGY", } SwitchFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index 86368e9208..324b7a35a9 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -16,6 +16,7 @@ local fields = require "utils.switch_fields" local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local capabilities = require "st.capabilities" +local im = require "st.matter.interaction_model" local log = require "log" local utils = {} @@ -78,28 +79,21 @@ end --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. function utils.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 == fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID then - for _, fingerprint in ipairs(fields.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(fields.device_overrides_per_vendor[fields.AQARA_MANUFACTURER_ID] or {}) do + if fingerprint.product_id == device.manufacturer_info.product_id and fingerprint.combo_switch_button == false then + return false -- For Aqara Dimmer Switch with Button. end end - return false + local dimmable_eps = utils.get_endpoints_by_dt(device, fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID) + return utils.tbl_contains(dimmable_eps, endpoint_id) end --- find_default_endpoint is a helper function to handle situations where --- device does not have endpoint ids in sequential order from 1 function utils.find_default_endpoint(device) - if device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then + if device.manufacturer_info and + device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and + device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then -- In case of Aqara Climate Sensor W100, in order to sequentially set the button name to button 1, 2, 3 return device.MATTER_DEFAULT_ENDPOINT end @@ -162,8 +156,9 @@ function utils.endpoint_to_component(device, ep) return "main" end -function utils.find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) +function utils.find_child(parent_device, ep_id) + local primary_ep_key = utils.get_field_for_endpoint(parent_device, fields.PRIMARY_CHILD_EP, ep_id) or ep_id + return parent_device:get_child_by_parent_assigned_key(string.format("%d", primary_ep_key)) end -- Fallback handler for responses that dont have their own handler @@ -182,15 +177,21 @@ function utils.create_multi_press_values_list(size, supportsHeld) return list end -function utils.detect_bridge(device) +-- get a list of endpoints for a specified device type. +function utils.get_endpoints_by_dt(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 == fields.AGGREGATOR_DEVICE_TYPE_ID then - return true + if dt.device_type_id == device_type_id then + table.insert(dt_eps, ep.endpoint_id) end end end - return false + return dt_eps +end + +function utils.detect_bridge(device) + return #utils.get_endpoints_by_dt(device, fields.AGGREGATOR_DEVICE_TYPE_ID) > 0 end function utils.detect_matter_thing(device) @@ -202,42 +203,44 @@ function utils.detect_matter_thing(device) return device:supports_capability(capabilities.refresh) end -function utils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) - local current_time = os.time() - local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 - - -- Ensure that the previous report was sent at least 15 minutes ago - if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then - return - end - - device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) +-- associate this EP with the first OnOff EP. These are not necessarily the same EP. +function utils.collect_and_set_electrical_sensor_info(device) + local el_dt_eps = utils.get_endpoints_by_dt(device, fields.ELECTRICAL_SENSOR_ID) + local electrical_sensor_eps = {} + local available_eps_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, ep in ipairs(device.endpoints) do + if utils.tbl_contains(el_dt_eps, ep.endpoint_id) then + local el_ep_info = { endpoint_id = ep.endpoint_id } + for _, cluster in ipairs(ep.clusters) do + el_ep_info[cluster.cluster_id] = cluster.feature_map -- key the cluster's feature map on each supported cluster id + end + table.insert(electrical_sensor_eps, el_ep_info) + -- this read request will ONLY be sent if the device supports the SET_TOPOLOGY feature map + available_eps_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(device, ep.endpoint_id)) + end end - local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - if not device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - else - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) + local electrical_ep = electrical_sensor_eps[1] or {} + if electrical_ep[clusters.PowerTopology.ID] == clusters.PowerTopology.types.Feature.SET_TOPOLOGY then + device:set_field(fields.SET_TOPOLOGY_EPS, electrical_sensor_eps) -- assume any other stored EPs also have a SET topology + device:send(available_eps_req) + elseif electrical_ep[clusters.PowerTopology.ID] == clusters.PowerTopology.types.Feature.NODE_TOPOLOGY then + -- ElectricalSensor EP has a NODE topology, so this is the ONLY Electrical Sensor EP + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.NODE_TOPOLOGY) + -- associate this EP's electrical tags with the first OnOff EP. These are not necessarily the same EP. + local tags = "" + if electrical_ep[clusters.ElectricalPowerMeasurement.ID] then tags = tags.."-power" end + if electrical_ep[clusters.ElectricalEnergyMeasurement.ID] then tags = tags.."-energy-powerConsumption" end + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(switch_eps) + if switch_eps[1] then + utils.set_field_for_endpoint(device, fields.PRIMARY_CHILD_EP, electrical_ep.endpoint_id, switch_eps[1], { persist = true }) + utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, switch_eps[1], tags) + else + device.log.warn("Electrical Sensor EP with NODE topology found, but no OnOff EPs exist. Electrical Sensor capabilities will not be exposed.") + end + else -- either no Electrical Sensor EPs are supported, or an unsupported topology was found (ex. TREE, SET+DYPF) + device:set_field(fields.profiling_data.POWER_TOPOLOGY, false) end end