Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eve Energy: Improve the performance when the device is off #1087

Merged
Merged
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
119 changes: 81 additions & 38 deletions drivers/SmartThings/matter-switch/src/eve-energy/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ local PRIVATE_ATTR_ID_WATT = 0x130A000A
local PRIVATE_ATTR_ID_WATT_ACCUMULATED = 0x130A000B
local PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT = 0x130A000E

local LAST_REPORT_TIME = "LAST_REPORT_TIME"
-- Timer to update the data each minute if the device is on
local RECURRING_POLL_TIMER = "RECURRING_POLL_TIMER"
local TIMER_REPEAT = (1 * 60) -- Run the timer each minute
local TIMER_REPEAT = (1 * 60) -- Run the timer each minute

-- Timer to report the power consumption every 15 minutes to satisfy the ST energy requirement
local RECURRING_REPORT_POLL_TIMER = "RECURRING_REPORT_POLL_TIMER"
local LAST_REPORT_TIME = "LAST_REPORT_TIME"
local LATEST_TOTAL_CONSUMPTION_WH = "LATEST_TOTAL_CONSUMPTION_WH"
local REPORT_TIMEOUT = (15 * 60) -- Report the value each 15 minutes


Expand All @@ -55,38 +60,11 @@ local function iso8061Timestamp(time)
end

local function updateEnergyMeter(device, totalConsumptionWh)
-- Remember the total consumption so we can report it every 15 minutes
device:set_field(LATEST_TOTAL_CONSUMPTION_WH, totalConsumptionWh, { persist = true })

-- Report the energy consumed
device:emit_event(capabilities.energyMeter.energy({ value = totalConsumptionWh, unit = "Wh" }))

-- Only send powerConsumptionReport every couple of minutes (REPORT_TIMEOUT)
local current_time = os.time()
local last_time = device:get_field(LAST_REPORT_TIME) or 0
local next_time = last_time + REPORT_TIMEOUT
if current_time < next_time then
return
end

device:set_field(LAST_REPORT_TIME, current_time, { persist = true })

-- Calculate the energy consumed between the start and the end time
local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport.ID,
capabilities.powerConsumptionReport.powerConsumption.NAME)

local deltaEnergyWh = 0.0
if previousTotalConsumptionWh ~= nil and previousTotalConsumptionWh.energy ~= nil then
deltaEnergyWh = math.max(totalConsumptionWh - previousTotalConsumptionWh.energy, 0.0)
end

local startTime = iso8061Timestamp(last_time)
local endTime = iso8061Timestamp(current_time - 1)

-- Report the energy consumed during the time interval. The unit of these values should be 'Wh'
device:emit_event(capabilities.powerConsumptionReport.powerConsumption({
start = startTime,
["end"] = endTime,
deltaEnergy = deltaEnergyWh,
energy = totalConsumptionWh
}))
end


Expand All @@ -106,6 +84,11 @@ local function requestData(device)
end

local function create_poll_schedule(device)
local poll_timer = device:get_field(RECURRING_POLL_TIMER)
if poll_timer ~= nil then
return
end

-- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy
-- Eve Energy generally report changes every 10 or 17 minutes
local timer = device.thread:call_on_schedule(TIMER_REPEAT, function()
Expand All @@ -115,6 +98,48 @@ local function create_poll_schedule(device)
device:set_field(RECURRING_POLL_TIMER, timer)
end

local function delete_poll_schedule(device)
local poll_timer = device:get_field(RECURRING_POLL_TIMER)
if poll_timer ~= nil then
device.thread:cancel_timer(poll_timer)
device:set_field(RECURRING_POLL_TIMER, nil)
end
end


local function create_poll_report_schedule(device)
-- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy
local timer = device.thread:call_on_schedule(REPORT_TIMEOUT, function()
local current_time = os.time()
local last_time = device:get_field(LAST_REPORT_TIME) or 0
local latestTotalConsumptionWH = device:get_field(LATEST_TOTAL_CONSUMPTION_WH) or 0

device:set_field(LAST_REPORT_TIME, current_time, { persist = true })

-- Calculate the energy consumed between the start and the end time
local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport.ID,
capabilities.powerConsumptionReport.powerConsumption.NAME)

local deltaEnergyWh = 0.0
if previousTotalConsumptionWh ~= nil and previousTotalConsumptionWh.energy ~= nil then
deltaEnergyWh = math.max(latestTotalConsumptionWH - previousTotalConsumptionWh.energy, 0.0)
end

local startTime = iso8061Timestamp(last_time)
local endTime = iso8061Timestamp(current_time - 1)

-- Report the energy consumed during the time interval. The unit of these values should be 'Wh'
device:emit_event(capabilities.powerConsumptionReport.powerConsumption({
start = startTime,
["end"] = endTime,
deltaEnergy = deltaEnergyWh,
energy = latestTotalConsumptionWH
}))
end, "polling_report_schedule_timer")

device:set_field(RECURRING_REPORT_POLL_TIMER, timer)
end


-------------------------------------------------------------------------------------
-- Matter Utilities
Expand All @@ -133,7 +158,8 @@ local function find_default_endpoint(device, component)
break
end
end
device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT))
device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead",
device.MATTER_DEFAULT_ENDPOINT))
return res
end

Expand Down Expand Up @@ -164,6 +190,7 @@ local function device_init(driver, device)
device:subscribe()

create_poll_schedule(device)
create_poll_report_schedule(device)
end

local function device_added(driver, device)
Expand All @@ -173,11 +200,7 @@ local function device_added(driver, device)
end

local function device_removed(driver, device)
local poll_timer = device:get_field(RECURRING_POLL_TIMER)
if poll_timer ~= nil then
device.thread:cancel_timer(poll_timer)
device:set_field(RECURRING_POLL_TIMER, nil)
end
delete_poll_schedule(device)
end

local function handle_refresh(self, device)
Expand Down Expand Up @@ -216,6 +239,23 @@ end
-- Eve Energy Handler
-------------------------------------------------------------------------------------

local function on_off_attr_handler(driver, device, ib, response)
if ib.data.value then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on())

create_poll_schedule(device)
else
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off())

-- We want to prevent to read the power reports of the device if the device is off
-- We set here the power to 0 before the read is skipped so that the power is correctly displayed and not using a stale value
device:emit_event(capabilities.powerMeter.power({ value = 0, unit = "W" }))

-- Stop the timer when the device is off
delete_poll_schedule(device)
end
end

local function watt_attr_handler(driver, device, ib, zb_rx)
if ib.data.value then
local wattValue = ib.data.value
Expand All @@ -240,6 +280,9 @@ local eve_energy_handler = {
},
matter_handlers = {
attr = {
[clusters.OnOff.ID] = {
[clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler,
},
[PRIVATE_CLUSTER_ID] = {
[PRIVATE_ATTR_ID_WATT] = watt_attr_handler,
[PRIVATE_ATTR_ID_WATT_ACCUMULATED] = watt_accumulated_attr_handler
Expand Down
100 changes: 90 additions & 10 deletions drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ test.register_coroutine_test(
end
)

test.register_coroutine_test(
"Check when the device is removed", function()
test.socket.matter:__set_channel_ordering("relaxed")

local poll_timer = mock_device:get_field("RECURRING_POLL_TIMER")
assert(poll_timer ~= nil, "poll_timer should exist")

local report_poll_timer = mock_device:get_field("RECURRING_REPORT_POLL_TIMER")
assert(report_poll_timer ~= nil, "report_poll_timer should exist")

test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" })
test.wait_for_events()
end
)

test.register_coroutine_test(
"Check that the timer created in create_poll_schedule properly reads the device in requestData",
Expand Down Expand Up @@ -262,16 +276,6 @@ test.register_coroutine_test(
mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 50000, unit = "Wh" }))
)

test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.powerConsumptionReport.powerConsumption({
energy = 50000,
deltaEnergy = 0.0,
start = "1970-01-01T00:00:00Z",
["end"] = "1970-01-01T16:39:59Z"
}))
)

test.wait_for_events()
end
)
Expand Down Expand Up @@ -311,4 +315,80 @@ test.register_coroutine_test(
end
)

test.register_coroutine_test(
"Test the on attribute", function()
local data = data_types.validate_or_build_type(1, data_types.Uint16, "on")
test.socket.matter:__queue_receive(
{
mock_device.id,
cluster_base.build_test_report_data(
mock_device,
0x01,
clusters.OnOff.ID,
clusters.OnOff.attributes.OnOff.ID,
data
)
}
)

test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.switch.switch({ value = "on" }))
)

test.wait_for_events()
end
)

test.register_coroutine_test(
"Report with power consumption after 15 minutes even when device is off", function()
-- device is off
test.socket.matter:__queue_receive({
mock_device.id,
clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 1, false)
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.switch.switch({ value = "off" }))
)

test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 0, unit = "W" }))
)

test.wait_for_events()
-- after 15 minutes, the device should still report power consumption even when off
test.mock_time.advance_time(60 * 15) -- Ensure that the timer created in create_poll_schedule triggers


test.socket.capability:__expect_send(
mock_device:generate_test_message("main",
capabilities.powerConsumptionReport.powerConsumption({
energy = 0,
deltaEnergy = 0.0,
start = "1970-01-01T00:00:00Z",
["end"] = "1970-01-01T00:14:59Z"
}))
)

test.wait_for_events()
end,
{
test_init = function()
local cluster_subscribe_list = {
clusters.OnOff.attributes.OnOff,
}
test.socket.matter:__set_channel_ordering("relaxed")
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
for i, cluster in ipairs(cluster_subscribe_list) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device))
end
end
test.socket.matter:__expect_send({ mock_device.id, subscribe_request })
test.mock_device.add_test_device(mock_device)
test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule")
test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule")
end
}
)

test.run_registered_tests()
Loading