-
Notifications
You must be signed in to change notification settings - Fork 463
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Eve] Improve Eve Energy support (#940)
* These changes improve the Eve Energy support - A subdriver has been implemented - It implements the switch, powerMeter, energyMeter and powerConsumptionReport capabilities - It supports the refresh capability - It reads with a timer each minute the current Watt and Watt Accumulated values from custom attributes - The powerMeter and energyMeter values are updated each values or on a refresh - A powerConsumptionReport is sent each 10 minutes by comparing the previous Watt Accumulated value with the current Watt Accumulated value * Eve Energy: Fix 2 unused variable warnings from the Luacheck linter * Use 2-width spaces instead of tabs * Rename the profile to power-energy-powerConsumption * We now store the timer in the device storage and not as a driver-level variable * Don't override the on_off_attr_handler since it is equivalent to the one in the base driver * Rename the profile to power-energy-powerConsumption * Add initial basic tests * Add missing copyright * Remove the matter_handler fallback which is identical to the base driver * Fix formatting * Add unit test to chech that the power meter and energy meter are properly set up when the device is added * Add unit test to check that the timer created in create_poll_schedule triggers the function requestData(). This function will read the standard and custom attributes from the device. * Unit test to check the refresh command * Add a unit test when reporting custom Watt data * Add a unit test when reporting custom Watt accumulated data * Unit test for a report with the custom Watt accumulated attribute sent after 10 minutes * Set the correct creation year in the copyright * Only send the powerConsumptionReport event every 15 minutes instead of every 10 minutes * Eve Energy: Reset the consumption when using the SmartThings command to reset the energy meter * - Set LAST_REPORT_TIME after reading it - Add safety check in case os.time() returns a value before 2001 which could be the cases in unit tests * Add unit tests for the reset command
- Loading branch information
Showing
5 changed files
with
604 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
drivers/SmartThings/matter-switch/profiles/power-energy-powerConsumption.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
name: power-energy-powerConsumption | ||
components: | ||
- id: main | ||
capabilities: | ||
- id: switch | ||
version: 1 | ||
- id: powerMeter | ||
version: 1 | ||
- id: energyMeter | ||
version: 1 | ||
- id: powerConsumptionReport | ||
version: 1 | ||
- id: firmwareUpdate | ||
version: 1 | ||
- id: refresh | ||
version: 1 | ||
categories: | ||
- name: SmartPlug |
265 changes: 265 additions & 0 deletions
265
drivers/SmartThings/matter-switch/src/eve-energy/init.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
-- Copyright 2023 SmartThings | ||
-- | ||
-- Licensed under the Apache License, Version 2.0 (the "License"); | ||
-- you may not use this file except in compliance with the License. | ||
-- You may obtain a copy of the License at | ||
-- | ||
-- http://www.apache.org/licenses/LICENSE-2.0 | ||
-- | ||
-- Unless required by applicable law or agreed to in writing, software | ||
-- distributed under the License is distributed on an "AS IS" BASIS, | ||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
-- See the License for the specific language governing permissions and | ||
-- limitations under the License. | ||
|
||
------------------------------------------------------------------------------------- | ||
-- Definitions | ||
------------------------------------------------------------------------------------- | ||
|
||
local capabilities = require "st.capabilities" | ||
local log = require "log" | ||
local clusters = require "st.matter.clusters" | ||
local cluster_base = require "st.matter.cluster_base" | ||
local utils = require "st.utils" | ||
local data_types = require "st.matter.data_types" | ||
|
||
local EVE_MANUFACTURER_ID = 0x130A | ||
local PRIVATE_CLUSTER_ID = 0x130AFC01 | ||
|
||
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" | ||
local RECURRING_POLL_TIMER = "RECURRING_POLL_TIMER" | ||
local TIMER_REPEAT = (1 * 60) -- Run the timer each minute | ||
local REPORT_TIMEOUT = (15 * 60) -- Report the value each 15 minutes | ||
|
||
|
||
------------------------------------------------------------------------------------- | ||
-- Eve specifics | ||
------------------------------------------------------------------------------------- | ||
|
||
local function is_eve_energy_products(opts, driver, device) | ||
if device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID then | ||
return true | ||
end | ||
|
||
return false | ||
end | ||
|
||
-- Return a ISO 8061 formatted timestamp in UTC (Z) | ||
-- @return e.g. 2022-02-02T08:00:00Z | ||
local function iso8061Timestamp(time) | ||
return os.date("!%Y-%m-%dT%TZ", time) | ||
end | ||
|
||
local function updateEnergyMeter(device, totalConsumptionWh) | ||
-- 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 | ||
|
||
|
||
------------------------------------------------------------------------------------- | ||
-- Timer | ||
------------------------------------------------------------------------------------- | ||
|
||
local function requestData(device) | ||
-- Update the on/off status | ||
device:send(clusters.OnOff.attributes.OnOff:read(device)) | ||
|
||
-- Update the Watt usage | ||
device:send(cluster_base.read(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_WATT, nil)) | ||
|
||
-- Update the energy consumption | ||
device:send(cluster_base.read(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_WATT_ACCUMULATED, nil)) | ||
end | ||
|
||
local function create_poll_schedule(device) | ||
-- 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() | ||
requestData(device) | ||
end, "polling_schedule_timer") | ||
|
||
device:set_field(RECURRING_POLL_TIMER, timer) | ||
end | ||
|
||
|
||
------------------------------------------------------------------------------------- | ||
-- Matter Utilities | ||
------------------------------------------------------------------------------------- | ||
|
||
--- component_to_endpoint helper function to handle situations where | ||
--- device does not have endpoint ids in sequential order from 1 | ||
--- In this case the function returns the lowest endpoint value that isn't 0 | ||
local function find_default_endpoint(device, component) | ||
local res = device.MATTER_DEFAULT_ENDPOINT | ||
local eps = device:get_endpoints(nil) | ||
table.sort(eps) | ||
for _, v in ipairs(eps) do | ||
if v ~= 0 then --0 is the matter RootNode endpoint | ||
res = v | ||
break | ||
end | ||
end | ||
return res | ||
end | ||
|
||
local function component_to_endpoint(device, component_id) | ||
-- Assumes matter endpoint layout is sequentional starting at 1. | ||
local ep_num = component_id:match("switch(%d)") | ||
return ep_num and tonumber(ep_num) or find_default_endpoint(device, component_id) | ||
end | ||
|
||
local function endpoint_to_component(device, ep) | ||
local switch_comp = string.format("switch%d", ep) | ||
if device.profile.components[switch_comp] ~= nil then | ||
return switch_comp | ||
else | ||
return "main" | ||
end | ||
end | ||
|
||
|
||
------------------------------------------------------------------------------------- | ||
-- Device Management | ||
------------------------------------------------------------------------------------- | ||
|
||
local function device_init(driver, device) | ||
log.info_with({ hub_logs = true }, "device init") | ||
device:set_component_to_endpoint_fn(component_to_endpoint) | ||
device:set_endpoint_to_component_fn(endpoint_to_component) | ||
device:subscribe() | ||
|
||
create_poll_schedule(device) | ||
end | ||
|
||
local function device_added(driver, device) | ||
-- Reset the values | ||
device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) | ||
device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) | ||
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 | ||
end | ||
|
||
local function handle_refresh(self, device) | ||
requestData(device) | ||
end | ||
|
||
local function handle_resetEnergyMeter(self, device) | ||
-- 978307200 is the number of seconds from 1 January 1970 to 1 January 2001 | ||
local current_time = os.time() | ||
local current_time_2001 = current_time - 978307200 | ||
if current_time_2001 < 0 then | ||
current_time_2001 = 0 | ||
end | ||
|
||
local last_time = device:get_field(LAST_REPORT_TIME) or 0 | ||
local startTime = iso8061Timestamp(last_time) | ||
local endTime = iso8061Timestamp(current_time - 1) | ||
|
||
-- Reset the consumption on the device | ||
local data = data_types.validate_or_build_type(current_time_2001, data_types.Uint32) | ||
device:send(cluster_base.write(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT, nil, | ||
data)) | ||
|
||
-- 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 = 0, | ||
energy = 0 | ||
})) | ||
|
||
device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) | ||
end | ||
|
||
------------------------------------------------------------------------------------- | ||
-- Eve Energy Handler | ||
------------------------------------------------------------------------------------- | ||
|
||
local function watt_attr_handler(driver, device, ib, zb_rx) | ||
if ib.data.value then | ||
local wattValue = ib.data.value | ||
device:emit_event(capabilities.powerMeter.power({ value = wattValue, unit = "W" })) | ||
end | ||
end | ||
|
||
local function watt_accumulated_attr_handler(driver, device, ib, zb_rx) | ||
if ib.data.value then | ||
local totalConsumptionRawValue = ib.data.value | ||
local totalConsumptionWh = utils.round(1000 * totalConsumptionRawValue) | ||
updateEnergyMeter(device, totalConsumptionWh) | ||
end | ||
end | ||
|
||
local eve_energy_handler = { | ||
NAME = "Eve Energy Handler", | ||
lifecycle_handlers = { | ||
init = device_init, | ||
added = device_added, | ||
removed = device_removed, | ||
}, | ||
matter_handlers = { | ||
attr = { | ||
[PRIVATE_CLUSTER_ID] = { | ||
[PRIVATE_ATTR_ID_WATT] = watt_attr_handler, | ||
[PRIVATE_ATTR_ID_WATT_ACCUMULATED] = watt_accumulated_attr_handler | ||
} | ||
}, | ||
}, | ||
capability_handlers = { | ||
[capabilities.refresh.ID] = { | ||
[capabilities.refresh.commands.refresh.NAME] = handle_refresh, | ||
}, | ||
[capabilities.energyMeter.ID] = { | ||
[capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, | ||
}, | ||
}, | ||
supported_capabilities = { | ||
capabilities.switch, | ||
capabilities.powerMeter, | ||
capabilities.energyMeter, | ||
capabilities.powerConsumptionReport | ||
}, | ||
can_handle = is_eve_energy_products | ||
} | ||
|
||
return eve_energy_handler |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.