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 @@ -22,6 +22,9 @@ local fields = require "utils.switch_fields"
local switch_utils = require "utils.switch_utils"
local color_utils = require "utils.color_utils"

local cfg = require "utils.device_configuration"
local device_cfg = cfg.DeviceCfg

local AttributeHandlers = {}

-- [[ ON OFF CLUSTER ATTRIBUTES ]] --
Expand Down Expand Up @@ -247,15 +250,10 @@ end
function AttributeHandlers.active_power_handler(driver, device, ib, response)
if ib.data.value then
local watt_value = ib.data.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT
if ib.endpoint_id ~= 0 then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"}))
else
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"}))
end
if type(device.register_native_capability_attr_handler) == "function" then
device:register_native_capability_attr_handler("powerMeter","power")
end
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"}))
end
if type(device.register_native_capability_attr_handler) == "function" then
device:register_native_capability_attr_handler("powerMeter","power")
end
end

Expand All @@ -279,17 +277,37 @@ end

-- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] --

local function report_power_consumption_to_st_energy(device, endpoint_id, latest_total_imported_energy_wh)
local current_time = os.time()
local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0

-- Ensure that the previous report was sent at least 15 minutes ago
if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then
return
end
device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true })

local state_device = switch_utils.find_child(device, endpoint_id) or device
local previous_imported_report = state_device:get_latest_state("main", capabilities.powerConsumptionReport.ID,
capabilities.powerConsumptionReport.powerConsumption.NAME, { energy = latest_total_imported_energy_wh })
local energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) -- Calculate the energy delta between reports

-- Report the energy consumed during the time interval. The unit of these values should be 'Wh'
local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC
device:emit_event_for_endpoint(endpoint_id, capabilities.powerConsumptionReport.powerConsumption({
start = epoch_to_iso8601(last_time),
["end"] = epoch_to_iso8601(current_time - 1),
deltaEnergy = energy_delta_wh,
energy = latest_total_imported_energy_wh
}))
end

function AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response)
if ib.data.elements.energy then
local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT
device:set_field(fields.TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true})
if ib.endpoint_id ~= 0 then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" }))
else
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" }))
end
switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY))
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" }))
report_power_consumption_to_st_energy(device, ib.endpoint_id, device:get_field(fields.TOTAL_IMPORTED_ENERGY))
end
end

Expand All @@ -299,8 +317,8 @@ function AttributeHandlers.per_energy_imported_handler(driver, device, ib, respo
local latest_energy_report = device:get_field(fields.TOTAL_IMPORTED_ENERGY) or 0
local summed_energy_report = latest_energy_report + watt_hour_value
device:set_field(fields.TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true})
device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" }))
switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY))
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" }))
report_power_consumption_to_st_energy(device, ib.endpoint_id, device:get_field(fields.TOTAL_IMPORTED_ENERGY))
end
end

Expand All @@ -322,8 +340,35 @@ function AttributeHandlers.energy_imported_factory(is_cumulative_report)
end


-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] --
-- [[ POWER TOPOLOGY CLUSTER ATTRIBUTES ]] --

function AttributeHandlers.available_endpoints_handler(driver, device, ib, response)
local set_topology_eps = device:get_field(fields.SET_TOPOLOGY_EPS)
for i, ep in pairs(set_topology_eps or {}) do
if ep.endpoint_id == ib.endpoint_id then
set_topology_eps[i] = nil -- seen, remove from list
local tags = ""
if ep[clusters.ElectricalPowerMeasurement.ID] then tags = tags.."-power" end
if ep[clusters.ElectricalEnergyMeasurement.ID] then tags = tags.."-energy-powerConsumption" end
table.sort(ib.data.elements)
local primary_available_ep = ib.data.elements[1].value -- for consistency, associate data with first listed EP
switch_utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_available_ep, tags)
switch_utils.set_field_for_endpoint(device, fields.PRIMARY_CHILD_EP, ib.endpoint_id, primary_available_ep, { persist = true })
break
end
end

if #set_topology_eps ~= 0 then -- we have not handled all eps
device:set_field(fields.SET_TOPOLOGY_EPS, set_topology_eps) -- permanently remove deleted ep
return
end

device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY)
device_cfg.match_profile(driver, device)
end


-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] --

function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response)
if ib.data.value then
Expand Down
20 changes: 11 additions & 9 deletions drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -45,6 +44,8 @@ function SwitchLifecycleHandlers.device_added(driver, device)
-- was created after the initial subscription report
if device.network_type == device_lib.NETWORK_TYPE_CHILD then
device:send(clusters.OnOff.attributes.OnOff:read(device))
elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then
switch_utils.collect_and_set_electrical_sensor_info(device)
end

-- call device init in case init is not called after added due to device caching
Expand Down Expand Up @@ -87,17 +88,15 @@ function SwitchLifecycleHandlers.device_init(driver, device)
end
local main_endpoint = switch_utils.find_default_endpoint(device)
-- ensure subscription to all endpoint attributes- including those mapped to child devices
for idx, ep in ipairs(device.endpoints) do
for _, ep in ipairs(device.endpoints) do
if ep.endpoint_id ~= main_endpoint then
if device:supports_server_cluster(clusters.OnOff.ID, ep) then
local child_profile = switch_cfg.assign_child_profile(device, ep)
if idx == 1 and string.find(child_profile, "energy") then
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true})
end
end
local id = 0
for _, dt in ipairs(ep.device_types) do
if dt.device_type_id == fields.ELECTRICAL_SENSOR_ID then
for _, attr in pairs(fields.device_type_attribute_map[fields.ELECTRICAL_SENSOR_ID]) do
device:add_subscribed_attribute(attr)
end
end
id = math.max(id, dt.device_type_id)
end
for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do
Expand Down Expand Up @@ -180,6 +179,9 @@ local matter_driver_template = {
[clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler,
[clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler,
},
[clusters.PowerTopology.ID] = {
[clusters.PowerTopology.attributes.AvailableEndpoints.ID] = attribute_handlers.available_endpoints_handler,
},
[clusters.RelativeHumidityMeasurement.ID] = {
[clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -271,10 +272,8 @@ test.register_coroutine_test(
function()
test.socket.matter:__queue_receive(
{
-- don't use "aqara_mock_children[aqara_child1_ep].id,"
-- because energy management is at the root endpoint.
aqara_mock_device.id,
clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000)
clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 0, 17000)
}
)

Expand All @@ -288,7 +287,7 @@ test.register_coroutine_test(
test.socket.matter:__queue_receive(
{
aqara_mock_device.id,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19)
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_19)
}
)

Expand All @@ -308,7 +307,7 @@ test.register_coroutine_test(
test.socket.matter:__queue_receive(
{
aqara_mock_device.id,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29)
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_29)
}
)

Expand All @@ -325,7 +324,7 @@ test.register_coroutine_test(
{
aqara_mock_device.id,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(
aqara_mock_device, 1, cumulative_report_val_39
aqara_mock_device, 0, cumulative_report_val_39
)
}
)
Expand All @@ -338,7 +337,7 @@ test.register_coroutine_test(
aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({
start = "1970-01-01T00:15:01Z",
["end"] = "1970-01-01T00:40:00Z",
deltaEnergy = 0.0,
deltaEnergy = 20.0,
energy = 39.0
}))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
},
},
})

Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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(
Expand Down Expand Up @@ -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 }
)
Expand All @@ -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 }
)
Expand Down
Loading
Loading