From da6c04c3fcc4ef2beb441a022e97da631d11778f Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 29 Sep 2025 18:08:58 -0500 Subject: [PATCH 1/4] remove ENERGY_MANAGEMENT_ENDPOINT field --- .../generic_handlers/attribute_handlers.lua | 30 ++++++++----------- .../SmartThings/matter-switch/src/init.lua | 10 +------ .../src/test/test_aqara_light_switch_h2.lua | 22 +++++++------- .../src/utils/device_configuration.lua | 8 ++--- .../matter-switch/src/utils/switch_fields.lua | 4 +-- .../matter-switch/src/utils/switch_utils.lua | 29 +++++++----------- 6 files changed, 40 insertions(+), 63 deletions(-) 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..4be292d11f 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -16,6 +16,7 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local version = require "version" local im = require "st.matter.interaction_model" +local device_lib = require "st.device" local st_utils = require "st.utils" local fields = require "utils.switch_fields" @@ -245,17 +246,15 @@ end -- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.active_power_handler(driver, device, ib, response) + local component = device.profile.components["main"] 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_component_event(component, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + else + device:emit_component_event(component, capabilities.powerMeter.power({ value = 0, unit = "W"})) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") end end @@ -281,25 +280,22 @@ end function AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then + local energy_component = device.profile.components["main"] 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 + device:emit_component_event(energy_component, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end function AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then + local energy_component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT 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" })) + device:emit_component_event(energy_component, 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)) end end @@ -309,7 +305,7 @@ function AttributeHandlers.energy_imported_factory(is_cumulative_report) -- workaround: ignore devices supporting Eve's private energy cluster AND the ElectricalEnergyMeasurement cluster local EVE_MANUFACTURER_ID, EVE_PRIVATE_CLUSTER_ID = 0x130A, 0x130AFC01 local eve_private_energy_eps = device:get_endpoints(EVE_PRIVATE_CLUSTER_ID) - if device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then + if device.network_type == device_lib.NETWORK_TYPE_MATTER and device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then return end diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bbf9ca24ee..12d1552d35 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" @@ -87,15 +86,8 @@ 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 id = math.max(id, dt.device_type_id) 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..b5d534d65f 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 @@ -271,10 +271,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) + aqara_mock_children[aqara_child1_ep].id, + clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_children[aqara_child1_ep], 1, 17000) } ) @@ -283,12 +281,14 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) ) + aqara_mock_children[aqara_child1_ep]:expect_native_attr_handler_registration("powerMeter", "power") + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) 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) + aqara_mock_children[aqara_child1_ep].id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_children[aqara_child1_ep], 1, cumulative_report_val_19) } ) @@ -307,8 +307,8 @@ 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) + aqara_mock_children[aqara_child1_ep].id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_children[aqara_child1_ep], 1, cumulative_report_val_29) } ) @@ -323,9 +323,9 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_device.id, + aqara_mock_children[aqara_child1_ep].id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - aqara_mock_device, 1, cumulative_report_val_39 + aqara_mock_children[aqara_child1_ep], 1, cumulative_report_val_39 ) } ) @@ -338,7 +338,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/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index feb21ac193..4390916716 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -59,7 +59,7 @@ function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) -- 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 + if device.manufacturer_info and 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 end @@ -75,7 +75,7 @@ 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 @@ -92,10 +92,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 diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 2244eab661..b476b5fc0d 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,7 +98,8 @@ 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 diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index 86368e9208..bfdbcca530 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -83,7 +83,7 @@ function utils.device_type_supports_button_switch_combination(device, endpoint_i 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 + if device.manufacturer_info and device.manufacturer_info.product_id == fingerprint.product_id then return false -- For Aqara Dimmer Switch with Button. end end @@ -98,8 +98,9 @@ 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 @@ -224,21 +225,13 @@ function utils.report_power_consumption_to_st_energy(device, latest_total_import 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 - })) - end + local power_consumption_component = device.profile.components["main"] + device:emit_component_event(power_consumption_component, 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 return utils From d52d7636bfbc6ca19bd8f2e9971607d1100f8b8c Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Tue, 30 Sep 2025 22:48:11 -0500 Subject: [PATCH 2/4] remove unrelated null powerMeter report logic --- .../matter-switch/src/generic_handlers/attribute_handlers.lua | 2 -- 1 file changed, 2 deletions(-) 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 4be292d11f..3145aa9517 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -250,8 +250,6 @@ 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 device:emit_component_event(component, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - device:emit_component_event(component, capabilities.powerMeter.power({ value = 0, unit = "W"})) end if type(device.register_native_capability_attr_handler) == "function" then device:register_native_capability_attr_handler("powerMeter","power") From 11b751c18bacd1efab89c8282b5aec063c7f715b Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 1 Oct 2025 14:19:37 -0500 Subject: [PATCH 3/4] add handling for Power Topology cluster --- .../generic_handlers/attribute_handlers.lua | 30 ++++++- .../SmartThings/matter-switch/src/init.lua | 10 +++ .../src/test/test_aqara_light_switch_h2.lua | 3 +- .../src/test/test_electrical_sensor.lua | 62 ++++++++++++-- .../test/test_matter_switch_device_types.lua | 8 +- .../test_multi_switch_parent_child_lights.lua | 4 +- .../test_multi_switch_parent_child_plugs.lua | 40 ++++++++- .../src/utils/device_configuration.lua | 81 +++++++++++-------- .../matter-switch/src/utils/switch_fields.lua | 23 ++++-- .../matter-switch/src/utils/switch_utils.lua | 73 +++++++++++++---- 10 files changed, 261 insertions(+), 73 deletions(-) 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 3145aa9517..395610f31d 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -23,6 +23,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 ]] -- @@ -316,8 +319,33 @@ 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) -- for consistency, set tags on first listed EP + switch_utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS_FOR_EP, ib.data.elements[1].value, tags) + 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 + else + device:set_field(fields.SET_TOPOLOGY_EPS, nil) + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY) + device_cfg.match_profile(driver, device) + end +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 12d1552d35..8aad650cf7 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -44,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 @@ -90,6 +92,11 @@ function SwitchLifecycleHandlers.device_init(driver, device) if ep.endpoint_id ~= main_endpoint then 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 @@ -172,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 b5d534d65f..4d666fb209 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 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 4390916716..0048365462 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -31,43 +31,42 @@ local DeviceConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} -function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) +function SwitchDeviceConfiguration.assign_switch_profile(device, switch_ep, is_child_device, electrical_tags) 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 and 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 - end - end + 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 - -- 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 _, 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 + return profile end function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) @@ -80,7 +79,8 @@ function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, m 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 electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS_FOR_EP, ep) + local child_profile = SwitchDeviceConfiguration.assign_switch_profile(device, ep, true, electrical_tags) driver:try_create_device( { type = "EDGE_CHILD", @@ -221,7 +221,18 @@ 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) @@ -233,22 +244,9 @@ function DeviceConfiguration.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 @@ -259,6 +257,19 @@ 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 + local electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS_FOR_EP, main_endpoint) + profile_name = SwitchDeviceConfiguration.assign_switch_profile(device, main_endpoint, false, electrical_tags) + -- 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 b476b5fc0d..969161c8d8 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -106,20 +106,27 @@ SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_ 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.ELECTRICAL_TAGS_FOR_EP = "__ELECTRICAL_TAGS_FOR_EP" +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 bfdbcca530..bb87880ebe 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,21 +79,13 @@ 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 and 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 @@ -183,15 +176,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) @@ -234,4 +233,44 @@ function utils.report_power_consumption_to_st_energy(device, latest_total_import })) end +-- 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 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.ELECTRICAL_TAGS_FOR_EP, 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 + return utils From 7ef7418b88b5314d5a11c406114d57dd4baef916 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 2 Oct 2025 15:26:42 -0500 Subject: [PATCH 4/4] rework find_child logic --- .../generic_handlers/attribute_handlers.lua | 55 +++++++++++++------ .../src/test/test_aqara_light_switch_h2.lua | 18 +++--- .../src/utils/device_configuration.lua | 27 +++++---- .../matter-switch/src/utils/switch_fields.lua | 3 +- .../matter-switch/src/utils/switch_utils.lua | 39 ++----------- 5 files changed, 70 insertions(+), 72 deletions(-) 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 395610f31d..704ee19c97 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -16,7 +16,6 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local version = require "version" local im = require "st.matter.interaction_model" -local device_lib = require "st.device" local st_utils = require "st.utils" local fields = require "utils.switch_fields" @@ -249,10 +248,9 @@ end -- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.active_power_handler(driver, device, ib, response) - local component = device.profile.components["main"] if ib.data.value then local watt_value = ib.data.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - device:emit_component_event(component, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + 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") @@ -279,25 +277,48 @@ 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 energy_component = device.profile.components["main"] 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}) - device:emit_component_event(energy_component, capabilities.energyMeter.energy({ value = watt_hour_value, 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 = watt_hour_value, unit = "Wh" })) + report_power_consumption_to_st_energy(device, ib.endpoint_id, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end function AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then - local energy_component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT 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_component_event(energy_component, 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 @@ -306,7 +327,7 @@ function AttributeHandlers.energy_imported_factory(is_cumulative_report) -- workaround: ignore devices supporting Eve's private energy cluster AND the ElectricalEnergyMeasurement cluster local EVE_MANUFACTURER_ID, EVE_PRIVATE_CLUSTER_ID = 0x130A, 0x130AFC01 local eve_private_energy_eps = device:get_endpoints(EVE_PRIVATE_CLUSTER_ID) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then + if device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then return end @@ -329,19 +350,21 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo 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) -- for consistency, set tags on first listed EP - switch_utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS_FOR_EP, ib.data.elements[1].value, tags) + 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 - else - device:set_field(fields.SET_TOPOLOGY_EPS, nil) - device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY) - device_cfg.match_profile(driver, device) + return end + + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY) + device_cfg.match_profile(driver, device) 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 4d666fb209..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 @@ -272,8 +272,8 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, - clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_children[aqara_child1_ep], 1, 17000) + aqara_mock_device.id, + clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 0, 17000) } ) @@ -282,14 +282,12 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) ) - aqara_mock_children[aqara_child1_ep]:expect_native_attr_handler_registration("powerMeter", "power") - test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_children[aqara_child1_ep], 1, cumulative_report_val_19) + aqara_mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_19) } ) @@ -308,8 +306,8 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_children[aqara_child1_ep], 1, cumulative_report_val_29) + aqara_mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_29) } ) @@ -324,9 +322,9 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, + aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - aqara_mock_children[aqara_child1_ep], 1, cumulative_report_val_39 + aqara_mock_device, 0, cumulative_report_val_39 ) } ) diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index 0048365462..e8bc890d63 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -31,8 +31,9 @@ local DeviceConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} -function SwitchDeviceConfiguration.assign_switch_profile(device, switch_ep, is_child_device, electrical_tags) +function SwitchDeviceConfiguration.assign_switch_profile(device, switch_ep, opts) local profile + 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 @@ -52,17 +53,18 @@ function SwitchDeviceConfiguration.assign_switch_profile(device, switch_ep, is_c end end + 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 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 + 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 @@ -79,8 +81,7 @@ function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, m 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 electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS_FOR_EP, ep) - local child_profile = SwitchDeviceConfiguration.assign_switch_profile(device, ep, true, electrical_tags) + local child_profile = SwitchDeviceConfiguration.assign_switch_profile(device, ep, { is_child_device = true }) driver:try_create_device( { type = "EDGE_CHILD", @@ -238,6 +239,11 @@ function DeviceConfiguration.match_profile(driver, device) 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 @@ -261,8 +267,7 @@ function DeviceConfiguration.match_profile(driver, device) end -- after doing all previous profiling steps, attempt to re-profile main/parent switch/plug device - local electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS_FOR_EP, main_endpoint) - profile_name = SwitchDeviceConfiguration.assign_switch_profile(device, main_endpoint, false, electrical_tags) + 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 diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 969161c8d8..ace96d2ac6 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -124,7 +124,8 @@ SwitchFields.device_overrides_per_vendor = { } SwitchFields.SET_TOPOLOGY_EPS = "__SET_TOPOLOGY_EPS" -SwitchFields.ELECTRICAL_TAGS_FOR_EP = "__ELECTRICAL_TAGS_FOR_EP" +SwitchFields.PRIMARY_CHILD_EP = "__PRIMARY_CHILD_EP" +SwitchFields.ELECTRICAL_TAGS = "__ELECTRICAL_TAGS" SwitchFields.profiling_data = { POWER_TOPOLOGY = "__POWER_TOPOLOGY", } diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index bb87880ebe..324b7a35a9 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -156,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 @@ -202,37 +203,6 @@ 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) - 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' - local power_consumption_component = device.profile.components["main"] - device:emit_component_event(power_consumption_component, 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 - -- 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) @@ -264,7 +234,8 @@ function utils.collect_and_set_electrical_sensor_info(device) 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.ELECTRICAL_TAGS_FOR_EP, switch_eps[1], tags) + 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