Import Autarco solar panel data from web api Topic is solved

Moderator: leecollings

Post Reply
HappyNerd
Posts: 11
Joined: Wednesday 25 May 2022 10:10
Target OS: NAS (Synology & others)
Domoticz version: 2025.2
Location: Netherlands
Contact:

Import Autarco solar panel data from web api

Post by HappyNerd »

I am trying to import data from my autarco solar panels (through a web api from the autarco website) into domoticz. I created the virtual sensor and a lua script, but it keeps returning errors. It is driving me crazy. Any help is appreciated.

The script that I am using

Code: Select all

commandArray = {}

-- API credentials and URL
local username = '[email protected]'
local password = 'myactualpassword'
local site_id = 'myactualsiteID'
local api_url_power = 'https://my.autarco.com/api/m1/site/' .. site_id .. '/power'

-- Function to fetch data from API
function fetch_data(url)
    local handle = io.popen("curl -s -u ' .. username .. ':' .. password .. ' ' .. api_url_power ..'")
    local result = handle:read("*a")
    handle:close()
    return result
end

-- Fetch and parse power data
local power_data = fetch_data(api_url_power)
local power_json = json.decode(power_data)
local pv_now = power_json.stats.kpis.pv_now / 1000 -- Convert to kW

-- Update Domoticz virtual sensors
commandArray['UpdateDevice'] = 'myactualidx|0|' .. pv_now

return commandArray

The error message:

Code: Select all

EventSystem: in SpeelScript: [string "commandArray = {} ..."]:20: attempt to index a nil value (global 'json')
Things I already checked
The virtual domoticz device is available. It should work.
Furthermore I tested the API manually:
When I simply type in

Code: Select all

my.autarco.com/api/m1/site/myactualsiteID/power
into my browser, it asks me to logon and then the website provides a neatly formatted output:

Code: Select all

{
    "dt_config_changed": "202x-0x-10T12:41:06+00:00",
    "inverters": {
        "myactualinverterid01": {
            "sn": "myactualserialnumber01",
            "dt_latest_msg": "2024-07-06T18:38:59+00:00",
            "out_ac_power": 190,
            "out_ac_energy_total": 5506,
            "error": null,
            "grid_turned_off": false,
            "health": "OK"
        },
        "myactualinverterid02": {
            "sn": "myactualserialnumber02",
            "dt_latest_msg": "2024-07-06T11:22:46+00:00",
            "out_ac_power": 940,
            "out_ac_energy_total": 3156,
            "error": null,
            "grid_turned_off": false,
            "health": "OK"
        }
    },
    "stats": {
        "graphs": {
            "pv_power": {
                "myactualinverterid01": {
                    "2024-07-06 00:00:00": 0,
                    "2024-07-06 01:00:00": 0,
                    "2024-07-06 02:00:00": 0,
                    "2024-07-06 03:00:00": 0,
                    "2024-07-06 04:00:00": 0,
                    "2024-07-06 05:00:00": 0,
                    "2024-07-06 06:00:00": 60,
                    "2024-07-06 07:00:00": 827,
                    "2024-07-06 08:00:00": 460,
                    "2024-07-06 09:00:00": 687,
                    "2024-07-06 10:00:00": 780,
                    "2024-07-06 11:00:00": 883,
                    "2024-07-06 12:00:00": 713,
                    "2024-07-06 13:00:00": 1427,
                    "2024-07-06 14:00:00": 2320,
                    "2024-07-06 15:00:00": 2700,
                    "2024-07-06 16:00:00": 1273,
                    "2024-07-06 17:00:00": 820,
                    "2024-07-06 18:00:00": 387,
                    "2024-07-06 19:00:00": 190,
                    "2024-07-06 20:00:00": 120,
                     "2024-07-06 20:30:00": 165
                },
                "myactualinverterid02": {
                    "2024-07-06 00:00:00": 0,
                    "2024-07-06 01:00:00": 0,
                    "2024-07-06 02:00:00": 0,
                    "2024-07-06 03:00:00": 0,
                    "2024-07-06 04:00:00": 0,
                    "2024-07-06 05:00:00": 0,
                    "2024-07-06 06:00:00": 25,
                    "2024-07-06 07:00:00": 167,
                    "2024-07-06 08:00:00": 245,
                    "2024-07-06 09:00:00": 390,
                    "2024-07-06 10:00:00": 470,
                    "2024-07-06 11:00:00": 520,
                    "2024-07-06 12:00:00": 460,
                    "2024-07-06 13:00:00": 823,
                    "2024-07-06 14:00:00": 0,
                    "2024-07-06 15:00:00": 0,
                    "2024-07-06 16:00:00": 0,
                    "2024-07-06 17:00:00": 0,
                    "2024-07-06 18:00:00": 0,
                    "2024-07-06 19:00:00": 0,
                    "2024-07-06 20:00:00": 0,
                    "2024-07-06 20:15:00": 0,
                    "2024-07-06 20:30:00": null
                }
            },
            "no_comms": [
                {
                    "start": "2024-07-06 08:30:00",
                    "end": "2024-07-06 08:44:59"
                },
                {
                    "start": "2024-07-06 13:30:00",
                    "end": null
                }
            ]
        },
        "kpis": {
            "pv_now": 190
        }
    }
}
Also when I enter my domoticz docker and then manually use the curl command

Code: Select all

curl -s -u 'username':'password' https://my.autarco.com/api/m1/site/myactualsiteID/power
, the system returns output, albeit it that it seems to be more in a plain text format.

Code: Select all

{"dt_config_changed":"202x-05-10T12:41:06+00:00","inverters":{"myactualinverterid01":{"sn":"myactualserialnumber01","dt_latest_msg":"2024-07-06T18:53:59+00:00","out_ac_power":190,"out_ac_energy_total":5506,"error":null,"grid_turned_off":false,"health":"OK"},"myactualinverterid02":{"sn":"myactualserialnumber02","dt_latest_msg":"2024-07-06T11:22:46+00:00","out_ac_power":940,"out_ac_energy_total":3156,"error":null,"grid_turned_off":false,"health":"OK"}},"stats":{"graphs":{"pv_power":{"154812216300018":{"2024-07-06 00:00:00":0,"2024-07-06 00:15:00":0,"2024-07-06 00:30:00":0,"2024-07-06 00:45:00":0,"2024-07-06 01:00:00":0,"2024-07-06 01:15:00":0,"2024-07-06 01:30:00":0,"2024-07-06 01:45:00":0,"2024-07-06 02:00:00":0,"2024-07-06 02:15:00":0,"2024-07-06 02:30:00":0,"2024-07-06 02:45:00":0,"2024-07-06 03:00:00":0,"2024-07-06 03:15:00":0,"2024-07-06 03:30:00":0,"2024-07-06 03:45:00":0,"2024-07-06 04:00:00":0,"2024-07-06 04:15:00":0,"2024-07-06 04:30:00":0,"2024-07-06 04:45:00":0,"2024-07-06 05:00:00":0,"2024-07-06 05:15:00":0,"2024-07-06 05:30:00":0,"2024-07-06 05:45:00":20,"2024-07-06 06:00:00":60,"2024-07-06 06:15:00":113,"2024-07-06 06:30:00":240,"2024-07-06 06:45:00":563,"2024-07-06 07:00:00":827,"2024-07-06 07:15:00":533,"2024-07-06 07:30:00":407,"2024-07-06 07:45:00":347,"2024-07-06 08:00:00":460,"2024-07-06 08:15:00":547,"2024-07-06 08:30:00":603,"2024-07-06 08:45:00":600,"2024-07-06 09:00:00":687,"2024-07-06 09:15:00":647,"2024-07-06 09:30:00":737,"2024-07-06 09:45:00":840,"2024-07-06 10:00:00":780,"2024-07-06 10:15:00":523,"2024-07-06 10:30:00":223,"2024-07-06 10:45:00":523,"2024-07-06 11:00:00":883,"2024-07-06 11:15:00":693,"2024-07-06 11:30:00":513,"2024-07-06 11:45:00":393,"2024-07-06 12:00:00":713,"2024-07-06 12:15:00":763,"2024-07-06 12:30:00":1493,"2024-07-06 12:45:00":1413,"2024-07-06 13:00:00":1427,"2024-07-06 13:15:00":2010,"2024-07-06 13:30:00":1923,"2024-07-06 13:45:00":1493,"2024-07-06 14:00:00":2320,"2024-07-06 14:15:00":2710,"2024-07-06 14:30:00":2513,"2024-07-06 14:45:00":2733,"2024-07-06 15:00:00":2700,"2024-07-06 15:15:00":1513,"2024-07-06 15:30:00":2513,"2024-07-06 15:45:00":1393,"2024-07-06 16:00:00":1273,"2024-07-06 16:15:00":943,"2024-07-06 16:30:00":850,"2024-07-06 16:45:00":817,"2024-07-06 17:00:00":820,"2024-07-06 17:15:00":520,"2024-07-06 17:30:00":747,"2024-07-06 17:45:00":437,"2024-07-06 18:00:00":387,"2024-07-06 18:15:00":417,"2024-07-06 18:30:00":510,"2024-07-06 18:45:00":573,"2024-07-06 19:00:00":190,"2024-07-06 19:15:00":230,"2024-07-06 19:30:00":110,"2024-07-06 19:45:00":110,"2024-07-06 20:00:00":120,"2024-07-06 20:15:00":157,"2024-07-06 20:30:00":167,"2024-07-06 20:45:00":210},"1805030221110035":{"2024-07-06 00:00:00":0,"2024-07-06 00:15:00":0,"2024-07-06 00:30:00":0,"2024-07-06 00:45:00":0,"2024-07-06 01:00:00":0,"2024-07-06 01:15:00":0,"2024-07-06 01:30:00":0,"2024-07-06 01:45:00":0,"2024-07-06 02:00:00":0,"2024-07-06 02:15:00":0,"2024-07-06 02:30:00":0,"2024-07-06 02:45:00":0,"2024-07-06 03:00:00":0,"2024-07-06 03:15:00":0,"2024-07-06 03:30:00":0,"2024-07-06 03:45:00":0,"2024-07-06 04:00:00":0,"2024-07-06 04:15:00":0,"2024-07-06 04:30:00":0,"2024-07-06 04:45:00":0,"2024-07-06 05:00:00":0,"2024-07-06 05:15:00":0,"2024-07-06 05:30:00":0,"2024-07-06 05:45:00":0,"2024-07-06 06:00:00":25,"2024-07-06 06:15:00":30,"2024-07-06 06:30:00":67,"2024-07-06 06:45:00":100,"2024-07-06 07:00:00":167,"2024-07-06 07:15:00":177,"2024-07-06 07:30:00":177,"2024-07-06 07:45:00":170,"2024-07-06 08:00:00":245,"2024-07-06 08:15:00":295,"2024-07-06 08:30:00":0,"2024-07-06 08:45:00":330,"2024-07-06 09:00:00":390,"2024-07-06 09:15:00":390,"2024-07-06 09:30:00":430,"2024-07-06 09:45:00":528,"2024-07-06 10:00:00":470,"2024-07-06 10:15:00":313,"2024-07-06 10:30:00":153,"2024-07-06 10:45:00":267,"2024-07-06 11:00:00":520,"2024-07-06 11:15:00":323,"2024-07-06 11:30:00":410,"2024-07-06 11:45:00":157,"2024-07-06 12:00:00":460,"2024-07-06 12:15:00":410,"2024-07-06 12:30:00":1060,"2024-07-06 12:45:00":1040,"2024-07-06 13:00:00":823,"2024-07-06 13:15:00":1130,"2024-07-06 13:30:00":0,"2024-07-06 13:45:00":0,"2024-07-06 14:00:00":0,"2024-07-06 14:15:00":0,"2024-07-06 14:30:00":0,"2024-07-06 14:45:00":0,"2024-07-06 15:00:00":0,"2024-07-06 15:15:00":0,"2024-07-06 15:30:00":0,"2024-07-06 15:45:00":0,"2024-07-06 16:00:00":0,"2024-07-06 16:15:00":0,"2024-07-06 16:30:00":0,"2024-07-06 16:45:00":0,"2024-07-06 17:00:00":0,"2024-07-06 17:15:00":0,"2024-07-06 17:30:00":0,"2024-07-06 17:45:00":0,"2024-07-06 18:00:00":0,"2024-07-06 18:15:00":0,"2024-07-06 18:30:00":0,"2024-07-06 18:45:00":0,"2024-07-06 19:00:00":0,"2024-07-06 19:15:00":0,"2024-07-06 19:30:00":0,"2024-07-06 19:45:00":0,"2024-07-06 20:00:00":0,"2024-07-06 20:15:00":0,"2024-07-06 20:30:00":0,"2024-07-06 20:45:00":null}},"no_comms":[{"start":"2024-07-06 08:30:00","end":"2024-07-06 08:44:59"},{"start":"2024-07-06 13:30:00","end":null}]},"kpis":{"pv_now":190}}}

Anybody have any ideas?
User avatar
gizmocuz
Posts: 3025
Joined: Thursday 11 July 2013 18:59
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Location: Top of the world
Contact:

Re: Import Autarco solar panel data from web api

Post by gizmocuz »

Try not using LUA but use dzVents.
Quality outlives Quantity!
HappyNerd
Posts: 11
Joined: Wednesday 25 May 2022 10:10
Target OS: NAS (Synology & others)
Domoticz version: 2025.2
Location: Netherlands
Contact:

Re: Import Autarco solar panel data from web api

Post by HappyNerd »

Thx. I appreciate the help and I will follow your advice. I am quite new at this, so this may take a couple of days. I’ll post my progress.
HappyNerd
Posts: 11
Joined: Wednesday 25 May 2022 10:10
Target OS: NAS (Synology & others)
Domoticz version: 2025.2
Location: Netherlands
Contact:

Re: Import Autarco solar panel data from web api

Post by HappyNerd »

With a lot of help from ChatGPT and Copilot, I turned the script into a dzvents script and made some adjustments.

The good news
The script is definitely retrieving the data from the autarco website.
Also it is capable to identify the pv_now value that I require to update my virtual device.
And finally, the log doesn't show any errors

The bad news:
The script doesn't update my virtual device.
I am quite sure that it can be pinpointed to a single line of code, but I can't figure it out
It is driving me absolutely crazy :? . Any help is appreciated.

The suspected culprit

Code: Select all

commandArray['UpdateDevice'] = 'deviceIdx|0|pv_now'
Sensor characteristics:
I tried this with two types of sensors and both have the same problem: they are not updated.
Sensor 1: Type = "General", SubType = "custom sensor"
Sensor 2: Type = "Usage", SubType = "Electric"

The entire code

Code: Select all

-- API credentials and URL
local username = 'myactualusername'
local password = 'myactualpassword'
local site_id = 'myactualsite_id'
local api_url_power = 'https://my.autarco.com/api/m1/site/'.. site_id ..'/power'

-- DeviceID
local deviceIdx = 1234

-- Function to fetch data from API
function fetch_data(url)
    print("Fetching data from API...")  -- Troubleshooting line
    local handle = io.popen("curl -s -u '" .. username .. "':'" .. password .. "' " .. api_url_power)
    local result = handle:read("*a")
    handle:close()
    return result
end

-- Fetch data
local power_data = fetch_data(api_url_power)
print("Received data from API:", power_data)  -- Troubleshooting line

-- Extract pv_now value
local pv_now = tonumber(power_data:match('"pv_now":%s*(%d+)'))
if not pv_now then
    print("Error: Unable to extract pv_now value")
    return commandArray
end

print("PV power (kW):", pv_now)  -- Troubleshooting line

-- Update Domoticz virtual sensor
commandArray['UpdateDevice'] = 'deviceIdx|0|pv_now'
print("Updating virtual sensor with PV power:", pv_now)  -- Troubleshooting line

-- Log successful update
local success_message = "Updated virtual sensor with PV power: " .. pv_now
print(success_message)  -- Log success message
--otherdevices_svalues['InfoLog'] = success_message  -- Write to Domoticz info log

return commandArray
User avatar
waltervl
Posts: 7008
Joined: Monday 28 January 2019 18:48
Target OS: Linux
Domoticz version: 2026.3
Location: NL
Contact:

Re: Import Autarco solar panel data from web api

Post by waltervl »

Domoticz running on Udoo X86 (on Ubuntu)
Devices/plugins: ZigbeeforDomoticz (with Xiaomi, Ikea, Tuya devices), Nefit Easy, Midea Airco, Omnik Solar, Goodwe Solar
HappyNerd
Posts: 11
Joined: Wednesday 25 May 2022 10:10
Target OS: NAS (Synology & others)
Domoticz version: 2025.2
Location: Netherlands
Contact:

Re: Import Autarco solar panel data from web api

Post by HappyNerd »

Finally got it to work :idea: . I stayed with LUA script. See code below.
It may not be the most elegant script, but it does the job.
Also I am very much aware that there are many more data available in the two autarco api sites (power and energy), but for now I am very happy with the pv_now value.

I thought I'd post this result, since I have been struggling finding useful information in respect to scraping the Autarco solar panel data into domoticz. Maybe this post will help a small audience in accomplishing this same result.
If you want to push this a step further (e.g. to make use of more data from the API), need any help, I strongly advise you to use Microsoft Copilot and simply ask it to debug the script. It will definitely help you further.

Code: Select all

-- API credentials and URL
local username = 'myactualusername' --username to logon to autarco website
local password = 'myactualpassword' --password to logon to autarco website
local site_id = 'myactualsite_id' --8digit code, which you see in the webURL once you logged on to autarco website
local api_url_power = 'https://my.autarco.com/api/m1/site/'.. site_id ..'/power'

-- DeviceID and nValue
local deviceIdx = 1234 --you first need to create this virtual device, type usage, subtype electric
local nValue = 0

-- Function to fetch data from API
function fetch_data(url)
--  print("Fetching data from API...")  -- Troubleshooting line
    local handle = io.popen("curl -s -u '" .. username .. "':'" .. password .. "' " .. api_url_power)
    local result = handle:read("*a")
    handle:close()
    return result
end

-- Fetch data
local power_data = fetch_data(api_url_power)
-- print("Received data from API:", power_data)  -- Troubleshooting line

-- Extract pv_now value
local pv_now = tonumber(power_data:match('"pv_now":%s*(%d+)'))
if not pv_now then
    print("Error: Unable to extract pv_now value")
    return commandArray
end

-- Additional troubleshooting lines
-- print("PV power (kW):", pv_now)  -- Troubleshooting line
-- print ("DeviceIdx:", deviceIdx) -- Troubleshooting line

-- Update Domoticz virtual sensor
commandArray = {}
commandArray['UpdateDevice'] = deviceIdx .. '|' .. nValue .. '|' .. pv_now

-- print("Updating virtual sensor with PV power:", pv_now)  -- Troubleshooting line

-- Log successful update
local success_message = "Updated virtual sensor with PV power: " .. pv_now
print(success_message)  -- Log success message

return commandArray
HappyNerd
Posts: 11
Joined: Wednesday 25 May 2022 10:10
Target OS: NAS (Synology & others)
Domoticz version: 2025.2
Location: Netherlands
Contact:

Re: Import Autarco solar panel data from web api

Post by HappyNerd »

Finished the project.
The script allows to specify a time interval in minutes
I updated the script with bunch of comment lines.
Also made it a bit more robust:
It now writes the errors in the error log and the info messages in the info log.
Also the script now tries two more times to fetch data API data, if the first time fails.
Theoretically it should not into errors: If the API fails, it will update the PV Power value with 0.
I left in the troubleshooting lines, but they are not active.

Hope there are at least one or two Autarco/Domoticz users who may enjoy the script.

The final code:

Code: Select all

-- Initialize commandArray
commandArray = {}

-- Script logic to define the update interval
local interval = 2 -- Set the interval in minutes
local currentTime = os.date("*t")
local currentMinute = currentTime.min

--Start of the script execution
if currentMinute % interval == 0 then

    -- Declare local variables: API credentials, URL, deviceIdx and nValue
    local username      = 'myactualuserid' -- Autarco UserID (email)
    local password      = 'myactualpassword'     -- Autarco Password
    local site_id       = 'myactualsite_id' --8-digit site_id, which will show when you logon to the autaro website
    local api_url_power = 'https://my.autarco.com/api/m1/site/'.. site_id ..'/power' --One of two API's (other one is energy)
    local deviceIdx     = 1234 --My actual deviceIdx: First create new hardware (dummy), then create a virtual device (type = usage, subtype = electric)
    local nValue        = 0 -- 0 is the right value. See Domoticz website for more info on nValue and sValue in commandArray['UpdateDevice']

    -- Define function to fetch data from API with retries
    function fetch_data_with_retries(url, max_retries)
        local retry_count = 0
        local result = nil
    
        repeat
            local curl_command = string.format("curl -s -u '%s':'%s' %s 2>&1", username, password, url)
            local handle = io.popen(curl_command)
            result = handle:read("*a")
            local exit_status, exit_reason, exit_code = handle:close()
    
            if exit_status then
                -- Command executed successfully
                return result
            else
                retry_count = retry_count + 1
                if retry_count <= max_retries then
                    print("Retrying API fetch (attempt " .. retry_count .. ")...")
                else
                    local error_message = "Error executing curl command: " .. exit_reason .. "Exit code: " .. exit_code
                    assert(false, error_message)  -- Log the error message
                    return nil
                end
            end
        until retry_count > max_retries
    end

    -- Execute function to fetch data from API with retries (up to 2 times)
    local max_retries = 2
    local power_data = fetch_data_with_retries(api_url_power, max_retries)
    --print("Received data from API:", power_data)  -- Troubleshooting line

    -- Extract pv_now value from the retrieved data
    local pv_now = tonumber(power_data:match('"pv_now":%s*(%d+)'))
    if not pv_now then
        local error_message = "Error: Unable to extract pv_now value from API data. pv_now value set to 0"
        assert(false, error_message)  -- This will write the error message to the error log
        pv_now = 0 -- Set a default value to allow the script to continue
    end

    -- Additional troubleshooting lines to validate the input for the commandArray['UpdateDevice'] command
    --local success_message = "DeviceIdx: " .. deviceIdx --Troubleshooting line
    --print(success_message)  -- Log success message     --Troubleshooting line
    --local success_message = "nvalue: " .. nValue       --Troubleshooting line
    --print(success_message)  -- Log success message     --Troubleshooting line
    --local success_message = "pv_now: " .. pv_now       --Troubleshooting line
    --print(success_message)  -- Log success message     --Troubleshooting line

    -- Update Domoticz virtual sensor with pv_now value
    --local success_message = "Start updating virtual sensor with PV power: " .. pv_now
    --print(success_message)  -- Log success message
    commandArray['UpdateDevice'] = deviceIdx .. '|' .. nValue .. '|' .. pv_now

    -- Log successful update
    local success_message = "Successfully updated virtual sensor with PV power: " .. pv_now
    print(success_message)  -- Log success message

end

return commandArray
HappyNerd
Posts: 11
Joined: Wednesday 25 May 2022 10:10
Target OS: NAS (Synology & others)
Domoticz version: 2025.2
Location: Netherlands
Contact:

Re: Import Autarco solar panel data from web api

Post by HappyNerd »

Made some additional updates to further improve the script:
- Included an introduction section with a full explanation on the script itsself and how to setup the virtual device in domoticz
- Included lines so that the script only runs during daytime (including an offset)
- Updated the script to work with a more suitable virtual device type "electric (instant+counter)", so that the graphs now also show the cumulative values
For more details, just start reading the introduction section in the code. Things will be very much self-explanatory.

Final code

Code: Select all

-- Introduction
    -- This LUA "time" script is intended to be used in domoticz
    -- It uses an API to fetch solar panel data from the Autarco website and feeds these data into a virtual domoticz sensor
    -- In fact Autarco has two API's: One for (actual) power and one for (cumulative) energy
    -- This script only uses the "power" API and has domoticz calculate the cumulative values
    -- Before activating the script, first read the guidelines below
-- Before you activate this script, make sure that you first create a virtual sensor in domoticz
    -- Create new hardware - type "dummy" (settings, hardware)
    -- From that hardware create a virtual sensor by pushing the button "create virtual sensors": Type = "Electricity (actuals and counter)"
    -- Locate the sensor in the tab "settings", "hardware" and write down the deviceIdx (you'll need it in a minute)
    -- Locate the sensor in the tab "utilities" and make the following adjustments
        -- adjust the type from "usage" to "delivery"
        -- adjust the energy read from "from device" to "calculated"
-- Then create this LUA script in domoticz
    -- Settings, more options, events
    -- Use the "+" to create a LUA script (type "time")
    -- Copy this entire code into that script and save
-- Next you'll need to adjust the script to your needs
    -- Set the interval with which you want this script to run.
        -- The default is set at 5 minutes
        -- There is no need for a higher frequency as the website also only obtains its data every five minutes or so
        -- Only during testing I find it usefull to use a higher frequency (i.e. every minute)
    -- Set the offset values for sunrise and sunset.
        -- this parameter is built in to avoid the script running during the night
        -- The default is 0 minutes before sunrise and 60 minutes after sunset
    -- Set your personalized variables:
        -- Username and password that you also use to logon to the Autarco website
        -- The Autarco site_ID. that is the 8 digit number which you'll find when you logon manually to the autarco website
    -- Set the deviceIdx that needs to be updated (the one you just created)
    -- Set the maximum number of retries for fetching the data.
        -- If the script fails to obtain data from the website after the maximum number of retries, it finishes by updating the sensor with value "0"
        -- Default number of retries is 2, so the script tries 3 times in total
-- As a last step "activate" the script
-- Note: The script contains a set of troubleshooting lines. If you run into trouble, you can uncomment these to further analyze the problem.

-- Initialize commandArray
commandArray = {}

-- Script logic to define the update interval
local interval      = 5                                                                 -- Set the interval in minutes
local currentTime   = os.date("*t")
local currentMinute = currentTime.min

-- Script logic to have the script only run at x minutes before sunrise to y minutes after sunset
local currentMinutes           = currentTime.hour * 60 + currentTime.min                -- Get current time in minutes since midnight
local sunriseInMinutes         = timeofday['SunriseInMinutes']                          -- Get sunrise time in minutes since midnight
local sunsetInMinutes          = timeofday['SunsetInMinutes']                           -- Get sunset time in minutes since midnight
local offsetBeforeSunrise      = 0                                                      -- Set the offset value in minutes
local offsetAfterSunset        = 60                                                     -- Set the offset value in minutes
local adjustedSunriseInMinutes = sunriseInMinutes - offsetBeforeSunrise                 -- Calculate the start of the period for running the script
local adjustedSunsetInMinutes  = sunsetInMinutes + offsetAfterSunset                    -- Calculate the end   of the period for running the script
local function isDaytime()
    return currentMinutes >= adjustedSunriseInMinutes and currentMinutes <= adjustedSunsetInMinutes -- true if current time is between adjusted sunrise and sunset
end

-- Start of the script execution
if currentMinute % interval == 0 and isDaytime() then                                   -- execute logic else finish the script

    -- Declare local variables: API credentials, URL, deviceIdx and nValue
    local username      = 'myactualuserid'                                              -- Autarco UserID (email)
    local password      = 'myactualpassword'                                            -- Autarco Password
    local site_id       = 'myactualsiteid'                                              -- 8-digit site_id, which will show when you logon to the autaro website
    local api_url_power = 'https://my.autarco.com/api/m1/site/'.. site_id ..'/power'    -- One of two API's (other one is energy, but this script will only be using the power API)
    local deviceIdx     = 1234                                                          -- First create new hardware (dummy), then create a virtual device (type = "Electricity (actuals and counter)")
    local nValue        = 0                                                             -- 0 is the right value. See Domoticz website for more info on nValue and sValue in commandArray['UpdateDevice']

    -- Define function to fetch data from API with retries
    function fetch_data_with_retries(url, max_retries)
        local retry_count = 0
        local result = nil
    
        repeat
            local curl_command = string.format("curl -s -u '%s':'%s' %s 2>&1", username, password, url)
            local handle = io.popen(curl_command)
            result = handle:read("*a")
            local exit_status, exit_reason, exit_code = handle:close()
    
            if exit_status then                                                         -- Command executed successfully
                return result
            else
                retry_count = retry_count + 1
                if retry_count <= max_retries then
                    print("Retrying API fetch (attempt " .. retry_count .. ")...")
                else
                    local error_message = "Error executing curl command: " .. exit_reason .. "Exit code: " .. exit_code
                    assert(false, error_message)                                        -- Log the error message
                    return nil
                end
            end
        until retry_count > max_retries
    end

    -- Execute function to fetch data from API with retries (up to 2 times)
    local max_retries = 2                                                               -- Maximum number of retries before ending the script (with pv_now value = 0)
    local power_data = fetch_data_with_retries(api_url_power, max_retries)
    --print("Received data from API:", power_data)                                      -- Troubleshooting line

    -- Extract pv_now value from the retrieved data
    local pv_now = tonumber(power_data:match('"pv_now":%s*(%d+)'))
    if not pv_now then
        local error_message = "Error: Unable to extract pv_now value from API data. pv_now value set to 0"
        assert(false, error_message)                                                    -- This will write the error message to the error log
        pv_now = 0                                                                      -- Set a default value to allow the script to continue
    end

    -- Additional troubleshooting lines to validate the input for the commandArray['UpdateDevice'] command
    --local success_message = "DeviceIdx: " .. deviceIdx                                -- Troubleshooting line
    --print(success_message)  -- Log success message                                    -- Troubleshooting line
    --local success_message = "nvalue: " .. nValue                                      -- Troubleshooting line
    --print(success_message)  -- Log success message                                    -- Troubleshooting line
    --local success_message = "pv_now: " .. pv_now                                      -- Troubleshooting line
    --print(success_message)  -- Log success message                                    -- Troubleshooting line

    -- Update Domoticz virtual sensor
    --local success_message = "Start updating virtual sensor " .. deviceIdx .. " with PV power: " .. pv_now
    --print(success_message)                                                            -- Troubleshooting line
    commandArray['UpdateDevice'] = deviceIdx .. '|' .. nValue .. '|' .. pv_now .. ';' ..pv_now

    -- Log successful update
    local success_message = "Successfully updated virtual sensor " .. deviceIdx .. " with PV power: " .. pv_now
    print(success_message)                                                              -- Log success message

end

return commandArray
User avatar
Domoberry
Posts: 131
Joined: Tuesday 30 May 2017 19:00
Target OS: Raspberry Pi / ODroid
Domoticz version: 2026.2
Contact:

Re: Import Autarco solar panel data from web api

Post by Domoberry »

Thanks HappyNerd for the script, i was looking for a script to get the key data out of the Autarco system.
I took your script and (with help of AI) rewrote it to dzVents and added retrieving the total energy as well.
The most challanging part (for me noob-coder) was the http call in dzVents with basic username and password authentication, the result works and seems formally correct, but looks quite complex to me with the base64encode. Perhaps I missed a feature in dzVents openURL. Using a url with embedded username and password is not working from within Domoticz (security I assume).
Retrieving the energy is basically identical to retrieving power albeit with a different endpoint as you mentioned. So, you will see two almost identical blocks of code. I will improve that later :idea: .
Since the domoticz Electricity device must have both power and energy when it is updated and given the async character of retrieving both, added some related persistent data storage for these two parameters.
260528 PV output device.png
260528 PV output device.png (13.02 KiB) Viewed 6168 times
There are a lot comments in the code, overkill probably.

Code: Select all

-- get the current power and total energy from the Autarco PV web portal
-- 260527.4

--[[
Description
Get the current power delivered and the total life time energy harvested by the Autarco PV system and
update a suitable Domoticz device. The 'current power' value in [W] is prominently shown in the mobile Autarco 
app as well on the similar Autarco website.
I did not find an option to directly download this data from the Autarco invertor directly, the alternative
is to use the open API provided by Autarco on their monitoring website. To retrieve this, you need a GET to
'power' endpoint of the site my.autarco.com. You will need to provide your user name and password for the 
site as well as the site_id (see below). The reply is JSON formatted and contains the data you are looking for.
This script uses the typical dzVents structure to implement an asynchronous http call. E.g. the script has two
triggers: one time-based for sending the request to the Autarco API. A second one is triggered by the http responses.
This part processes the reply and updates the Domoticz variable. There is some error handling as well as a retry mechanism.
Note that this power data is updated every 5 minutes only, so running the script more frequently is useless  

Installation and requirements
1. An account with the Autarco monitoring portal (username and password) for your PV system 
2. Your site_id. You can find this by logging on the monitoring portal (https://my.autarco.com).
   after successful login, the url shows the site_id at the end of the url
   example: https://my.autarco.com/site/xyzxyzxy
3. A dummy sensor (device) in Domoticz to store the PV power data (idx)
   using the 'Dummy' hardware, create a Electrical (Instant + Counter) and note the idx
   Edit the device and make sure 'Energy Read' is set to 'from device'
4. install the script in Domoticz
    Go to setup / More Options / Events
    Click the "+" tab and select "dzVents / Minimal" and paste the script code
    Name it "Autarco PV power v1", click for now "disabled" and save
5. Set the proper variables in the script and run it
   - idx of above device
   - username / password
   - site_id
   - In case you want maximum logging, set the line "local logLevel =" to domoticz.LOG_DEBUG, otherwise set it to domoticz.LOG_INFO
   - set to 'enabled' and save

As soon as the first-time trigger occurs, you will see the results in the log and in the updated device
--]]

-- define local variables/constants
local logMarker             = "Autarco PV pwr V1"
local logLevel              = domoticz.LOG_INFO -- use domoticz.LOG_DEBUG for debugging, otherwise use domoticz.LOG_INFO
local offsetBeforeSunrise   = 0 -- minutes, script start time
local offsetAfterSunset     = 60 -- minutes -- script end time
local usr                   = "<your_username>" -- as used on the Autarco webportal
local pwd                   = "<your_password>"
local site_id               = "<your_site_id<"
local api_url_power         = "https://my.autarco.com/api/m1/site/" .. site_id .. "/power"
local api_url_energy        = "https://my.autarco.com/api/m1/site/" .. site_id .. "/energy"
local deviceIdxPE             = 116 -- CORRECT THIS
local maxTries              = 2
local autarcoCallbackP      = "autarco_power_callbackP"
local autarcoCallbackE      = "autarco_power_callbackE"
local startTime -- just declare variable as local
local endTime -- just declare variable as local


-- functions go here
local function base64encode(data) -- proudly found elsewhere
    local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    return ((data:gsub('.', function(x)
        local r,bits='',x:byte()
        for i=8,1,-1 do
            r = r .. (bits % 2^i - bits % 2^(i-1) > 0 and '1' or '0')
        end
        return r
    end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
        if #x < 6 then return '' end
        local c = 0
        for i=1,6 do
            c = c + (x:sub(i,i) == '1' and 2^(6-i) or 0)
        end
        return b:sub(c+1, c+1)
    end) .. ({ '', '==', '=' })[#data % 3 + 1])
end -- function base64 encoder

local function callAutarco(dz,url,auth, callback)
    -- call Autarco webportal
    dz.openURL({
        url = url,
        method = "GET",
        headers = {
            Authorization = auth,
            ['User-Agent'] = 'Mozilla/5.0'
        },
        callback = callback
    })
end -- function callAutarco
-- end functions

-- main block
return {
    on = {
		timer = {
			"every 5 minutes" -- Autarco web portal is not updated faster
			-- "every 1 minutes" -- for testing
		},
		httpResponses = {
			autarcoCallbackP, -- this must match with the below callback passed to the openURL command
			autarcoCallbackE -- this must match with the below callback passed to the openURL command
		},
	},

	-- persistent data
	data = {
		tryCountP = { initial = 0 }, -- to keep track of the number of http call attempts made
		tryCountE = { initial = 0 }, -- to keep track of the number of http call attempts made
		pvNowPower = { initial = 0 }, -- to keep track of the latest value received
		pvTotalEnergy = { initial = 0 }, -- to keep track of the latest value received
	},

    -- custom logging
	logging = {
		level = logLevel,
		marker = logMarker
	},

    execute = function(dz, item)
        local jsonReply -- just declare the variable local
        local pvNowPower -- just to declare local
        local pvTotalEnergy -- just to declare local
        local now  -- just declare variable as local
        local auth = 'Basic ' .. base64encode(usr .. ':' .. pwd) -- needed to do basic authentication with dzVents

	--------------------------
        -- sending the requests --
        --------------------------
        if item.isTimer then -- triggered by timer
        	-- show some information for debugging
            dz.log("Autarco endpoint used for Power: " .. tostring(api_url_power), dz.LOG_DEBUG)
            dz.log("Autarco endpoint used for Energy: " .. tostring(api_url_energy), dz.LOG_DEBUG)
            dz.log("Autarco endpoint credentials: " .. tostring(usr) .. " - " .. tostring(pwd), dz.LOG_DEBUG)
            --
			dz.log("Script triggered by isTimer, sending the http request", dz.LOG_DEBUG) -- show what triggered the script
            -- Sunrise/sunset filtering (no calls during nightime hours, power will be 0 anyhow
            now = dz.time
            -- offsetBeforeSunrise = 0 -- defined above
            -- offsetAfterSunset   = 60 -- defined above
            --
            startTime = now.sunriseInMinutes - offsetBeforeSunrise -- in minutes
            endTime   = now.sunsetInMinutes  + offsetAfterSunset -- in minutes
            --
            if now.minutesSinceMidnight < startTime or now.minutesSinceMidnight > endTime 
            then
                dz.log("Skipping Autarco data retrieval (due to night time)", dz.LOG_DEBUG)
                return -- nothing more to do, force exit script
            end
            --
            -- call Autarco web portal for Power and Energy
            dz.log("Requesting Autarco PV power...", dz.LOG_INFO)
            callAutarco(dz, api_url_power, auth, autarcoCallbackP)
            dz.data.tryCountP = dz.data.tryCountP + 1 -- increment number of attempts
            --
            dz.log("Requesting Autarco PV energy...", dz.LOG_INFO)
            callAutarco(dz, api_url_energy, auth, autarcoCallbackE)
            dz.data.tryCountE = dz.data.tryCountE + 1 -- increment number of attempts
            --
            return -- we are done, so force exit script
        end -- if item is timer

        ---------------------------------
        -- handling the response Power --
        ---------------------------------
        if item.isHTTPResponse and item.callback == autarcoCallbackP then -- it is a http response and it has the correct callback 
            -- some debugging information
            dz.log("Idx of Autarco power device: " .. deviceIdxPE, dz.LOG_DEBUG)
			dz.log("Script triggered by isHTTPResponse, processing the response", dz.LOG_DEBUG) -- show what triggered the script
            dz.log("RAW response data (first 250 characters) from 'item.data' was '" .. (item.data or ""):sub(1, 250) .. "'", dz.LOG_DEBUG) -- to avoid flodding the log with potentially huge item.data
			dz.log("HTTP response status: " .. tostring(item.statusCode), dz.LOG_DEBUG) -- add http response status code to the log
            --
            if not item.ok then -- e.g. not a (good) reply received
                dz.log("HTTP error retrieving Autarco data: " .. tostring(item.statusCode), dz.LOG_ERROR)
                if dz.data.tryCountP < maxTries then -- not yet reached the maximum number of http call attempts
                    dz.data.tryCountP = dz.data.tryCountP + 1 -- increment number of attempts
                    dz.log("Retrying Autarco fetch (attempt: " .. dz.data.tryCountP .. ")", dz.LOG_WARNING)
                    -- send call again
                    callAutarco(dz, api_url_power, auth, autarcoCallbackP)
                    return -- we are done, force exit the script
                else -- exceeded max number of tries
                    dz.log("Max attempts (" .. tonumber(dz.data.tryCountP) .. ") reached — no update for Autarco power device", dz.LOG_ERROR)
                    dz.data.tryCountP = 0 -- reset tryCountP
                    return -- we are done, force exit the script
                end -- if tryCountP test
            end -- if not item.ok
            --
            -- Process the json reply
            jsonReply = item.json -- get the json converted version of item.data, dzVents provides this automatically
            if not jsonReply then -- if no json version (so item.data could not be converted into json)
                dz.log("Json decode failed, the first 250 chars of raw data: '" .. (item.data or ""):sub(1, 250) .. "'", dz.LOG_ERROR) -- to avoid flodding the log with potentially huge item.data
                dz.log("No update for Autarco power device", dz.LOG_ERROR)
                dz.data.tryCountP = 0 -- reset P
                return -- we are done, force exit the script
            end -- if no valid json
            -- 
            pvNowPower = 
                jsonReply.stats
                and jsonReply.stats.kpis
                and jsonReply.stats.kpis.pv_now -- this to ensure that 'stats' and ‘kpis' actually exist, avoiding 'attempt to index nil value' errors
            --
            if pvNowPower == nil then -- pvNowPower could still be nil, this checks that
                dz.log("Autarco PV power returned nil, skipping device update", dz.LOG_WARNING)
                return -- nothing more to do, force exit script
            end

            pvNowPower = tonumber(pvNowPower)

            dz.log("Autarco PV power = " .. pvNowPower .. " W", dz.LOG_INFO)

            dz.data.pvNowPower = pvNowPower -- store new value
            pvTotalEnergy = dz.data.pvTotalEnergy -- retrieve latest value
            dz.log("Device (" .. deviceIdxPE .. ") will be updated with power: " .. pvNowPower .. " and energy: " .. pvTotalEnergy, dz.LOG_INFO)
            dz.devices(deviceIdxPE).updateElectricity(tonumber(pvNowPower), tonumber(pvTotalEnergy) * 1000) -- do not use device(idx).update{power,energy) !

            -- Reset try counter after success
            dz.devices(deviceIdxPE).updateElectricity(tonumber(pvNowPower), tonumber(pvTotalEnergy) * 1000)
            dz.data.tryCountP = 0
            
        end -- handling http response with correct callback string.

        ----------------------------------
        -- handling the response Energy -- (highly identical to power, so use function?)
        ----------------------------------
        if item.isHTTPResponse and item.callback == autarcoCallbackE then -- it is a http response and it has the correct callback 
            -- some debugging information
            dz.log("Idx of Autarco energy device: " .. deviceIdxPE, dz.LOG_DEBUG)
			dz.log("Script triggered by isHTTPResponse, processing the response", dz.LOG_DEBUG) -- show what triggered the script
            dz.log("RAW response data (first 250 characters) from 'item.data' was '" .. (item.data or ""):sub(1, 250) .. "'", dz.LOG_DEBUG) -- to avoid flodding the log with potentially huge item.data
			dz.log("HTTP response status: " .. tostring(item.statusCode), dz.LOG_DEBUG) -- add http response status code to the log
            --
            if not item.ok then -- e.g. not a (good) reply received
                dz.log("HTTP error retrieving Autarco data: " .. tostring(item.statusCode), dz.LOG_ERROR)
                if dz.data.tryCountE < maxTries then -- not yet reached the maximum number of http call attempts
                    dz.data.tryCountE = dz.data.tryCountE + 1 -- increment number of attempts
                    dz.log("Retrying Autarco fetch (attempt: " .. dz.data.tryCountE .. ")", dz.LOG_WARNING)
                    -- send call again
                    callAutarco(dz, api_url_power, auth, autarcoCallbackE)
                    return -- we are done, force exit the script
                else -- exceeded max number of tries
                    dz.log("Max attempts (" .. tonumber(dz.data.tryCountE) .. ") reached — no update for Autarco energy device", dz.LOG_ERROR)
                    dz.data.tryCountE = 0 -- reset tryCountE
                    return -- we are done, force exit the script
                end -- if tryCountE test
            end -- if not item.ok
            --
            -- actually what follows next is the 'else' case, e.g. item.ok is true since the 'not ok' case always ends with forced return, it can be written like this
            --
            -- Process the json reply
            jsonReply = item.json -- get the json converted version of item.data, dzVents provides this automatically
            if not jsonReply then -- if no json version (so item.data could not be converted into json)
                dz.log("Json decode failed, the first 250 chars of raw data: '" .. (item.data or ""):sub(1, 250) .. "'", dz.LOG_ERROR) -- to avoid flodding the log with potentially huge item.data
                dz.log("No update for Autarco energy device", dz.LOG_ERROR)
                dz.data.tryCountE = 0 -- reset tryCountE
                return -- we are done, force exit the script
            end -- if no valid json
            -- 
            pvTotalEnergy = 
                jsonReply.stats
                and jsonReply.stats.kpis
                and jsonReply.stats.kpis.pv_to_date -- this to ensure that 'stats' and ‘kpis' actually exist, avoiding 'attempt to index nil value' errors
            --
            if pvTotalEnergy == nil then -- pvTotalEnergy could still be nil, this checks that
                dz.log("Autarco PV energy returned nil, skipping device update", dz.LOG_WARNING)
                return -- nothing more to do, force exit script
            end

            pvTotalEnergy = tonumber(pvTotalEnergy)

            dz.log("Autarco PV energy = " .. pvTotalEnergy .. " kWh", dz.LOG_INFO)

            dz.data.pvTotalEnergy = pvTotalEnergy -- store new value
            pvNowPower = dz.data.pvNowPower -- retrieve latest value
            dz.log("Device (" .. deviceIdxPE .. ") will be updated with power: " .. pvNowPower .. " and energy: " .. pvTotalEnergy, dz.LOG_INFO)
            dz.devices(deviceIdxPE).updateElectricity(tonumber(pvNowPower), tonumber(pvTotalEnergy) * 1000) -- do not use device(idx).update{power,energy) !
            
            -- Reset try counter after success
            dz.data.tryCountP = 0
            
        end -- handling http response with correct callback string.


	end -- execute function
}
-- end main block
Post Reply