From da6c04c3fcc4ef2beb441a022e97da631d11778f Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 29 Sep 2025 18:08:58 -0500 Subject: [PATCH 1/2] 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/2] 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")