With the ideas and setup of myself and the help of Claude I present you my EV Consumption script.
What is needed
- a text sensor named EV Consumption Facts (Facts added to avoid naming problems within Domoticz)
- a switch sensor type doorbell with the name Reset EV Data
- you will have to create a file EV_Consumption.csv to start with in your /home/pi directory
- the contens of the file are these
Code: Select all
timeStamp;odometer;batteryLevel;deltaDistance;kWhTotal;deltakWh;kWh100km;euro;a>
01-01-2025 01:00;15513;;;2562.958;;;;
and the script
Code: Select all
-- Data is written to file and to textSensor
-- file: EV_Consumption.csv, textSensor EV Consumption
-- monthly data are saved to file at the end of each Month
-- it enables you to depict the relationship between EV Consumption and temperature
-- Constants
local CONSTANTS = {
BATTERY_CAPACITY = 79, -- kWh
TARGET_BATTERY_PERCENTAGE = 82, -- Target percentage for normalization
PRICE_PER_KWH = 0.25, -- euro/kWh
CSV_FILE_PATH = "/home/pi/EV_Consumption.csv", -- Path to the CSV file
MAX_TEMP_ARRAY_SIZE = 1440 -- Max number of temperature measurements (1 day with 1 measurement per minute)
}
-- Helper functions
local function spaces(count)
return string.rep(" ", count)
end
local function round2Decimals(number)
if number == nil then
return 0
end
return math.floor(number * 100 + 0.5) / 100
end
-- Check if today is the last day of the month
local function isLastDayOfMonth()
local today = os.date("*t")
local tomorrow = os.date("*t", os.time() + 24 * 60 * 60)
return today.month ~= tomorrow.month
end
-- Format display text for the text device
local function formatDisplayText(domoticz, data, currentMonth)
-- Ensure domoticz.log is available
if domoticz and domoticz.log then
-- Log all data for debugging
domoticz.log('Last Update: ' .. (data.lastUpdate or "N/A"), domoticz.LOG_DEBUG)
domoticz.log('Current Odometer: ' .. (tonumber(data.currentOdometer) or 0) .. ' km', domoticz.LOG_DEBUG)
domoticz.log('Battery Level: ' .. (tonumber(data.batteryLevel) or 0) .. ' %', domoticz.LOG_DEBUG)
domoticz.log('Distance ' .. currentMonth .. ': ' .. (tonumber(data.distance) or 0) .. ' km', domoticz.LOG_DEBUG)
domoticz.log('kWh ' .. currentMonth .. ': ' .. (tonumber(data.kWhPartial) or 0) .. ' kWh', domoticz.LOG_DEBUG)
domoticz.log('Costs ' .. currentMonth .. ': ' .. (tonumber(data.euro) or 0) .. ' €', domoticz.LOG_DEBUG)
domoticz.log('Average Temp: ' .. (tonumber(data.avgTemp) or 0) .. '°C', domoticz.LOG_DEBUG)
domoticz.log('EV Consumption: ' .. (tonumber(data.evConsumption) or 0) .. ' kWh/100km', domoticz.LOG_DEBUG)
domoticz.log('Battery level normalized to ' .. CONSTANTS.TARGET_BATTERY_PERCENTAGE .. '%', domoticz.LOG_DEBUG)
domoticz.log('Original kWh loaded: ' .. (tonumber(data.kWhPartial) or 0) .. ' kWh', domoticz.LOG_DEBUG)
domoticz.log('Normalized kWh: ' .. (tonumber(data.normalizedKWh) or 0) .. ' kWh', domoticz.LOG_DEBUG)
else
-- Provide a clearer error message
local errorMsg = 'Er is een fout opgetreden in local function formatDisplayText: '
if not domoticz then
errorMsg = errorMsg .. 'domoticz is nil. '
end
if domoticz and not domoticz.log then
errorMsg = errorMsg .. 'domoticz.log is nil.'
end
domoticz.log(errorMsg, domoticz.LOG_ERROR)
end
return string.format(
"Last Update : %s\n" ..
"Current Odometer : %d km\n" ..
"Battery Level : %d %% \n" ..
"Distance %s : %d km\n" ..
"kWh %s : %.2f kWh\n" ..
spaces(15) .. "Costs %s : %.2f €\n" ..
spaces(15) .. "Average Temp : %.1f°C\n" ..
spaces(15) .. "EV Consumption : %.2f kWh/100km\n" ..
spaces(15) .. "Battery level normalized to %d%%\n" ..
spaces(15) .. "Original kWh loaded: %.2f kWh\n" ..
spaces(15) .. "Normalized kWh   : %.2f kWh\n",
data.lastUpdate or "N/A",
tonumber(data.currentOdometer) or 0,
tonumber(data.batteryLevel) or 0,
currentMonth, tonumber(data.distance) or 0,
currentMonth, tonumber(data.kWhPartial) or 0,
currentMonth, tonumber(data.euro) or 0,
tonumber(data.avgTemp) or 0,
tonumber(data.evConsumption) or 0,
CONSTANTS.TARGET_BATTERY_PERCENTAGE,
tonumber(data.kWhPartial) or 0,
tonumber(data.normalizedKWh) or 0
)
end
-- Update the text device with the latest data
local function updateDisplay(domoticz, displayText)
local displayDevice = domoticz.devices('EV Consumption Facts')
if displayDevice then
displayDevice.updateText(displayText)
domoticz.log('Text sensor updated with new data', domoticz.LOG_DEBUG)
else
domoticz.log('Text sensor "EV Consumption Facts" not found', domoticz.LOG_ERROR)
end
end
-- Normalize battery consumption to target percentage
local function normalizeToTargetLevel(domoticz, currentLevel, kWhUsed)
local kWhPerPercent = CONSTANTS.BATTERY_CAPACITY / 100
local difference = CONSTANTS.TARGET_BATTERY_PERCENTAGE - currentLevel
local kWhAdjustment = difference * kWhPerPercent
local normalizedKWh = kWhUsed + kWhAdjustment
domoticz.log('Battery normalization calculation:', domoticz.LOG_DEBUG)
domoticz.log(string.format('Current level: %.1f%%', currentLevel), domoticz.LOG_DEBUG)
domoticz.log(string.format('Difference to target: %.1f%%', difference), domoticz.LOG_DEBUG)
domoticz.log(string.format('kWh adjustment: %.2f kWh', kWhAdjustment), domoticz.LOG_DEBUG)
domoticz.log(string.format('Original kWh: %.2f, Normalized kWh: %.2f', kWhUsed, normalizedKWh), domoticz.LOG_DEBUG)
return normalizedKWh
end
-- Save data to CSV file
local function saveToCSV(data)
local file = io.open(CONSTANTS.CSV_FILE_PATH, "a")
if not file then return false end
local fields = {
os.date('%d-%m-%Y %H:%M'),
math.floor(data.currentOdometer or 0),
math.floor(data.batteryLevel or 0),
math.floor(data.distance or 0),
round2Decimals(data.kWhTotal),
round2Decimals(data.kWhPartial),
round2Decimals(data.evConsumption),
round2Decimals(data.euro),
round2Decimals(data.avgTemp)
}
local csvLine = table.concat(fields, ";") .. "\n"
file:write(csvLine)
file:close()
return true
end
-- Load data from CSV file
local function loadDataFromCSV()
local file = io.open(CONSTANTS.CSV_FILE_PATH, "r")
if not file then return nil end
-- Skip the header line
file:read("*l")
local lastLine
for line in file:lines() do
lastLine = line
end
file:close()
if not lastLine then return nil end
local fields = {}
for field in string.gmatch(lastLine, "([^;]+)") do
table.insert(fields, field)
end
return {
lastUpdate = fields[1],
beginningOfTheMonthOdometer = tonumber(fields[2]),
beginningOfTheMonthBatteryLevel = tonumber(fields[3]),
beginningOfTheMonthkWhTotal = tonumber(fields[5]),
avgTemp = tonumber(fields[9])
}
end
-- Process EV data and update display
local function processEVData(domoticz, sensorData, csvData)
local kWhPartial = sensorData.currentkWhTotal - csvData.beginningOfTheMonthkWhTotal
local euro = kWhPartial * CONSTANTS.PRICE_PER_KWH
local distance = sensorData.currentOdometer - csvData.beginningOfTheMonthOdometer
local normalizedKWh = normalizeToTargetLevel(domoticz, sensorData.batteryLevel, kWhPartial)
local evConsumption = (distance > 0) and ((normalizedKWh / distance) * 100) or 0
-- Log for debugging
domoticz.log('kWhPartial: ' .. kWhPartial, domoticz.LOG_DEBUG)
domoticz.log('euro: ' .. euro, domoticz.LOG_DEBUG)
domoticz.log('distance: ' .. distance, domoticz.LOG_DEBUG)
domoticz.log('normalizedKWh: ' .. normalizedKWh, domoticz.LOG_DEBUG)
domoticz.log('evConsumption: ' .. evConsumption, domoticz.LOG_DEBUG)
-- Prepare display data
local displayData = {
currentOdometer = sensorData.currentOdometer,
batteryLevel = sensorData.batteryLevel,
distance = distance,
kWhPartial = kWhPartial,
kWhTotal = sensorData.currentkWhTotal,
euro = euro,
avgTemp = sensorData.temperature,
evConsumption = evConsumption,
normalizedKWh = normalizedKWh,
lastUpdate = os.date('%Y-%m-%d %H:%M:%S')
}
-- Update display
local currentMonth = os.date('%B')
local displayText = formatDisplayText(displayData, currentMonth)
updateDisplay(domoticz, displayText)
end
-- Main script
return {
on = {
devices = {'Charging Level', 'Reset EV Data'},
timer = {'at 23:56'}
},
logging = {
level = domoticz.LOG_DEBUG,
marker = "----- EV Consumption -----"
},
data = {
previousSelectorLevel = { initial = 0 }
},
execute = function(domoticz, device)
-- Load data from CSV
local csvData = loadDataFromCSV()
if not csvData then
domoticz.log('Failed to load data from CSV', domoticz.LOG_ERROR)
return
end
-- Read current sensor values
local sensorData = {
selectorLevel = domoticz.devices('Charging Level').levelVal,
batteryLevel = domoticz.devices('XC40-ChargeLevel').nValue,
currentOdometer = domoticz.devices('Volvo-Odometer').nValue,
currentkWhTotal = domoticz.devices('Laadpaal').WhTotal / 1000,
temperature = tonumber(domoticz.devices('Buitentemperatuur').sValue)
}
-- Check if Charging Level changed from > 0 to 0
if sensorData.selectorLevel == 0 and domoticz.data.previousSelectorLevel > 0 then
-- Process data and update display
processEVData(domoticz, sensorData, csvData)
end
-- Update previousSelectorLevel
domoticz.data.previousSelectorLevel = sensorData.selectorLevel
-- Save data to CSV only on the last day of the month at 23:56
if isLastDayOfMonth() and device.isTimer then
local displayData = {
currentOdometer = sensorData.currentOdometer,
batteryLevel = sensorData.batteryLevel,
distance = sensorData.currentOdometer - csvData.beginningOfTheMonthOdometer,
kWhPartial = sensorData.currentkWhTotal - csvData.beginningOfTheMonthkWhTotal,
kWhTotal = sensorData.currentkWhTotal,
euro = (sensorData.currentkWhTotal - csvData.beginningOfTheMonthkWhTotal) * CONSTANTS.PRICE_PER_KWH,
avgTemp = sensorData.temperature,
evConsumption = (sensorData.currentOdometer - csvData.beginningOfTheMonthOdometer > 0) and (((sensorData.currentkWhTotal - csvData.beginningOfTheMonthkWhTotal) / (sensorData.currentOdometer - csvData.beginningOfTheMonthOdometer)) * 100) or 0,
normalizedKWh = normalizeToTargetLevel(domoticz, sensorData.batteryLevel, sensorData.currentkWhTotal - csvData.beginningOfTheMonthkWhTotal),
lastUpdate = os.date('%Y-%m-%d %H:%M:%S')
}
saveToCSV(displayData)
end
end
}
EDIT 08-02-2025
I have done some cleaning up in the file and added Normalised kWh.
When charging state ~= TARGET_BATTERY_PERCENTAGE, partialkWh will be calculated to TARGET_BATTERY_PERCENTAGE.
In this way cuurent EV Consumtion is quite accurate.
The length of the script has shrunk from 404 to 256 lines
Here are the data that are displayed:
Current Odometer: 17349 km
Battery Level: 56 %
Distance February: 99 km
kWh February: 5.85 kWh
Costs February: 1.46 €
Average Temp: 2.8°C
EV Consumption: 26.66 kWh/100km
Battery level normalized to 82%
Original kWh loaded: 5.85 kWh
Normalized kWh: 26.39 kWh