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
Original file line number Diff line number Diff line change
Expand Up @@ -325,17 +325,17 @@ end
--- SET feature, all AvailableEndpoints responses must be handled before profiling.
function AttributeHandlers.available_endpoints_handler(driver, device, ib, response)
if device:get_field(fields.profiling_data.POWER_TOPOLOGY) ~= nil then
device.log.warn("Received an AvailableEndpoints response after power topology has already been determined. Ignoring this response.")
device.log.warn_with({hub_logs=true},"Received an AvailableEndpoints response after power topology has already been determined. Ignoring this response.")
return
end
local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
if set_topology_eps == nil then
device.log.warn("Received an AvailableEndpoints response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with SET feature. Ignoring this response.")
device.log.warn_with({hub_logs=true},"Received an AvailableEndpoints response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with SET feature. Ignoring this response.")
return
end

device.log.debug_with({hub_logs=true}, string.format("Handling AvailableEndpoints response for endpoint %d with elements: %s", ib.endpoint_id, st_utils.stringify_table(ib.data.elements or {})))
for i, set_ep_info in pairs(set_topology_eps or {}) do
for i, set_ep_info in ipairs(set_topology_eps or {}) do
if ib.endpoint_id == set_ep_info.endpoint_id then
-- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i)
Expand All @@ -349,7 +349,8 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo
break
end
end
if #set_topology_eps == 0 then -- in other words, all AvailableEndpoints attribute responses have been handled
if switch_utils.is_field_empty(device, fields.ELECTRICAL_SENSOR_EPS) then
device.log.info_with({hub_logs=true}, "All AvailableEndpoints attribute responses for SET Electrical Sensor endpoints have been handled, attempting to match profile")
device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY, {persist=true})
device_cfg.match_profile(driver, device)
end
Expand All @@ -360,17 +361,17 @@ end

function AttributeHandlers.parts_list_handler(driver, device, ib, response)
if device:get_field(fields.profiling_data.POWER_TOPOLOGY) ~= nil then
device.log.warn("Received a PartsList response after power topology has already been determined. Ignoring this response.")
device.log.warn_with({hub_logs=true}, "Received a PartsList response after power topology has already been determined. Ignoring this response.")
return
end
local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
if tree_topology_eps == nil then
device.log.warn("Received a PartsList response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with TREE feature. Ignoring this response.")
device.log.warn_with({hub_logs=true}, "Received a PartsList response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with TREE feature. Ignoring this response.")
return
end

device.log.debug_with({hub_logs=true}, string.format("Handling PartsList response for endpoint %d with elements: %s", ib.endpoint_id, st_utils.stringify_table(ib.data.elements or {})))
for i, tree_ep_info in pairs(tree_topology_eps or {}) do
device.log.info_with({hub_logs=true}, string.format("Handling PartsList response for endpoint %d with elements: %s", ib.endpoint_id, st_utils.stringify_table(ib.data.elements or {})))
for i, tree_ep_info in ipairs(tree_topology_eps or {}) do
if ib.endpoint_id == tree_ep_info.endpoint_id then
-- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i)
Expand All @@ -384,7 +385,8 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response)
break
end
end
if #tree_topology_eps == 0 then -- in other words, all PartsList attribute responses for TREE Electrical Sensor EPs have been handled
if switch_utils.is_field_empty(device, fields.ELECTRICAL_SENSOR_EPS) then
device.log.info_with({hub_logs=true}, "All PartsList attribute responses for TREE Electrical Sensor endpoints have been handled, attempting to match profile")
device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, {persist=true})
device_cfg.match_profile(driver, device)
end
Expand Down
11 changes: 10 additions & 1 deletion drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,20 @@ end
function utils.remove_field_index(device, field_name, index)
local new_table = device:get_field(field_name)
if type(new_table) == "table" then
new_table[index] = nil -- remove value associated with index from table
table.remove(new_table, index) -- remove value associated with index from table
device:set_field(field_name, new_table)
end
end

function utils.is_field_empty(device, field_name)
local field = device:get_field(field_name)
if type(field) == "table" then
return next(field) == nil
else
return field == nil
end
end

function utils.mired_to_kelvin(value, minOrMax)
if value == 0 then -- shouldn't happen, but has
value = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,59 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({
},
})

--- Models an outlet of a multi-outlet power strip, which combines the Electrical Sensor and
--- OnOff Plug In Unit device types on a single endpoint. Note that the Electrical Sensor device
--- type is listed first, so it is the endpoint's primary device type and no profile is mapped to it.
local function build_power_strip_outlet_endpoint(endpoint_id)
return {
endpoint_id = endpoint_id,
clusters = {
{ cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1, },
{ cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 2, },
{ cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 15, },
{ 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 In Unit
}
}
end

--- A 4-outlet power strip that reports its endpoints out of numerical order, as the Tapo
--- P304M does. The order in which the AvailableEndpoints reports are handled must not affect
--- profiling: every Electrical Sensor endpoint has to be accounted for before profiles are matched.
local mock_device_power_strip = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("plug-power-energy-powerConsumption.yml"),
manufacturer_info = {
vendor_id = 0x1392,
product_id = 0x010F,
},
endpoints = {
{
endpoint_id = 0,
clusters = {
{ cluster_id = clusters.Basic.ID, cluster_type = "SERVER" },
},
device_types = {
{ device_type_id = 0x0016, device_type_revision = 1 } -- RootNode
}
},
build_power_strip_outlet_endpoint(1),
build_power_strip_outlet_endpoint(3),
build_power_strip_outlet_endpoint(4),
build_power_strip_outlet_endpoint(2),
},
})

local subscribed_attributes_power_strip = {
clusters.OnOff.attributes.OnOff,
clusters.ElectricalPowerMeasurement.attributes.ActivePower,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported,
clusters.PowerTopology.attributes.AvailableEndpoints,
}

local subscribed_attributes_periodic = {
clusters.OnOff.attributes.OnOff,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
Expand Down Expand Up @@ -201,6 +254,17 @@ local function test_init_periodic()
test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request })
end

local function test_init_power_strip()
test.mock_device.add_test_device(mock_device_power_strip)
local subscribe_request = subscribed_attributes_power_strip[1]:subscribe(mock_device_power_strip)
for i, cluster in ipairs(subscribed_attributes_power_strip) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device_power_strip))
end
end
test.socket.matter:__expect_send({ mock_device_power_strip.id, subscribe_request })
end

test.register_message_test(
"Active power measurement should generate correct messages",
{
Expand Down Expand Up @@ -711,4 +775,39 @@ test.register_message_test(
}
)

test.register_coroutine_test(
"Profiling of a power strip must wait for the AvailableEndpoints report of every Electrical Sensor endpoint",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_power_strip.id, "doConfigure" })
mock_device_power_strip:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.wait_for_events()
-- the reports arrive in endpoint order, which does not match the order that the endpoints
-- were reported in during the interview. Each outlet is the only endpoint in its own power set.
for _, endpoint_id in ipairs({1, 2, 3, 4}) do
test.socket.matter:__queue_receive({
mock_device_power_strip.id,
clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(
mock_device_power_strip, endpoint_id, {uint32(endpoint_id)}
)
})
end
-- every outlet supports both power and energy measurement, so none of them should fall back
-- to the generic "switch-binary" profile used for an OnOff endpoint without electrical tags
for _, endpoint_id in ipairs({2, 3, 4}) do
mock_device_power_strip:expect_device_create({
type = "EDGE_CHILD",
label = string.format("nil %d", endpoint_id),
profile = "plug-power-energy-powerConsumption",
parent_device_id = mock_device_power_strip.id,
parent_assigned_child_key = string.format("%d", endpoint_id)
})
end
mock_device_power_strip:expect_metadata_update({ profile = "plug-power-energy-powerConsumption" })
end,
{
test_init = test_init_power_strip,
min_api_version = 14
}
)

test.run_registered_tests()
Loading