Thanks for the feedback! It contains relevant information!
in the meantime (apologies for not responding earlier), I created a Dzvents script that takes two key parameters generated by my heat pump (via the EMS-ESP "BBQ-kees" gateway): 'meter heating' ("eIn", the energy consumption for heating purposes) and 'energy heating' ("eOut", the energy generated by the heat pump). These are accumulating numbers updated every minute.
I found on
https://www.gridx.ai/knowledge/coeffici ... rmance-cop and on
https://en.wikipedia.org/wiki/Coefficie ... erformance that for a heat pump, used to heat a space, one could use:
COP = (eOut + eIn) / (eIn)
(for cooling a space the article suggests COP = (eOut) / (eIn)). That makes sense.
In order to calculate the COP, I need to decide over which time period I want determine the COP. I have taken 10 minutes for now (adjustable), I'm not sure how much time makes most sense yet. The script keeps track of the eOut and eIn and determines delta's for both over 10 minutes and calculates the COP over that period. The result is sent to a Custom Sensor virtual device. The sensor provides as usual in its log averages over days/months/years. The 10-minute-COP's averaged over -say- a week is obviously not the same as the COP calculated over that same week.
Some further care is taken to ensure the 'eIn' and 'eOut' originate from the same data message from the heatpump, it is not guaranteed that both eIn and eOut are updated at the same time in Domoticz, neither do I know which of them if first/last updated.
This is a (very) first working version (updated 250203):
Code: Select all
-- COP calculator
-- 250127.1/250128/250129.2
-- v2: rounding cop to 3 decimals, catch error when cop divide by zero, counter "== 1" replaced by '<=1' for COP calculation
-- 250130: if eInDelta is 0 over the sample period, a "COP" does not make sense (and would yield divide by 0), in that case, no COP is reported.
--[[
** Description
Objective of this script is to show the COP of my heat pump on a Domoticz device.
The script calculates the COP based in two parameters provided by the ems-esp gateway between my
heat pump and Domoticz: 'meter heating' ("eIn", the energy consumption for heating purposes) and
'energy heating' ("eOut", the energy generated by the heat pump). These are accumulating numbers
updated every minute.
As found on https://www.gridx.ai/knowledge/coefficient-of-performance-cop, you can use
COP = (eOut + eIn) / (eIn) to calculate the COP.
The script is triggered by updates of the eOut device. As I cannot be sure that eIn is updated before
eOut (or vice verse), the script triggers itself again a few seconds after the eOut trigger using
a Custom Event. This ensures both eOut and eIn originate from the same heat pump data sample.
The COP is calculated over several updates of eOut/eIn, for example spanning a period of 10 mins. An
internal sample counter is used for this.
The value of eOut and eIn is stored using a persistent variable. The script has some means to
reinitialize these values.
The COP values are sent to a dummy device of type general/custom counter. this device proides the
usual Domoticz averages 7 days, month, year, compare years. Note that for example the months COP
average is not the same as the COP calculated over a period of a month.
Installing and requirements
- 'meter heating' and 'energy heating' are available as Domoticz devices and updated each minute
- a dummy/virtual device type general/custom counter has been created with label "COP"
** Testing
-To start from 'zero'
- set local variable 'initializeOnly' to true
- start the script
- at the first trigger, all persistent variables will be reinitialized
- stop the script and set 'initializeOnly' to false again for normal used
- Debug logging
- set local var logLevel to domoticz.LOG_DEBUG, default to domoticz.LOG_INFO
- More triggering
- add "timer = { 'every minute' }," to run the script anyhow every minute
** Notes
- I'm not sure about the best time span (number of samples) to wait before a practical COP calculation
- during deicing, the heat pump uses heat from inside, so eOut will decrease, COP will be <1 or even
this could lead to negative COP if deicing during sample period takes more energy then eIn over same period
** Improvements
- add input validation on some local parameters? or just add noe to the comment?
** Abbreviations
dz = Dz = Domoticz
eOut = accumulated energy (e.g. heat) generated by the heatpump for heating purposes
eIn = accumulated energy (e.g. electrical) used by the heatpump for heating purposes
--]]
-- local variables
local logMarker = 'COP calc' -- [string] same as the title to make it clear in the Domoticz log
local logLevel = domoticz.LOG_DEBUG -- use domoticz.LOG_DEBUG for debugging, otherwise use domoticz.LOG_INFO
local eOutIdx = 1093 -- [idx] eOut Dz device (a general/kWh device), holds the accumulated heatpump output for heating
local eInIdx = 1100 -- [idx] eIn Dz device (a general/kWh device), holds the accumulate heatpump energy used for heating
local copSensorIdx = 1169 -- [idx] cop Dz dummy device, to show the COP this scropt determines
local copCustomEvent = 'waitAbit' -- [string] name of the custom event used to trigger this script
local delay = 2 -- [s] number of seconds to wait before retrieving second value
local initCounter = 10 -- [int] number of samples to wait between calculating CoP (e.g. calculate cop over 10 x sample time = 10 x 1 min = 10 mins)
local initializeOnly = false -- [boolean] default: false. if set to true, the script will *only* reinitialize persistent variables when triggered
-- end local variables
-- functions go here
function reinitPersisVar(dz)
dz.log('The initializeOnly local variable has been set to: ' .. tostring(initializeOnly), dz.LOG_DEBUG)
dz.log('Re-initializing persistent variables is all that will happen!', dz.LOG_DEBUG)
dz.log('counter = ' .. tostring(dz.data.counter), dz.LOG_DEBUG)
dz.log('eOutPrv = ' .. tostring(dz.data.eOutPrv), dz.LOG_DEBUG)
dz.log('eInPrv = ' .. tostring(dz.data.eInPrv), dz.LOG_DEBUG)
dz.log('eOutNow = ' .. tostring(dz.data.eOutNow), dz.LOG_DEBUG)
dz.log('re-initilizing persistent variables...', dz.LOG_DEBUG)
dz.data.initialize('eOutPrv')
dz.data.initialize('eInPrv')
dz.data.initialize('counter')
dz.data.initialize('eOutNow')
dz.log('counter = ' .. tostring(dz.data.counter), dz.LOG_DEBUG)
dz.log('eOutPrv = ' .. tostring(dz.data.eOutPrv), dz.LOG_DEBUG)
dz.log('eInPrv = ' .. tostring(dz.data.eInPrv), dz.LOG_DEBUG)
dz.log('eOutNow = ' .. tostring(dz.data.eOutNow), dz.LOG_DEBUG)
dz.log('All persistent variables have been reinitilized.', dz.LOG_STATUS)
return
end -- ReinitPersisVar
-- end functions
-- MAIN CODE BLOCK
return {
-- active
active = {
true, -- either true or false, or you can specify a function returning true or false
},
-- trigger
on = {
-- device triggers
devices = { eOutIdx, -- triggered by the eOut device
},
customEvents = {
copCustomEvent,
}
},
-- persistent data
data = {
counter = { initial = 10 }, -- counter to keep track of samples between cop calculation
eOutPrv = { initial = 0 }, -- to store the eOut for use in the next script run
eInPrv = { initial = 0 }, -- to store the eIn for use in the next script run
eOutNow = {initial = 0 }, -- to store new eOut value during script calculating COP run
}, -- end of persistent data section
-- logging
logging = {
-- level = domoticz.LOG_DEBUG, -- use LOG_DEBUG if you want more logging, LOG_INFO as default is OK
level = logLevel,
marker = logMarker
},
-- end of logging section
-- actual event code
execute = function(dz, item)
-- for testing only: reset all persistent vars back to initial values
if initializeOnly then
reinitPersisVar(dz)
return -- exit the script here
end
-- end testing
-- some shortcuts here
counter = dz.data.counter
eOutPrv = dz.data.eOutPrv
eInPrv = dz.data.eInPrv
eOutNow = dz.data.eOutNow
-- end shortcuts
dz.log('Remaining sample counter: ' .. tostring (counter),dz.LOG_DEBUG)
-- address one of these cases only (high level):
-- (1) get start values eOut and eIn if not available
-- (2) calculate COP if collected enough samples
-- (3) otherwise decrement the samples counter
--
-- (1)
if ( eOutPrv == 0 or eInPrv == 0 ) then -- if not set, get the first eOut and eIn values ever
dz.log('Persistent variables eOutPrv and/or eInPrv are 0, get start values first', dz.LOG_DEBUG)
if item.isDevice then -- if triggered by device update
eOutNow = dz.devices(eOutIdx).WhTotal -- get current value of Dz device for eOut
dz.data.eOutPrv = eOutNow -- store the first value in persistent variable eInPrv
dz.log('Retrieved current eOut (' .. tostring(eOutNow) .. ' [W]) and stored in persistent variable "eOutPrv"', dz.LOG_DEBUG)
dz.emitEvent(copCustomEvent,'some tbd data').afterSec(delay) -- can delete data part unless you find practical use for it
dz.log('Custom Event "' .. copCustomEvent .. '" will be raised afer ' .. tostring(delay) .. ' seconds', dz.LOG_DEBUG)
end -- from if triggered by item.isDevice (device update)
if item.isCustomEvent then -- if triggered by CustomEvent
-- log will mention what triggered this script run, no need to add to log again
eInNow = dz.devices(eInIdx).WhTotal -- get curreny value of Dz device for eIn
dz.data.eInPrv = eInNow -- store the first value in persistent variable eInPrv
dz.log('Retrieved current eIn (' .. tostring(eInNow) .. ' [W]) and stored in persistent variable "eInPrv"', dz.LOG_DEBUG)
end -- from if triggered by item.isCustomEvent
-- end catching the need to initialize eOutPrv and eInPrv persistent variable
--
-- (2)
elseif counter <= 1 then -- get the last sample and calculate COP
-- calculate COP
dz.log('Get last eOut and eIn samples and calculate CoP',dz.LOG_DEBUG)
-- get first value (eOut)
if item.isDevice then -- if triggered by device update
eOutNow = dz.devices(eOutIdx).WhTotal -- get current value of Dz device for eOut
dz.data.eOutNow = eOutNow -- store the first value in persistent variable eOutNow
dz.log('Retrieved current eOut (' .. tostring(eOutNow) .. ' [W]) and stored this in a persistent variable "eOutNow"', dz.LOG_DEBUG)
dz.emitEvent(copCustomEvent,'some tbd data').afterSec(delay) -- can delete data part unless you find practical use for it
dz.log('Custom Event "' .. copCustomEvent .. '" will be triggered afer ' .. tostring(delay) .. ' seconds', dz.LOG_DEBUG)
return -- abort script, no reason to continue now
end -- from if triggered by item.isDevice (device update)
-- get second value (eIn)
if item.isCustomEvent then -- if triggered by CustomEvent
-- log will mention what triggered this script run, no need to add that to log again
eInNow = dz.devices(eInIdx).WhTotal -- get curreny value of Dz device for eIn
dz.log('Retrieved current eIn (' .. tostring(eInNow) .. ' [W])', dz.LOG_DEBUG) -- no need to store it in a persistent variable, will be used directly
end -- from if triggered by item.isCustomEvent
--
-- do the math
eOutDelta = eOutNow - eOutPrv -- energy delivered during the time determined by the counter
eInDelta = eInNow - eInPrv -- energy used during the time determined by the counter
dz.log('eOutDelta = ' .. tostring(eOutNow) .. ' - ' .. tostring(eOutPrv) .. ' = ' .. tostring(eOutDelta) .. ' [W]', dz.LOG_DEBUG)
dz.log('eInDelta = ' .. tostring(eInNow) .. ' - ' .. tostring(eInPrv) .. ' = ' .. tostring(eInDelta) .. ' [W]', dz.LOG_DEBUG)
-- check for no energy used, in which case reporting a COP does not make sense (and would lead ot divide by 0)
if eInDelta == 0 then -- no energy used
dz.log('eInDelta = ' .. tostring(eInNow) .. ' - ' .. tostring(eInPrv) .. ' = ' .. tostring(eInDelta) .. ' [W] => no energy used, COP not updated',dz.LOG_STATUS)
else -- report COP and send update to device
cop = ( eOutDelta + eInDelta ) / eInDelta
dz.log('Calculated COP = ' .. tostring(cop), dz.LOG_STATUS)
cop = dz.utils.round(cop,3) -- round cop to 3 decimals, looks better on the device
dz.devices(copSensorIdx).updateCustomSensor(cop)
end
--
-- some further testing to find out what causes a negative COP
-- noted that eOutDelta can be negative, perhaps when heat pump is de-icing?
if (eOutDelta < 0) or (eInDelta < 0) then
dz.log('One of these was negative: eOutDelta = ' .. tostring(eOutDelta) .. ' eInDelta = ' .. tostring(eInDelta), dz.LOG_ERROR)
end
--
-- update previous values stored in persistent variables
dz.data.eOutPrv = eOutNow
dz.data.eInPrv = eInNow
dz.log('Updated the values stored in persistent data: eOut: ' .. tostring(dz.data.eOutPrv) .. ' [W] and eIn: ' .. tostring(dz.data.eInPrv) .. ' [W]', dz.LOG_DEBUG)
--
-- reset the counter
dz.data.counter = initCounter
--
--
-- (3)
else -- not the time yet to calculate COP
dz.data.counter = dz.data.counter - 1 -- decrement counter
dz.log('Earlier stored persistent values for eOut: ' .. tostring(eOutPrv) .. ' [W] and eIn: ' .. tostring(eInPrv) .. ' [W]', dz.LOG_DEBUG)
end -- from processing the cases
end
-- end actual event code
}
-- END MAIN CODE BLOCK