Practical way to script COP calculation?

Easy to use, 100% Lua-based event scripting framework.

Moderator: leecollings

Post Reply
User avatar
Domoberry
Posts: 121
Joined: Tuesday 30 May 2017 19:00
Target OS: Raspberry Pi / ODroid
Domoticz version: 2024.7
Contact:

Practical way to script COP calculation?

Post by Domoberry »

Hi Team,
As a next exercise, I want to create a script that shows the COP of my heatpump in a Domoticz device. The COP is not readily available from my heatpump interface. The related alternative input data is 'meter heating' and 'energy heating', These are total/accumulating values, not daily, monthly or alike. The COP can (likely) be calculated using this input data. So, it should be straightforward to create a script that shows this calculated COP. However, I would (also) like to see the COP 'in the day/week/month/year' or similar. The 'cumulative COP' will even out to some value and not give any information on 'how the system worked last day/week/...".
Question: what is the most practical way to do that? Would I need to use persistent variable with the 'History' feature? Is there a specific device supporting this?
User avatar
waltervl
Posts: 5842
Joined: Monday 28 January 2019 18:48
Target OS: Linux
Domoticz version: 2024.7
Location: NL
Contact:

Re: Practical way to script COP calculation?

Post by waltervl »

Put it in a custom sensor https://wiki.domoticz.com/Dummy_for_vir ... tom_Sensor

You probably have one in your environment so you can see how the data looks like.
Domoticz running on Udoo X86 (on Ubuntu)
Devices/plugins: ZigbeeforDomoticz (with Xiaomi, Ikea, Tuya devices), Nefit Easy, Midea Airco, Omnik Solar, Goodwe Solar
HvdW
Posts: 612
Joined: Sunday 01 November 2015 22:45
Target OS: Raspberry Pi / ODroid
Domoticz version: 2023.2
Location: Twente
Contact:

Re: Practical way to script COP calculation?

Post by HvdW »

I asked our new friend Deep Seek for a solution
Here is Deep's script. You can translate the Durch for yourself if needed.

Code: Select all

Hier is een **dzVents-script** dat de gegevens verzamelt wanneer de airco wordt uitgeschakeld, deze gegevens opslaat in een bestand, en aan het einde van de maand de COP (Coefficient of Performance) berekent over de afgelopen maand. Het script maakt gebruik van de volgende aannames:
- Het stroomverbruik van de airco wordt bijgehouden door een device genaamd **Airco**.
- De buitentemperatuur wordt bijgehouden door een device genaamd **Buitentemperatuur**.
- De gewenste binnentemperatuur is **21°C**.

### Script:
```lua
-- Script om COP van de airco te berekenen en gegevens op te slaan
return {
    on = {
        devices = {
            'Airco' -- Luister naar het Airco-device
        }
    },
    data = {
        aircoData = { history = {} } -- Opslag voor historische gegevens
    },
    execute = function(domoticz, device)
        local airco = domoticz.devices('Airco')
        local buitenTemp = domoticz.devices('Buitentemperatuur')
        local gewensteTemp = 21 -- Gewenste binnentemperatuur

        -- Controleer of de airco is uitgeschakeld
        if airco.state == 'Off' then
            -- Verzamel gegevens
            local stroomVerbruik = airco.usage -- Stroomverbruik in kWh
            local buitenTemperatuur = buitenTemp.temperature -- Buitentemperatuur in °C

            -- Bereken de warmte-uitwisseling (Q)
            -- Q = stroomVerbruik * COP (schatting, COP wordt later berekend)
            -- Voor nu slaan we alleen de ruwe gegevens op
            local gegevens = {
                tijdstip = domoticz.time.rawTime, -- Tijdstip van uitschakelen
                stroomVerbruik = stroomVerbruik,
                buitenTemperatuur = buitenTemperatuur,
                gewensteTemp = gewensteTemp
            }

            -- Voeg gegevens toe aan de geschiedenis
            table.insert(domoticz.data.aircoData.history, gegevens)

            -- Schrijf gegevens naar een bestand (optioneel)
            local bestandsNaam = 'airco_data.txt'
            local bestand = io.open(bestandsNaam, 'a') -- Open het bestand in append-modus
            if bestand then
                bestand:write('Tijdstip: ' .. gegevens.tijdstip .. ', ')
                bestand:write('Stroomverbruik: ' .. gegevens.stroomVerbruik .. ' kWh, ')
                bestand:write('Buitentemperatuur: ' .. gegevens.buitenTemperatuur .. ' °C\n')
                bestand:close()
            else
                domoticz.log('Kan bestand niet openen om gegevens op te slaan', domoticz.LOG_ERROR)
            end
        end

        -- Aan het einde van de maand de COP berekenen
        if domoticz.time.day == 1 and domoticz.time.hour == 0 and domoticz.time.minute == 0 then
            local totaalStroomVerbruik = 0
            local totaalWarmteUitwisseling = 0

            -- Loop door de geschiedenis om totale waarden te berekenen
            for _, gegevens in ipairs(domoticz.data.aircoData.history) do
                totaalStroomVerbruik = totaalStroomVerbruik + gegevens.stroomVerbruik
                -- Schat de warmte-uitwisseling (Q) op basis van temperatuurverschil
                local deltaT = gegevens.buitenTemperatuur - gegevens.geweneTemp
                totaalWarmteUitwisseling = totaalWarmteUitwisseling + (gegevens.stroomVerbruik * deltaT)
            end

            -- Bereken de COP
            local COP = totaalWarmteUitwisseling / totaalStroomVerbruik

            -- Log de COP
            domoticz.log('COP over de afgelopen maand: ' .. COP, domoticz.LOG_INFO)

            -- Wis de geschiedenis voor de nieuwe maand
            domoticz.data.aircoData.history = {}
        end
    end
}
```

---

### Uitleg van het script:
1. **Trigger:**
   - Het script wordt geactiveerd wanneer de status van het **Airco**-device verandert.
   - Het controleert of de airco is uitgeschakeld (`state == 'Off'`).

2. **Gegevens verzamelen:**
   - Het script verzamelt het stroomverbruik van de airco en de buitentemperatuur op het moment van uitschakelen.
   - Deze gegevens worden opgeslagen in een tabel (`domoticz.data.aircoData.history`).

3. **Opslaan in een bestand:**
   - De gegevens worden ook weggeschreven naar een tekstbestand (`airco_data.txt`) voor langdurige opslag.

4. **COP berekenen aan het einde van de maand:**
   - Op de eerste dag van de maand om middernacht wordt de COP berekend over de afgelopen maand.
   - Het script berekent het totale stroomverbruik en de geschatte warmte-uitwisseling.
   - De COP wordt berekend als de verhouding tussen de totale warmte-uitwisseling en het totale stroomverbruik.

5. **Geschiedenis wissen:**
   - Na het berekenen van de COP wordt de geschiedenis gewist om klaar te zijn voor de nieuwe maand.

---

### Opmerkingen:
- **Schatting van warmte-uitwisseling (Q):** Het script schat de warmte-uitwisseling op basis van het temperatuurverschil tussen binnen en buiten. Dit is een vereenvoudiging en kan worden verfijnd als je meer gegevens hebt (bijv. luchtdebiet).
- **Bestandsopslag:** Het script schrijft gegevens naar een tekstbestand. Zorg ervoor dat Domoticz toegang heeft tot het bestandssysteem.
- **Testen:** Test het script grondig om ervoor te zorgen dat het correct werkt in jouw Domoticz-omgeving.

Laat het weten als je verdere aanpassingen nodig hebt! 😊
Bugs bug me.
User avatar
waltervl
Posts: 5842
Joined: Monday 28 January 2019 18:48
Target OS: Linux
Domoticz version: 2024.7
Location: NL
Contact:

Re: Practical way to script COP calculation?

Post by waltervl »

When airco is off measure energy usage of airco?
Trigger is only on airco device not on time. So monthly COP calculation will most likely be shipped. Or you send data at least every minute to the airco device.....

The estimated heat transfer calculations as base for the COP seems to be a joke, at least I do not understand it.
Domoticz running on Udoo X86 (on Ubuntu)
Devices/plugins: ZigbeeforDomoticz (with Xiaomi, Ikea, Tuya devices), Nefit Easy, Midea Airco, Omnik Solar, Goodwe Solar
User avatar
Domoberry
Posts: 121
Joined: Tuesday 30 May 2017 19:00
Target OS: Raspberry Pi / ODroid
Domoticz version: 2024.7
Contact:

Re: Practical way to script COP calculation?

Post by Domoberry »

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
Last edited by Domoberry on Monday 03 February 2025 18:07, edited 4 times in total.
BazemanKM
Posts: 35
Joined: Wednesday 22 July 2015 21:39
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Practical way to script COP calculation?

Post by BazemanKM »

Code: Select all

return {
    on = {
        timer = { 'every minute'}
    },
    
    logging = 
    {
        --level = domoticz.LOG_DEBUG, -- for debugging
        --level = domoticz.LOG_INFO,
        marker = "COP berekening"
    },
    execute = function(dz)
        local verbruik = dz.devices(38).WhToday
        local opgewekt = dz.devices(39).WhToday
        local Cop = dz.utils.round(opgewekt / verbruik,2)
--      dz.log ('Verbruik is  ' ..  verbruik)
--      dz.log ('Opgewekt is  ' ..  opgewekt)
        dz.log ('COP is  '  ..  Cop)
        dz.devices(213).updateCustomSensor(Cop)
    end
}

User avatar
Domoberry
Posts: 121
Joined: Tuesday 30 May 2017 19:00
Target OS: Raspberry Pi / ODroid
Domoticz version: 2024.7
Contact:

Re: Practical way to script COP calculation?

Post by Domoberry »

That is in essence the same!
Based on what I found using Google (links in the earlier post), I'm using CoP = (energy-out + energy-in)/(energy-in), e.g. exactly 1 more than above. I understood this is because I'm looking at a CoP for heating, not cooling. However, I'm not an expert! Also, I think you need to consider the energy in/out over a certain time period, yet I do not know how much time is optimal. In the attached example, Energie-out/in are calculated over 10 mins (my heat pump provides the accumulated energy out/in every minute), yet the result is still quite 'noisy', so I probably need a longer period. Lastly, if the heat pump is not running, energy-in is obviously zero (divide by zero), so I capture that (by not giving an update on CoP in that case). Also noted that in some cases energy-out can be negative, which is probably when the system is deicing. Even to the extend that the CoP itself becomes negative. Unfortunately, the position of the 4-way valve in the heat pump (e.g. heating or cooling/deicing) seems not to be exposed via the EMS-ESP gateway.
Note that the Domoticz graph takes the 10-minute CoP updates and interpolates every 5 minutes for the graph. Perhaps I should rather use a bar-graph.
first few days of CoP tracking
first few days of CoP tracking
250203 CoP graph last 7 days.png (142.51 KiB) Viewed 1047 times
(the script code in my earlier is updated to what I'm currently testing with)
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest