Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function SwitchLifecycleHandlers.device_init(driver, device)
if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true})
end
switch_utils.handle_electrical_sensor_info(device)
switch_utils.cache_electrical_cluster_data(device)
device:extend_device("subscribe", switch_utils.subscribe)
device:subscribe()
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo
end
-- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling.
-- set an assigned child key in the case this will emit events on an EDGE_CHILD device
switch_utils.set_fields_for_electrical_sensor_endpoint(device, set_ep_info, available_endpoints_ids)
switch_utils.cache_electrical_data_from_endpoint(device, set_ep_info, available_endpoints_ids)
break
end
end
Expand Down Expand Up @@ -371,7 +371,7 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response)
end
-- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling.
-- set an assigned child key in the case this will emit events on an EDGE_CHILD device
switch_utils.set_fields_for_electrical_sensor_endpoint(device, tree_ep_info, associated_endpoints_ids)
switch_utils.cache_electrical_data_from_endpoint(device, tree_ep_info, associated_endpoints_ids)
break
end
end
Expand Down
72 changes: 44 additions & 28 deletions drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -413,50 +413,45 @@ function utils.report_power_consumption_to_st_energy(device, endpoint_id, total_
}))
end

--- sets fields for handling EPs with the Electrical Sensor device type
--- Sets fields for handling electrical clusters, including storage of electrical tags (like "-energy-powerConsumption", etc)
--- and associating the clusters with the appropriate devices via the find_child implementation
---
--- @param device table a Matter device object
--- @param electrical_sensor_ep table an EP object that includes an Electrical Sensor device type
--- @param associated_endpoint_ids table EP IDs that are associated with the Electrical Sensor EP
--- @param electrical_ep table an EP that includes electrical clusters.
--- @param associated_ep_ids table EP IDs that should be associated with the electrical clusters
--- @return boolean
function utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sensor_ep, associated_endpoint_ids)
if #associated_endpoint_ids == 0 then
function utils.cache_electrical_data_from_endpoint(device, electrical_ep, associated_ep_ids)
if #associated_ep_ids == 0 then
return false
else
local tags = ""
if utils.find_cluster_on_ep(electrical_sensor_ep, clusters.ElectricalPowerMeasurement.ID) then tags = tags.."-power" end
if utils.find_cluster_on_ep(electrical_sensor_ep, clusters.ElectricalEnergyMeasurement.ID) then tags = tags.."-energy-powerConsumption" end
if utils.find_cluster_on_ep(electrical_ep, clusters.ElectricalPowerMeasurement.ID) then tags = tags.."-power" end
if utils.find_cluster_on_ep(electrical_ep, clusters.ElectricalEnergyMeasurement.ID) then tags = tags.."-energy-powerConsumption" end
if tags == "" then return false end
-- note: using the lowest valued EP ID here is arbitrary (not spec defined) and is done to create internal consistency
-- Ex. for the NODE topology, electrical capabilities will then be associated with the default (aka lowest ID'd) OnOff EP
table.sort(associated_endpoint_ids)
local primary_associated_ep_id = associated_endpoint_ids[1]
table.sort(associated_ep_ids)
local primary_associated_ep_id = associated_ep_ids[1]
-- map the required electrical tags for this electrical sensor EP with the first associated EP ID, used later during profling.
utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_associated_ep_id, tags, {persist = true})
utils.set_field_for_endpoint(device, fields.ASSIGNED_CHILD_KEY, electrical_sensor_ep.endpoint_id, string.format("%d", primary_associated_ep_id), { persist = true })
utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_associated_ep_id, tags)
utils.set_field_for_endpoint(device, fields.ASSIGNED_CHILD_KEY, electrical_ep.endpoint_id, string.format("%d", primary_associated_ep_id), { persist = true })
return true
end
end

function utils.handle_electrical_sensor_info(device)
local electrical_sensor_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR, { with_info = true })
if #electrical_sensor_eps == 0 then
-- no Electrical Sensor EPs are supported. Set profiling data to false and return
device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, {persist=true})
return
end

function utils.cache_electrical_cluster_data(device)
-- energy reporting must be handled by a cumulative report, a periodic report, or both attributes simultaneously.
-- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported.
for _, ep_info in ipairs(electrical_sensor_eps) do
if utils.find_cluster_on_ep(ep_info, clusters.ElectricalEnergyMeasurement.ID,
{feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) then
device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true)
break
end
if #device:get_endpoints(clusters.ElectricalEnergyMeasurement.ID,
{ feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) > 0 then
device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true)
end

-- check the feature map for the first (or only) Electrical Sensor EP if the device profiling has not been completed
if device:get_field(fields.profiling_data.POWER_TOPOLOGY) == nil then
-- if this data has already been cached, we don't need to proceed further
if device:get_field(fields.profiling_data.POWER_TOPOLOGY) then return end
local electrical_sensor_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR, { with_info = true })
if #electrical_sensor_eps > 0 then
-- check the feature map for the first (or only) Electrical Sensor EP
local endpoint_power_topology_cluster = utils.find_cluster_on_ep(electrical_sensor_eps[1], clusters.PowerTopology.ID) or {}
local endpoint_power_topology_feature_map = endpoint_power_topology_cluster.feature_map or 0
if clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.SET_TOPOLOGY, endpoint_power_topology_feature_map) or
Expand All @@ -467,10 +462,31 @@ function utils.handle_electrical_sensor_info(device)
elseif clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, endpoint_power_topology_feature_map) then
-- EP has a NODE topology, so there is only ONE Electrical Sensor EP
device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, {persist=true})
if utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sensor_eps[1], device:get_endpoints(clusters.OnOff.ID)) == false then
if utils.cache_electrical_data_from_endpoint(device, electrical_sensor_eps[1], device:get_endpoints(clusters.OnOff.ID)) == false then
device.log.warn("Electrical Sensor EP with NODE topology found, but no OnOff EPs exist. Electrical Sensor capabilities will not be exposed.")
end
end
else
-- no electrical sensor endpoints are available, so mark power topology as absent
device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, {persist=true})
-- return early if electrical cluster endpoints are absent
local energy_meas_ep_ids = device:get_endpoints(clusters.ElectricalEnergyMeasurement.ID)
local power_meas_ep_ids = device:get_endpoints(clusters.ElectricalPowerMeasurement.ID)
if #energy_meas_ep_ids == 0 and #power_meas_ep_ids == 0 then return end
-- try to cache electrical data from each OnOff cluster endpoint
local endpoint_mapping_successful = false
for _, ep in ipairs(device.endpoints) do
if utils.find_cluster_on_ep(ep, clusters.OnOff.ID) then
endpoint_mapping_successful = utils.cache_electrical_data_from_endpoint(device, ep, { ep.endpoint_id }) or endpoint_mapping_successful
end
end
if endpoint_mapping_successful then return end
-- if all else fails, try to directly map the first (hopefully only) energy endpoint to an OnOff endpoint, with an assumption
-- that if a power cluster exists, it will be on the same endpoint as the energy one. This should only really happen if these
-- lone electrical endpoints exist without corresponding OnOff endpoints, which means they are likely attached to the root node,
-- which is against spec, or some device structure we don't yet support
local energy_ep_info = utils.get_endpoint_info(device, energy_meas_ep_ids[1])
utils.cache_electrical_data_from_endpoint(device, energy_ep_info, device:get_endpoints(clusters.OnOff.ID))
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
-- Copyright © 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local test = require "integration_test"
local clusters = require "st.matter.clusters"
local t_utils = require "integration_test.utils"
local version = require "version"
local switch_fields = require "switch_utils.fields"

if version.api < 11 then
clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement"
clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement"
clusters.PowerTopology = require "embedded_clusters.PowerTopology"
end

local mock_device_with_electrical_measurement_clusters = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("plug-power-energy-powerConsumption.yml"),
manufacturer_info = {
vendor_id = 0x0000,
product_id = 0x0000,
},
endpoints = {
{
endpoint_id = 2,
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.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, },
},
device_types = {
{ device_type_id = switch_fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 1 },
}
}
},
})

local mock_device_with_electrical_measurement_clusters_on_root = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("plug-power-energy-powerConsumption.yml"),
manufacturer_info = {
vendor_id = 0x0000,
product_id = 0x0000,
},
endpoints = {
{
endpoint_id = 0,
clusters = {
{ cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, },
{ cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, },
},
device_types = {
{ device_type_id = 0x0016, device_type_revision = 1 },
},
},
{
endpoint_id = 2,
clusters = {
{ cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, },
},
device_types = {
{ device_type_id = switch_fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 1 },
}
}
},
})

local function test_init()
test.disable_startup_messages()
test.mock_device.add_test_device(mock_device_with_electrical_measurement_clusters)
end
test.set_test_init_function(test_init)

test.register_coroutine_test(
"Test init for OnOff Plug with Electrical Measurement clusters is handled properly",
function()
local subscribed_attributes = {
clusters.OnOff.attributes.OnOff,
clusters.ElectricalPowerMeasurement.attributes.ActivePower,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported,
}
local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_with_electrical_measurement_clusters)
for i, cluster in ipairs(subscribed_attributes) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device_with_electrical_measurement_clusters))
end
end
test.socket.device_lifecycle:__queue_receive({ mock_device_with_electrical_measurement_clusters.id, "init" })
test.socket.matter:__expect_send({ mock_device_with_electrical_measurement_clusters.id, subscribe_request })
test.wait_for_events()
print(mock_device_with_electrical_measurement_clusters:get_field(switch_fields.ELECTRICAL_TAGS .. "_2"))
assert(mock_device_with_electrical_measurement_clusters:get_field(switch_fields.ELECTRICAL_TAGS .. "_2") == "-power-energy-powerConsumption", "Expected electrical tags on EP 2 field to be set correctly")
assert(mock_device_with_electrical_measurement_clusters:get_field(switch_fields.CUMULATIVE_REPORTS_SUPPORTED) == true, "Expected cumulative reports supported field to be set to true")
assert(mock_device_with_electrical_measurement_clusters:get_field(switch_fields.ASSIGNED_CHILD_KEY .. "_2") == "2", "Expected assigned child key to be correctly set to the associated EP 2 ID")
end,
{
min_api_version = 17
}
)

test.register_coroutine_test(
"Test init for OnOff Plug with Electrical Measurement clusters on Root is handled properly",
function()
local subscribed_attributes = {
clusters.OnOff.attributes.OnOff,
clusters.ElectricalPowerMeasurement.attributes.ActivePower,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported,
}
local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_with_electrical_measurement_clusters_on_root)
for i, cluster in ipairs(subscribed_attributes) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device_with_electrical_measurement_clusters_on_root))
end
end
test.socket.device_lifecycle:__queue_receive({ mock_device_with_electrical_measurement_clusters_on_root.id, "init" })
test.socket.matter:__expect_send({ mock_device_with_electrical_measurement_clusters_on_root.id, subscribe_request })
test.wait_for_events()
assert(mock_device_with_electrical_measurement_clusters_on_root:get_field(switch_fields.ELECTRICAL_TAGS .. "_2") == "-power-energy-powerConsumption", "Expected electrical tags on EP 2 field to be set correctly")
assert(mock_device_with_electrical_measurement_clusters_on_root:get_field(switch_fields.CUMULATIVE_REPORTS_SUPPORTED) == true, "Expected cumulative reports supported field to be set to true")
assert(mock_device_with_electrical_measurement_clusters_on_root:get_field(switch_fields.ASSIGNED_CHILD_KEY .. "_0") == "2", "Expected assigned child key to be correctly set to the associated EP 2 ID")
end,
{
test_init = function()
test.disable_startup_messages()
test.mock_device.add_test_device(mock_device_with_electrical_measurement_clusters_on_root)
end,
min_api_version = 17
}
)

test.register_coroutine_test(
"Test doConfigure for OnOff Plug with Electrical Measurement clusters is handled properly",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_with_electrical_measurement_clusters.id, "doConfigure" })
mock_device_with_electrical_measurement_clusters:expect_metadata_update({ profile = "plug-power-energy-powerConsumption" })
mock_device_with_electrical_measurement_clusters:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end,
{
test_init = function()
test.disable_startup_messages()
test.mock_device.add_test_device(mock_device_with_electrical_measurement_clusters)
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.ELECTRICAL_TAGS .. "_2", "-power-energy-powerConsumption", {persist = true})
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.profiling_data.BATTERY_SUPPORT, false, {persist = true})
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.profiling_data.POWER_TOPOLOGY, false, {persist = true})
end,
min_api_version = 17
}
)

test.register_coroutine_test(
"Test doConfigure for OnOff Plug with some Electrical Power Measurement cluster is handled properly",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_with_electrical_measurement_clusters.id, "doConfigure" })
mock_device_with_electrical_measurement_clusters:expect_metadata_update({ profile = "plug-power" })
mock_device_with_electrical_measurement_clusters:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end,
{
test_init = function()
test.disable_startup_messages()
test.mock_device.add_test_device(mock_device_with_electrical_measurement_clusters)
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.ELECTRICAL_TAGS .. "_2", "-power", {persist = true})
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.profiling_data.BATTERY_SUPPORT, false, {persist = true})
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.profiling_data.POWER_TOPOLOGY, false, {persist = true})
end,
min_api_version = 17
}
)

test.register_coroutine_test(
"Test doConfigure for OnOff Plug with Electrical Energy Measurement cluster is handled properly",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_with_electrical_measurement_clusters.id, "doConfigure" })
mock_device_with_electrical_measurement_clusters:expect_metadata_update({ profile = "plug-energy-powerConsumption" })
mock_device_with_electrical_measurement_clusters:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end,
{
test_init = function()
test.disable_startup_messages()
test.mock_device.add_test_device(mock_device_with_electrical_measurement_clusters)
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.ELECTRICAL_TAGS .. "_2", "-energy-powerConsumption", {persist = true})
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.profiling_data.BATTERY_SUPPORT, false, {persist = true})
mock_device_with_electrical_measurement_clusters:set_field(switch_fields.profiling_data.POWER_TOPOLOGY, false, {persist = true})
end,
min_api_version = 17
}
)

test.run_registered_tests()
Loading