Python Plugin: Volvo EV

Python and python framework

Moderator: leecollings

HvdW
Posts: 539
Joined: Sunday 01 November 2015 22:45
Target OS: Raspberry Pi / ODroid
Domoticz version: 2023.2
Location: Twente
Contact:

Re: Python Plugin: Volvo EV

Post by HvdW »

@akamming
I just tried the Volvo.lua script found within the plugin directory

- missing availabilityStatusDevice='Volvo-availabilityStatus' in the plugin
- log showing error: 2025-01-07 00:04:28.116 Error: EventSystem: Lua script Volvo warning did not return a commandArray

is this still experimental?
Bugs bug me.
akamming
Posts: 344
Joined: Friday 17 August 2018 14:03
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python Plugin: Volvo EV

Post by akamming »

impossible to troubleshoot if you don't post the logging. First Guess is that you renamed the volvo availability status device and did not change the script accordingly

I did notice that after a recent change i also neede to change the LUA, but forgot to checkin in github. So i just did.

It is now a working script (but just a sample. Adjust to your own needs). Make sure the device numbers or names match in the top of the script match your configuration, otherwise it will not work ofcourse..

Better to ask help then in the dzvents section of this forum
HvdW
Posts: 539
Joined: Sunday 01 November 2015 22:45
Target OS: Raspberry Pi / ODroid
Domoticz version: 2023.2
Location: Twente
Contact:

Re: Python Plugin: Volvo EV

Post by HvdW »

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
Bugs bug me.
HvdW
Posts: 539
Joined: Sunday 01 November 2015 22:45
Target OS: Raspberry Pi / ODroid
Domoticz version: 2023.2
Location: Twente
Contact:

Re: Python Plugin: Volvo EV

Post by HvdW »

Any idea what this is supposed to mean?
2025-02-13 13:10:10.201 Error: Volvo hardware (16) thread seems to have ended unexpectedly
2025-02-13 13:10:24.204 Error: Volvo hardware (16) thread seems to have ended unexpectedly
2025-02-13 13:10:38.207 Error: Volvo hardware (16) thread seems to have ended unexpectedly
2025-02-13 13:10:52.210 Error: Volvo hardware (16) thread seems to have ended unexpectedly
I deactivated and reactivated the plugin, no difference.
Bugs bug me.
akamming
Posts: 344
Joined: Friday 17 August 2018 14:03
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Python Plugin: Volvo EV

Post by akamming »

HvdW wrote: Thursday 13 February 2025 13:11 Any idea what this is supposed to mean?
2025-02-13 13:10:10.201 Error: Volvo hardware (16) thread seems to have ended unexpectedly
2025-02-13 13:10:24.204 Error: Volvo hardware (16) thread seems to have ended unexpectedly
2025-02-13 13:10:38.207 Error: Volvo hardware (16) thread seems to have ended unexpectedly
2025-02-13 13:10:52.210 Error: Volvo hardware (16) thread seems to have ended unexpectedly
I deactivated and reactivated the plugin, no difference.
This error can occur if the Volvo API takes too lojng to respond. Will probably resolve itself
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest