solar power prediction

Subforum for general discussions. Do not dump your questions/problems here, but try to find the subforum where it belongs!

Moderators: leecollings, remb0

Post Reply
JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

solar power prediction

Post by JanJaap »

Hey,

Has anyone already built some integration to fetch predicted solar power? Would be interested in light of controlling home batteries. First step would be to compare prediction vs actuals..... Solarpanel.forecast for example seems like a potential target to pull form.
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
User avatar
psubiaco
Posts: 233
Joined: Monday 20 August 2018 9:38
Target OS: Raspberry Pi / ODroid
Domoticz version: Beta
Location: Italy
Contact:

Re: solar power prediction

Post by psubiaco »

I don't remeber where I found this dzVents script, but it works. Try it!

Code: Select all

return {
  on = {
    timer = {
        'at *:50'
    },
    httpResponses = {
      'solarforecastEAST' -- must match with the callback passed to the openURL command
    }
  },
  logging = {
    level = domoticz.LOG_INFO,
    marker = 'get solar forecast',
  },
  execute = function(domoticz, item)
      
      local idxSolarForecastCounter=2481  	-- device with 24h forecast: type 'Managed counter', Energy generated
      local idxSolarForecast=2477  			-- device holding the forecast for the next hour: type 'Custom sensor', axis: 'Wh'


    if (item.isTimer) then
		print("EAST")
      domoticz.openURL({
        url = 'https://api.forecast.solar/estimate/watthours/period/45.8812/12.1833/15/-90/2.7',
        method = 'GET',
        callback = 'solarforecastEAST', -- see httpResponses above.
      })
    end

    if (item.isHTTPResponse) then

      if (item.ok) then
        --domoticz.log('item.data ' .. item.data .. '***************************', domoticz.LOG_INFO)
        
        if (item.isJSON) then
            domoticz.utils.dumpTable(item)
            
            local messagetype=item.json.message["type"]
            domoticz.log("message type" .. messagetype, domoticz.LOG_INFO) 
            
            if messagetype=="success" then
                local currentHR=os.date("%Y-%m-%d %H:00:00")
                local oneHRahead=os.date("%Y-%m-%d %H:00:00",os.time()+1*60*60)
                local twoHRahead=os.date("%Y-%m-%d %H:00:00",os.time()+2*60*60)
                local forecastCurrentHR=tonumber(item.json.result[currentHR])
                if forecastCurrentHR==nil then
                    forecastCurrentHR=0 
                end    
                local forecastOneHR=tonumber(item.json.result[oneHRahead])
                if forecastOneHR==nil then
                    forecastOneHR=0 
                end    
                local forecastTwoHR=tonumber(item.json.result[twoHRahead])    
                if forecastTwoHR==nil then
                    forecastTwoHR=0 
                end    
                domoticz.log("solar forecast for next three hours :" .. forecastCurrentHR .. "+" .. forecastOneHR .. " + " .. forecastTwoHR .. " WattHR", domoticz.LOG_INFO) 
                domoticz.devices(idxSolarForecast).updateCustomSensor(forecastOneHR)
                
                local updateHour=0
                for id = 1, 24 do
                    if id<10 then
                        updateHour="0"..tostring(id)
                    else 
                        updateHour=tostring(id)
                    end    
                    domoticz.devices(idxSolarForecastCounter).updateHistory(os.date("%Y-%m-%d ")..updateHour..":00:00","0;0")
                    domoticz.devices(idxSolarForecastCounter).updateHistory(os.date("%Y-%m-%d ",os.time()+24*60*60)..updateHour..":00:00","0;0")
                end    
                    
                local response=item.json.result
                for datehour,value in pairs(response) do
                    domoticz.log("solar forecast date "..domoticz.utils.stringSplit(datehour)[1].." hour "..domoticz.utils.stringSplit(domoticz.utils.stringSplit(datehour)[2],":")[1].." value "..value,domoticz.LOG_INFO)
                    local previousHour=domoticz.utils.stringSplit(domoticz.utils.stringSplit(datehour)[2],":")[1]-1
                    if previousHour<10 then 
                        previousHour="0"..tostring(previousHour)
                    else
                        previousHour=tostring(previousHour)
                    end    
                    domoticz.log("previousHour "..previousHour)    
                    if value>0 then
                        sensorDateHour=domoticz.utils.stringSplit(datehour)[1].." "..domoticz.utils.stringSplit(domoticz.utils.stringSplit(datehour)[2],":")[1]..":00:00"
                        sValueStr="0;"..value
                        domoticz.log("sensorDateHour "..sensorDateHour.." sValueStr "..sValueStr,domoticz.LOG_INFO)
                        domoticz.devices(idxSolarForecastCounter).updateHistory(sensorDateHour,sValueStr)
                        if sensorDateHour==currentHR then
                            domoticz.devices(idxSolarForecastCounter).updateCounter(value)
                        end    
                    end       
                end
            else
                domoticz.log("no successfull message", domoticz.LOG_INFO)
            end    
        else
            domoticz.log('is not json', domoticz.LOG_INFO) 
        end    
      else
        domoticz.log('There was a problem handling the request', domoticz.LOG_INFO)
        domoticz.log(item, domoticz.LOG_INFO)
      end

    end

  end
}

Paolo
--
I use DomBus modules to charge EV car, get a full alarm system, control heat pump, fire alarm detection, lights and much more. Video
Facebook page - Youtube channel
JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

Re: solar power prediction

Post by JanJaap »

Tnx! Looks simple enough, might convert it to plugin.
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
User avatar
psubiaco
Posts: 233
Joined: Monday 20 August 2018 9:38
Target OS: Raspberry Pi / ODroid
Domoticz version: Beta
Location: Italy
Contact:

Re: solar power prediction

Post by psubiaco »

JanJaap wrote: Saturday 01 February 2025 20:02 Tnx! Looks simple enough, might convert it to plugin.
Wow... keep us informed if you'll write a plugin doing that! Thanks a lot
Paolo
--
I use DomBus modules to charge EV car, get a full alarm system, control heat pump, fire alarm detection, lights and much more. Video
Facebook page - Youtube channel
willemd
Posts: 735
Joined: Saturday 21 September 2019 17:55
Target OS: Raspberry Pi / ODroid
Domoticz version: 2024.1
Location: The Netherlands
Contact:

Re: solar power prediction

Post by willemd »

here is my dzVents script to load solar forecast for my location (I replaced my location with XXXX an YYYY) and my panel specs (angles and capacity)
Please check the API specification for the other parameters in the call.

Code: Select all

return {
	on = {
		timer = {
		    'every hour'
		},
		httpResponses = {
			'solarforecast' -- must match with the callback passed to the openURL command
		}
	},
	logging = {
		level = domoticz.LOG_INFO,
		marker = 'get solar forecast',
	},
	execute = function(domoticz, item)
	    
	    local idxSolarForecast=106  -- device holding the forecast for the next hour
	    local idxSolarForecastCounter=186

		if (item.isTimer) then
			domoticz.openURL({
				url = 'https://api.forecast.solar/estimate/watthours/period/XXXX/YYYY/55/58/2.75',
				method = 'GET',
				callback = 'solarforecast', -- see httpResponses above.
			})
		end

		if (item.isHTTPResponse) then

			if (item.ok) then
				--domoticz.log('item.data ' .. item.data .. '***************************', domoticz.LOG_INFO)
				
				if (item.isJSON) then
				    domoticz.utils.dumpTable(item)
				    
				    local messagetype=item.json.message["type"]
				    domoticz.log("message type" .. messagetype, domoticz.LOG_INFO) 
				    
				    if messagetype=="success" then
				        local currentHR=os.date("%Y-%m-%d %H:00:00")
				        local oneHRahead=os.date("%Y-%m-%d %H:00:00",os.time()+1*60*60)
    				    local twoHRahead=os.date("%Y-%m-%d %H:00:00",os.time()+2*60*60)
    				    local forecastCurrentHR=tonumber(item.json.result[currentHR])
    				    if forecastCurrentHR==nil then
    				        forecastCurrentHR=0 
    				    end    
    				    local forecastOneHR=tonumber(item.json.result[oneHRahead])
    				    if forecastOneHR==nil then
    				        forecastOneHR=0 
    				    end    
    				    local forecastTwoHR=tonumber(item.json.result[twoHRahead])    
    				    if forecastTwoHR==nil then
    				        forecastTwoHR=0 
    				    end    
    				    domoticz.log("solar forecast for next three hours :" .. forecastCurrentHR .. "+" .. forecastOneHR .. " + " .. forecastTwoHR .. " WattHR", domoticz.LOG_INFO) 
    				    domoticz.devices(idxSolarForecast).updateCustomSensor(forecastOneHR)
    				    
    				    -- set previous forecast to zero on managed counter
    				    local updateHour=0
    				    for id = 1, 24 do
    				        if id<10 then
    				            updateHour="0"..tostring(id)
    				        else 
    				            updateHour=tostring(id)
    				        end    
    				        domoticz.devices(idxSolarForecastCounter).updateHistory(os.date("%Y-%m-%d ")..updateHour..":00:00","0;0")
    				        domoticz.devices(idxSolarForecastCounter).updateHistory(os.date("%Y-%m-%d ",os.time()+24*60*60)..updateHour..":00:00","0;0")
    				    end    
    				    
    				    -- load new forecast onto managed counter    
    				    local response=item.json.result
				        for datehour,value in pairs(response) do
				            domoticz.log("solar forecast date "..domoticz.utils.stringSplit(datehour)[1].." hour "..domoticz.utils.stringSplit(domoticz.utils.stringSplit(datehour)[2],":")[1].." value "..value,domoticz.LOG_INFO)
				            local previousHour=domoticz.utils.stringSplit(domoticz.utils.stringSplit(datehour)[2],":")[1]-1
				            if previousHour<10 then 
				                previousHour="0"..tostring(previousHour)
				            else
				                previousHour=tostring(previousHour)
				            end    
				            domoticz.log("previousHour "..previousHour)    
				            if value>0 then
				                sensorDateHour=domoticz.utils.stringSplit(datehour)[1].." "..domoticz.utils.stringSplit(domoticz.utils.stringSplit(datehour)[2],":")[1]..":00:00"
				                sValueStr="0;"..value
				                domoticz.log("sensorDateHour "..sensorDateHour.." sValueStr "..sValueStr,domoticz.LOG_INFO)
				                domoticz.devices(idxSolarForecastCounter).updateHistory(sensorDateHour,sValueStr)
				                if sensorDateHour==currentHR then
				                    domoticz.devices(idxSolarForecastCounter).updateCounter(value)
				                end    
				            end    
				        end
    				else
    				    domoticz.log("no successfull message", domoticz.LOG_INFO)
    				end    
				else
				    domoticz.log('is not json', domoticz.LOG_INFO) 
				end    
			else
				domoticz.log('There was a problem handling the request', domoticz.LOG_INFO)
				domoticz.log(item, domoticz.LOG_INFO)
			end

		end

	end
}

JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

Re: solar power prediction

Post by JanJaap »

ok I can fetch data, so far so good. As I never use the dzEvents I'm a bit puzzled to how the sensor update works. Is the 24 hr forecast counter updated per hour? So only one hour form now? Or is it always giving 24 hrs ahead but all hours being updated as time passes?

See first part: JanJaapKo/SolarForecast
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
erwindob
Posts: 5
Joined: Saturday 20 January 2024 12:03
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: solar power prediction

Post by erwindob »

@JanJaap
Looks promising,
I see the values in my log, but not yet in the devices. Keeping an eye on updates.
Toulon7559
Posts: 859
Joined: Sunday 23 February 2014 17:56
Target OS: Raspberry Pi / ODroid
Domoticz version: <2025
Location: Hengelo(Ov)/NL
Contact:

Re: solar power prediction

Post by Toulon7559 »

Try this weblink as introduction & explanation for the scripts earlier in this thread.
Set1 = RPI-Zero+RFXCom433+S0PCM+Shield for BMP180/DS18B20/RS485+DDS238-1ZNs
Set2 = RPI-3A++RFLinkGTW+ESP8266s+PWS_WS7000
Common = KAKUs+3*PVLogger+PWS_TFA_Nexus
plus series of 'satellites' for dedicated interfacing, monitoring & control.
eatme1972
Posts: 1
Joined: Friday 30 January 2026 16:52
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: solar power prediction

Post by eatme1972 »

I installed the Solar Forecast plugin from Jan-Jaap Kostelijk in my Domoticz, but I can not fill in my location anywhere. The counter also stays on 0 KWH.

What am i missing?
JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

Re: solar power prediction

Post by JanJaap »

The location is taken from your Domoticz settings. Then fill out all mandatory settings in the plugin hardware page. And then I think you need to wait for the evening as i remember only polling then to avoid rate limits.

If it then still doesnt do anything i need screenshot of HW config and debug logging.

Oh and the values only appear in the logging of the device, as there is never an actual value.
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
HvdW
Posts: 663
Joined: Sunday 01 November 2015 22:45
Target OS: Raspberry Pi / ODroid
Domoticz version: 2023.2
Location: Twente
Contact:

Re: solar power prediction

Post by HvdW »

Here is another Solar Power forecast which takes the cloud overcast into account.

Code: Select all

return {
    on = {
        timer = {
            'every 2 hours at daytime'  -- Metingen alleen overdag
        },
        httpResponses = {
            'solar_data'
        }
    },
    data = {
        measurementCycle = { initial = {
            startHour = nil,
            startWh = nil,
            measurements = {}
        }}
    },
    execute = function(domoticz, trigger)
        -- ##### CONFIGURATIE #####
        local powerThreshold = 50      -- Minimale weergave drempel (W/m²)
        local dataLogging = true      -- CSV logging aan/uit
        local logFilePath = "/home/pi/csvData/radiation_impact.csv"
        local daysToShow = 2          -- Number of days to display (including today)
        
        -- ##### HELPER FUNCTIES #####
        local function spaces(count)
            return ("&nbsp;"):rep(count)
        end

        local function calculateAdjustedIrradiance(hourData)
            local gr_w = tonumber(hourData.gr_w) or 0
            local lc = tonumber(hourData.lc) or 0
            local mc = tonumber(hourData.mc) or 0
            local hc = tonumber(hourData.hc) or 0
            
            local lowCloudFactor = 0.85
            local midCloudFactor = 0.60
            local highCloudFactor = 0.30
            
            local cloudAttenuation = (lc*lowCloudFactor + mc*midCloudFactor + hc*highCloudFactor) / 100
            cloudAttenuation = math.min(cloudAttenuation, 0.9)
            
            return math.floor(gr_w * (1 - cloudAttenuation) + 0.5)
        end

        local function generateBar(value)
            local adjustedValue = value or 0
            if adjustedValue < 50 then return "" end
            
            local symbol = adjustedValue >= 100 and "■" or "|"
            local count = adjustedValue >= 100 and math.floor(adjustedValue/100) or 1
            return string.rep("<span style='color:#FFA500'>"..symbol.."</span>", count)
        end

        local function appendToCSV(timestamp, data)
            local file = io.open(logFilePath, "a")
            if file then
                file:write(timestamp .. ";" .. table.concat(data, ";") .. "\n")
                file:close()
            else
                domoticz.log("Kan logbestand niet openen: " .. logFilePath, domoticz.LOG_ERROR)
            end
        end

        -- ##### HOOFDLOGICA #####
        if trigger.isTimer then
            -- Haal zoninstralingsdata op
            domoticz.openURL({
                url = 'https://data.meteoserver.nl/api/solar.php?locatie=YourHomeTown&key=abcdef123456',
                method = 'GET',
                callback = 'solar_data'
            })

        elseif trigger.isHTTPResponse then
            local data = trigger.json
            if not data then return end

            local now = os.date("*t")
            local currentHour = now.hour
            local solarDevice = domoticz.devices('Zonnepanelen')
            local whToday = solarDevice.WhToday

            -- Verwerk huidige meting
            local currentInstraling = 0
            local currentAdjusted = 0
            local currentClouds = 0
            for _, hour in ipairs(data.forecast) do
                if string.sub(hour.cet, 12, 13) == string.format("%02d", currentHour) then
                    currentInstraling = tonumber(hour.gr_w) or 0
                    currentAdjusted = calculateAdjustedIrradiance(hour)
                    currentClouds = tonumber(hour.lc) + tonumber(hour.mc) + tonumber(hour.hc)
                    break
                end
            end

            -- ##### DATA LOGGING #####
            if dataLogging then
                -- Initieer CSV met header indien niet bestaat
                if not io.open(logFilePath) then
                    appendToCSV("Timestamp", {
                        "StartUur", "EindUur",
                        "Instraling_Origineel", "Instraling_Gecorrigeerd",
                        "CloudCover", "WhToday_Verschil"
                    })
                end

                -- Controleer of we een nieuwe cyclus starten (elke even uur)
                if currentHour % 2 == 0 then
                    -- Nieuwe cyclus begint
                    domoticz.data.measurementCycle = {
                        startHour = currentHour,
                        startWh = whToday,
                        measurements = {
                            {
                                hour = currentHour,
                                instraling = currentInstraling,
                                adjusted = currentAdjusted,
                                clouds = currentClouds
                            }
                        }
                    }
                else
                    -- Voeg meting toe aan bestaande cyclus
                    table.insert(domoticz.data.measurementCycle.measurements, {
                        hour = currentHour,
                        instraling = currentInstraling,
                        adjusted = currentAdjusted,
                        clouds = currentClouds
                    })

                    -- Als we 3 metingen hebben (uur 0, 1 en 2 van de cyclus)
                    if #domoticz.data.measurementCycle.measurements == 3 then
                        local m = domoticz.data.measurementCycle.measurements
                        
                        -- Controleer of alle metingen gelijk zijn
                        local allEqual = true
                        for i = 2, #m do
                            if m[i].adjusted ~= m[1].adjusted or m[i].clouds ~= m[1].clouds then
                                allEqual = false
                                break
                            end
                        end
                        
                        -- Log alleen als alle metingen gelijk zijn
                        if allEqual then
                            local whDiff = whToday - domoticz.data.measurementCycle.startWh
                            appendToCSV(os.date("%Y-%m-%d %H:%M"), {
                                domoticz.data.measurementCycle.startHour,
                                currentHour,
                                m[1].instraling,
                                m[1].adjusted,
                                m[1].clouds,
                                whDiff
                            })
                        end
                    end
                end
            end

            -- ##### VISUELE WEERGAVE #####
            local datesToDisplay = {}
            for i = 0, daysToShow-1 do
                table.insert(datesToDisplay, os.date('%d-%m-%Y', os.time() + 86400 * i))
            end
            
            local outputLines = {}
            local totalDataLineCount = 0
            
            for dayIndex, currentDate in ipairs(datesToDisplay) do
                -- Voeg datumheader toe
                local dateHeader = "Verwachte zoninstraling ("..currentDate.."):"
                if dayIndex > 1 then dateHeader = spaces(15)..dateHeader end
                table.insert(outputLines, dateHeader)
                
                -- Voeg kolomheaders toe
                local columnHeader = "Uur   Origineel    Gecorrigeerd"
                if dayIndex > 1 then columnHeader = spaces(15)..columnHeader end
                table.insert(outputLines, columnHeader)
                
                -- Voeg scheidingslijn toe
                local separator = "-----------------------------"
                if dayIndex > 1 then separator = spaces(15)..separator end
                table.insert(outputLines, separator)
                
                local dateDataLineCount = 0
                
                for _, hour in ipairs(data.forecast) do
                    if string.sub(hour.cet, 1, 10) == currentDate then
                        local time = string.sub(hour.cet, 12, 16)
                        local original = tonumber(hour.gr_w) or 0
                        local adjusted = calculateAdjustedIrradiance(hour)
                        
                        if adjusted >= powerThreshold then
                            dateDataLineCount = dateDataLineCount + 1
                            totalDataLineCount = totalDataLineCount + 1
                            
                            local lineContent = string.format(
                                "%s  %03d W/m²   %03d W/m² %s",
                                time,
                                original,
                                adjusted,
                                generateBar(adjusted)
                            )
                            
                            -- Inspringing voor dagen na de eerste
                            if dayIndex > 1 then lineContent = spaces(15)..lineContent end
                            
                            -- Speciale inspringing voor eerste dag
                            if dayIndex == 1 and dateDataLineCount > 2 then
                                lineContent = spaces(15)..lineContent
                            end
                            
                            table.insert(outputLines, lineContent)
                        end
                    end
                end
                
                -- Geen data melding
                if dateDataLineCount == 0 then
                    local noDataMsg = "Geen significante zoninstraling verwacht"
                    if dayIndex > 1 then noDataMsg = spaces(15)..noDataMsg end
                    table.insert(outputLines, noDataMsg)
                end
                
                -- Lege regel tussen datums
                if currentDate ~= datesToDisplay[#datesToDisplay] then
                    table.insert(outputLines, "")
                end
            end
            
            -- Update device
            domoticz.devices('Zonnestraling').updateText(table.concat(outputLines, "\n"))
        end
    end
}
Just creatae a TextSensor named Zonnestraling.

The local function calculateAdjustedIrradiance(hourData) calculates the impact of low clouds (lc), medium clouds (mc) and high clouds (hc)
Visit https://data.meteoserver.nl/ to obtain a key.
Bugs bug me.
JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

Re: solar power prediction

Post by JanJaap »

Hey, how reliable are these forecasts? As the ones from Solar Forecast tend to be off by more than 40% regularly....
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
HvdW
Posts: 663
Joined: Sunday 01 November 2015 22:45
Target OS: Raspberry Pi / ODroid
Domoticz version: 2023.2
Location: Twente
Contact:

Re: solar power prediction

Post by HvdW »

JanJaap wrote: Saturday 31 January 2026 9:47 Hey, how reliable are these forecasts? As the ones from Solar Forecast tend to be off by more than 40% regularly....
I have tried to rely on it and have come to the conclusion that it is not reliable. (a bit yes)
It has to do with planning EV charging.
My conclusion: better do this manually.

The other side of the medal:
It's nice to develop something and I have nice displays now.

I made things like predicting the weather visually to be able to check what day is best for a bicycle tour, tomorrow or the day after tomorrow.
Working on airplanes flying over the house using python and dzVents instead of just dzVents.
You know, it's just a hobby keeping me off of the street.
Bugs bug me.
eatme1972
Posts: 1
Joined: Friday 30 January 2026 16:52
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: solar power prediction

Post by eatme1972 »

JanJaap wrote: Friday 30 January 2026 17:19 The location is taken from your Domoticz settings. Then fill out all mandatory settings in the plugin hardware page. And then I think you need to wait for the evening as i remember only polling then to avoid rate limits.

If it then still doesnt do anything i need screenshot of HW config and debug logging.

Oh and the values only appear in the logging of the device, as there is never an actual value.
Thanks JanJaap,
It makes sense to use Domoticz location, I didn't think of that.

I still see "0.00 KwH" in my dashboard. When I open it, I see values.

My goal is to switch on a boiler for x hrs when the solar prediction > 15 KwH.

I have solar panels on east and west, so I make 2 solar forecast hardware/devices and add them up.

Can I do this with the plugin?
solar1.jpg
solar1.jpg (134.97 KiB) Viewed 153 times
Last edited by eatme1972 on Saturday 31 January 2026 13:52, edited 1 time in total.
JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

Re: solar power prediction

Post by JanJaap »

HvdW wrote: Saturday 31 January 2026 11:14
I have tried to rely on it and have come to the conclusion that it is not reliable. (a bit yes)
It has to do with planning EV charging.
My conclusion: better do this manually.

The other side of the medal:
It's nice to develop something and I have nice displays now.
Yeah I want to use it for home battery charging strategy vs solar production. For EV charging im simply using the "slimmer laden" app that just picks the cheapest time.

Definition og a hobby: maximum effort, minimum result 🤪
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
eatme1972
Posts: 1
Joined: Friday 30 January 2026 16:52
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: solar power prediction

Post by eatme1972 »

Update :


Using the dzvents script from psubiaco and claude.ai, I created a script that adds up the 24-hour yield for today and tomorrow from my east and west panels and displays this.
Now to see over the coming year if it's reasonably accurate :)

Code: Select all

return {
    on = {
        timer = {
            'at *:50'
        },
        httpResponses = {
            'solarforecast_oost',
            'solarforecast_west'
        }
    },
    logging = {
        level = domoticz.LOG_INFO,
        marker = 'get solar forecast',
    },
    data = {
        forecastOostToday = { initial = 0 },
        forecastWestToday = { initial = 0 },
        forecastOostTomorrow = { initial = 0 },
        forecastWestTomorrow = { initial = 0 },
        oostReceived = { initial = false },
        westReceived = { initial = false }
    },
    execute = function(domoticz, item)
        local idxSolarForecastToday = 552 -- device voor VANDAAG totaal: type 'Custom sensor', axis: 'Wh'
        local idxSolarForecastTomorrow = 553 -- device voor MORGEN totaal: type 'Custom sensor', axis: 'Wh'
        
        if (item.isTimer) then
            print("Ophalen voorspelling OOST en WEST voor vandaag en morgen")
            
            -- Reset alle waarden
            domoticz.data.forecastOostToday = 0
            domoticz.data.forecastWestToday = 0
            domoticz.data.forecastOostTomorrow = 0
            domoticz.data.forecastWestTomorrow = 0
            domoticz.data.oostReceived = false
            domoticz.data.westReceived = false
            
            -- Call 1: Oost (3 kWp, -90 graden)
            domoticz.openURL({
                url = 'https://api.forecast.solar/estimate/watthours/period/50.0000000/4.0000000/45/-90/3',
                method = 'GET',
                callback = 'solarforecast_oost',
            })
            
            -- Call 2: West (1.4 kWp, 90 graden)
            domoticz.openURL({
                url = 'https://api.forecast.solar/estimate/watthours/period/50.0000000/4.0000000/45/90/1.4',
                method = 'GET',
                callback = 'solarforecast_west',
            })
        end
        
        if (item.isHTTPResponse) then
            if (item.ok) then
                if (item.isJSON) then
                    local messagetype = item.json.message["type"]
                    
                    if messagetype == "success" then
                        local totalToday = 0
                        local totalTomorrow = 0
                        
                        local today = os.date("%Y-%m-%d")
                        local tomorrow = os.date("%Y-%m-%d", os.time() + (24 * 60 * 60))
                        
                        -- Tel alle waarden op per dag
                        for datehour, value in pairs(item.json.result) do
                            local forecastDate = string.sub(datehour, 1, 10)
                            
                            if forecastDate == today then
                                totalToday = totalToday + tonumber(value)
                            elseif forecastDate == tomorrow then
                                totalTomorrow = totalTomorrow + tonumber(value)
                            end
                        end
                        
                        -- Sla het resultaat op afhankelijk van welke callback
                        if item.callback == 'solarforecast_oost' then
                            domoticz.data.forecastOostToday = totalToday
                            domoticz.data.forecastOostTomorrow = totalTomorrow
                            domoticz.data.oostReceived = true
                            domoticz.log("OOST - Vandaag: " .. totalToday .. " Wh, Morgen: " .. totalTomorrow .. " Wh", domoticz.LOG_INFO)
                        elseif item.callback == 'solarforecast_west' then
                            domoticz.data.forecastWestToday = totalToday
                            domoticz.data.forecastWestTomorrow = totalTomorrow
                            domoticz.data.westReceived = true
                            domoticz.log("WEST - Vandaag: " .. totalToday .. " Wh, Morgen: " .. totalTomorrow .. " Wh", domoticz.LOG_INFO)
                        end
                        
                        -- Als beide calls binnen zijn, update de devices met de totalen
                        if domoticz.data.oostReceived and domoticz.data.westReceived then
                            local grandTotalToday = domoticz.data.forecastOostToday + domoticz.data.forecastWestToday
                            local grandTotalTomorrow = domoticz.data.forecastOostTomorrow + domoticz.data.forecastWestTomorrow
                            
                            domoticz.log("TOTAAL VANDAAG: " .. grandTotalToday .. " Wh (" .. domoticz.data.forecastOostToday .. " + " .. domoticz.data.forecastWestToday .. ")", domoticz.LOG_INFO)
                            domoticz.log("TOTAAL MORGEN: " .. grandTotalTomorrow .. " Wh (" .. domoticz.data.forecastOostTomorrow .. " + " .. domoticz.data.forecastWestTomorrow .. ")", domoticz.LOG_INFO)
                            
                            domoticz.devices(idxSolarForecastToday).updateCustomSensor(grandTotalToday)
                            domoticz.devices(idxSolarForecastTomorrow).updateCustomSensor(grandTotalTomorrow)
                        end
                    else
                        domoticz.log("Geen succesvolle respons ontvangen", domoticz.LOG_INFO)
                    end
                else
                    domoticz.log('Respons is geen JSON', domoticz.LOG_INFO)
                end
            else
                domoticz.log('Probleem bij verwerken van het verzoek', domoticz.LOG_INFO)
                domoticz.log(item, domoticz.LOG_INFO)
            end
        end
    end
}
solar2.jpg
solar2.jpg (40.72 KiB) Viewed 139 times
Last edited by eatme1972 on Saturday 31 January 2026 17:53, edited 2 times in total.
Artenverho
Posts: 8
Joined: Friday 26 December 2025 16:02
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: solar power prediction

Post by Artenverho »

I am using NED.nl as data source (free). It also provides prediction for solar production in (provinces of) the Netherlands. I extrapolate the capacity used value (%) for each hour to my own setup and apply correction for angle and orientation. It results are reasonably accurate, at least as accurate as their own web tool they provide as well: https://ned.nl/index.php/nl/zonne-energievoorspeller (which is not accessible through the API).

I have been running it for a month and it is pretty good aside from the occasion misfire when it comes predicting effects of sudden weather changes and it cannot predict the impact of snow on the panels at all :P.

dzvents script below. be sure to make the necessary sensors, user variables and counters

EDIT: forgot to add that the counter for the daily forecast should have a divider of 10000 in order to get kwh..

Code: Select all

--Dzvents script to fetch solar production predictions from NED.nl and adjusts/corrects for PV angle and orientation. Made by Artneverho.

return {
    active = true,
    logging = {
        level = domoticz.LOG_INFO,
        marker = 'NED-hourly-solar'
    },

    -- Persistent forecast storage
    data = {
        solar = { initial = {} },
        solarAll = { initial = {} }  -- store all hours for user variable
    },

    on = {
        timer = { 'every hour' },
        httpResponses = { 'NEDHourlySolar' }
    },

    execute = function(domoticz, item)

        local idxNEDsolarforecast = 528 --provide IDX for counter that shows the forecast (updates hourly)
        local idxNEDtoken         = 29 --IDX for uservariable that contain NED token.
        local idxSolarYesterdayForecast = 529  --idx for custom sensor that shows the final prediction for yesterday (at 23h), in total kWh
        local idxSolarTomorrowForecast = 530 --idx for custom sensor that shows prediction for tomorrow at 13h in total kwh
        local nameUserVariable = 'NEDsolarForecast' --name of uservariable that stors the kwh data in json.

        local token = domoticz.variables(idxNEDtoken).value
        local baseURL = "https://api.ned.nl/v1/utilizations?"

        local pvPeakKW = 4.5 -- set peak power output of solar PV array
        local panelTilt = 27 -- angle of panels
        local pvAzimuth  = 245 -- orientation of the panels.

        local tomorrow = os.time() + 48 * 3600
        local today = os.time()
        local from = os.date("%Y-%m-%d", today)
        local to   = os.date("%Y-%m-%d", tomorrow)
        local currentHour = tonumber(os.date("%H"))


        -- ---- REQUEST ----
        if item.isTimer then
            domoticz.data.solar = {}
            domoticz.data.solarAll = {}  -- clear previous export
            
            --NOTE: adjust the region to your own location by setting "point=0" to any of the values below: 
                -- 0 Nederland
                -- 1 Groningen
                -- 2 Friesland
                -- 3 Drenthe
                -- 4 Overijssel
                -- 5 Flevoland
                -- 6 Gelderland
                -- 7 Utrecht
                -- 8 Noord-Holland
                -- 9 Zuid-Holland
                -- 10 Zeeland
                -- 11 Noord-Brabant
                -- 12 Limburg
                
            local url =
                baseURL ..
                "point=0" ..
                "&type=2" ..
                "&granularity=5" ..
                "&granularitytimezone=1" ..
                "&classification=1" ..
                "&activity=1" ..
                "&validfrom%5Bafter%5D=" .. from ..
                "&validfrom%5Bstrictly_before%5D=" .. to

            domoticz.openURL({
                url = url,
                method = 'GET',
                callback = 'NEDHourlySolar',
                headers = {
                    ['X-AUTH-TOKEN'] = token,
                    ['accept'] = 'application/xml'
                }
            })
        end

        -- ---- RESPONSE ----
        if item.isHTTPResponse then
            if not item.ok or not item.isXML then
                domoticz.log('NED request failed', domoticz.LOG_ERROR)
                return
            end
            domoticz.devices(idxNEDsolarforecast).updateCounter(0)

            local totalToday = 0
            local totalTomorrow = 0
            local daycounter = 0

            for _, entry in ipairs(item.xml.response.item or {}) do
                local ts = entry.validfrom
                local hour = tonumber(string.sub(ts, 12, 13))
                local perc = tonumber(entry.percentage)

                if hour then
                    -- ---- SOLAR POSITION / CORRECTIONS ----
                    local numOfDay = os.date('%j')
                    local angularSpeed = 360/365.25
                    local declination = math.deg(math.asin(0.3978 * math.sin(math.rad(angularSpeed) *(numOfDay - (81 - 2 * math.sin((math.rad(angularSpeed) * (numOfDay - 2))))))))
                    local latitude    = domoticz.settings.location.latitude
                    local longitude   = domoticz.settings.location.longitude
                    local function rad(deg) return deg * math.pi / 180 end
                    local function deg(rad) return rad * 180 / math.pi end
                    
                    local timeUTC = hour
                    local solarHour = timeUTC + (4 * longitude / 60)
                    local hourlyAngle = 15 * (12 - solarHour)
                    local sunAltitude = math.deg(math.asin(math.sin(math.rad(latitude))* math.sin(math.rad(declination)) + math.cos(math.rad(latitude)) * math.cos(math.rad(declination)) * math.cos(math.rad(hourlyAngle))))
                    local sunAzimuth = math.acos((math.sin(math.rad(declination)) - math.sin(math.rad(latitude)) * math.sin(math.rad(sunAltitude))) / (math.cos(math.rad(latitude)) * math.cos(math.rad(sunAltitude) ))) * 180 / math.pi
                    local sinAzimuth = (math.cos(math.rad(declination)) * math.sin(math.rad(hourlyAngle))) / math.cos(math.rad(sunAltitude))
                    if(sinAzimuth<0) then sunAzimuth=360-sunAzimuth end
                    local sunAlt = sunAltitude
                    local sunAz = sunAzimuth

                    local diffuseMin = 0.12
                    local lowSunAltCutoff = 3
                    local correction = 0

                    if perc > 0 and sunAlt > -2 then
                        local azDiff = sunAz - pvAzimuth
                        if azDiff > 180 then azDiff = azDiff - 360 end
                        if azDiff < -180 then azDiff = azDiff + 360 end
                        local directFactor
                        if azDiff >= -55 then
                            directFactor = 1.0
                        elseif azDiff >= -100 then
                            directFactor = math.max(0.15, (azDiff + 100) / 45)
                        else
                            directFactor = 0.1
                        end
                        local altFactor = (sunAlt <= -2) and 0 or (sunAlt < 5 and 0.15 + 0.85*(sunAlt+2)/7 or 1)
                        directFactor = directFactor * altFactor
                        local diffuseFactor = 0.18 * altFactor
                        correction = math.min(1, directFactor + diffuseFactor)
                    end

                    -- ---- FINAL ENERGY ----
                    local kWh = pvPeakKW * perc * correction * 10000

                    -- --- STORE FOR PLOTTING / HISTORY (existing logic) ---
                    domoticz.data.solar[hour] = kWh
                    local function nedToDomoticzTime(ts)
                        local y,m,d,H,M,S = ts:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
                        local t = os.time({year=y, month=m, day=d, hour=H, min=M, sec=S, isdst=nil}) + 3600
                        return os.date("%Y-%m-%d %H:%M:%S", t)
                    end
                    local dt = nedToDomoticzTime(ts)
                    domoticz.devices(idxNEDsolarforecast).updateHistory(dt, "0;" .. kWh)

                    -- --- NEW: store for user variable export
                    table.insert(domoticz.data.solarAll, { ts = ts, kWh = kWh })

                    -- --- DAILY TOTALS (existing logic) ---
                    if daycounter <= 24 then
                        totalToday = totalToday + kWh
                    else
                        totalTomorrow = totalTomorrow + kWh
                    end
                    daycounter = daycounter + 1

                    -- Debug log
                    domoticz.log(string.format("Hour %d: perc=%.4f sunAz=%.1f, sunAlt=%.1f, correction=%.3f kWh=%.3f", hour, perc, sunAz, sunAlt, correction, kWh), domoticz.LOG_INFO)
                end
            end

            -- ---- EXPORT TO USERVARIABLE ----
            local out = {}
            for _, e in ipairs(domoticz.data.solarAll) do
                table.insert(out, { ts = e.ts, kWh = tonumber(string.format('%.3f', e.kWh)) })
            end
            table.sort(out, function(a,b) return a.ts < b.ts end)
            domoticz.variables(nameUserVariable).set(domoticz.utils.toJSON(out))
            domoticz.log('Stored NEDsolarForecast ('..#out..' hourly entries, including tomorrow if available)', domoticz.LOG_INFO)

            -- ---- DAILY CUSTOM SENSOR UPDATES (existing logic) ----
            if currentHour == 23 then
                local totalKWh = totalToday / 10000
                domoticz.devices(idxNEDsolarforecast).updateHistory(from, '0;' .. totalToday)
                domoticz.devices(idxSolarYesterdayForecast).updateCustomSensor(domoticz.utils.round(totalKWh,2))
            else
                domoticz.devices(idxSolarYesterdayForecast).updateCustomSensor(domoticz.devices(idxSolarYesterdayForecast).rawData[1])
            end

            if currentHour == 13 then
                local totalKWhTomorrow = totalTomorrow / 10000
                domoticz.devices(idxSolarTomorrowForecast).updateCustomSensor(domoticz.utils.round(totalKWhTomorrow,2))
            else
                domoticz.devices(idxSolarTomorrowForecast).updateCustomSensor(domoticz.devices(idxSolarTomorrowForecast).rawData[1])
            end

            domoticz.devices(idxNEDsolarforecast).updateCounter(domoticz.utils.round(totalToday,2))
            domoticz.log("Total predicted solar production today: "..string.format("%.3f kWh", totalToday), domoticz.LOG_INFO)
        end
    end
}
JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

Re: solar power prediction

Post by JanJaap »

eatme1972 wrote: Saturday 31 January 2026 13:04
I have solar panels on east and west, so I make 2 solar forecast hardware/devices and add them up.

Can I do this with the plugin?
Seeing you already fixed that. No you can't, the plugin you need to set up twice and then use some other script to add them.
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
JanJaap
Posts: 229
Joined: Thursday 12 October 2017 20:46
Target OS: Raspberry Pi / ODroid
Domoticz version: Dev
Location: the Netherlands
Contact:

Re: solar power prediction

Post by JanJaap »

Artenverho wrote: Saturday 31 January 2026 20:51 I am using NED.nl as data source (free). It also provides prediction for solar production in (provinces of) the Netherlands. I extrapolate the capacity used value (%) for each hour to my own setup and apply correction for angle and orientation. It results are reasonably accurate, at least as accurate as their own web tool they provide as well: https://ned.nl/index.php/nl/zonne-energievoorspeller (which is not accessible through the API).

I have been running it for a month and it is pretty good aside from the occasion misfire when it comes predicting effects of sudden weather changes and it cannot predict the impact of snow on the panels at all :P.

dzvents script below. be sure to make the necessary sensors, user variables and counters

EDIT: forgot to add that the counter for the daily forecast should have a divider of 10000 in order to get kwh..
Any insights on the accuracy of this API? Ive compared the results of last months of my plugin and saw some massive outliers where predictions were a factor 5 or more too high. And then read your comment about the snow ;). For the normal days I see accurcy vary around 30% in both directions.

I plan to put this into a plugin as well (will fire up copolit tonight) as this is usually a bit more user friendly way to fetch the data (although many users combine the data collection with the actual business logic, I like to separate those).
RPi 3, Domoticz dev version, Aeon ZWave stick (with a whole bunch of slaves), Zigbee using Zigbee2MQTT, Nest thermo, P1 smart meter on RPi Zero
Artenverho
Posts: 8
Joined: Friday 26 December 2025 16:02
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: solar power prediction

Post by Artenverho »

JanJaap wrote: Saturday 07 February 2026 10:49 Any insights on the accuracy of this API? Ive compared the results of last months of my plugin and saw some massive outliers where predictions were a factor 5 or more too high. And then read your comment about the snow ;). For the normal days I see accurcy vary around 30% in both directions.
It seems the most accurate on the day itself rather predicting tomorrow. But this is probably just the Dutch in general I guess. on a sunny day like today my solar system generated 8.7kwh, the script I made predicted 8.5kwh this morning. Yesterday it predicted 6.8kwh for today.. so yeah it is not perfect but for my purpose in scheduling my home battery it works (see my other post: viewtopic.php?t=44342). The API allows quite some refreshing so I am updating the predictions every hour.

FYI, you can also read out wind production predictions from the same API. I use this to relax the economics of my home battery scheduling every time the wind energy production goes above 90% in The Netherlands. The aim here is to increase my renewable energy usage. script is very similar to the solar one:

Code: Select all

--Dzvents script to fetch wind production predictions from NED.nl and adjusts/corrects for PV angle and orientation. Made by Artneverho.


return {
    on = {
        timer = { 'every hour' },
        httpResponses = { 'NEDHourlyWind' }
    },

    logging = {
        level = domoticz.LOG_INFO,
        marker = 'NED-hourly-wind'
    },

    data = {
        windAll = { initial = {} } -- NEW: store all hours for user variable export
    },

    execute = function(domoticz, item)

        local idxWindForecast = 525      -- Managed counter (%)
        local idxNEDtoken     = 29  --IDX for uservariable that contain NED token.
        
        local nameUserVariable = 'NEDwindForecast'  --name of uservariable that stors the kwh data in json.

        local token = domoticz.variables(idxNEDtoken).value
        local baseURL = "https://api.ned.nl/v1/utilizations?"

        local now = os.time()
        local from = os.date("%Y-%m-%d", now)
        local to   = os.date("%Y-%m-%d", now + 48 * 3600)

        local function nedLocalDateHour(ts)
            local y,m,d,H,M,S = ts:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
            local t = os.time({
                year = tonumber(y),
                month = tonumber(m),
                day = tonumber(d),
                hour = tonumber(H),
                min = tonumber(M),
                sec = tonumber(S),
                isdst = nil
            }) + 3600
            return os.date("%Y-%m-%d", t), tonumber(os.date("%H", t))
        end

        -- ---- REQUEST ----
        if item.isTimer then
            domoticz.data.windAll = {} -- clear previous export

            local url =
                baseURL ..
                "point=0" ..
                "&type=1" ..
                "&granularity=5" ..
                "&granularitytimezone=1" ..
                "&classification=1" ..
                "&activity=1" ..
                "&validfrom%5Bafter%5D=" .. from ..
                "&validfrom%5Bstrictly_before%5D=" .. to

            domoticz.openURL({
                url = url,
                method = 'GET',
                callback = 'NEDHourlyWind',
                headers = {
                    ['X-AUTH-TOKEN'] = token,
                    ['accept'] = 'application/xml'
                }
            })
        end

        local todayDate  = os.date("%Y-%m-%d")
        local currentHour = tonumber(os.date("%H"))

        -- ---- RESPONSE ----
        if item.isHTTPResponse then
            if not item.ok or not item.isXML then
                domoticz.log('NED wind request failed', domoticz.LOG_ERROR)
                return
            end

            -- reset visible counter
            domoticz.devices(idxWindForecast).updateCounter(0)

            for _, entry in ipairs(item.xml.response.item or {}) do
                local ts = entry.validfrom
                local perc = tonumber(entry.percentage)

                if ts and perc then
                    -- --- Store for user variable export
                    table.insert(domoticz.data.windAll, { ts = ts, perc = perc })

                    -- Domoticz-safe timestamp
                    local function nedToDomoticzTime(ts)
                        local y,m,d,H,M,S = ts:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
                        local t = os.time({
                            year=y, month=m, day=d,
                            hour=H, min=M, sec=S,
                            isdst=nil
                        }) + 3600
                        return os.date("%Y-%m-%d %H:%M:%S", t)
                    end

                    local dt = nedToDomoticzTime(ts)
                    local date, hour = nedLocalDateHour(ts)

                    -- Existing device history update
                    local value = perc * 10000
                    domoticz.devices(idxWindForecast).updateHistory(dt, "0;" .. value)

                    if date == todayDate and hour == currentHour then
                        domoticz.devices(idxWindForecast).update(0, string.format("%.1f", value))
                    end

                    domoticz.log(string.format("Hour %s wind perc=%.2f%%", dt:sub(12,13), perc * 100), domoticz.LOG_INFO)
                end
            end

            -- ---- EXPORT TO USERVARIABLE ----
            local out = {}
            for _, e in ipairs(domoticz.data.windAll) do
                table.insert(out, { ts = e.ts, perc = tonumber(string.format('%.4f', e.perc)) })
            end
            table.sort(out, function(a,b) return a.ts < b.ts end)
            domoticz.variables(nameUserVariable).set(domoticz.utils.toJSON(out))
            domoticz.log('Stored NEDwindForecast ('..#out..' hourly entries, including tomorrow if available)', domoticz.LOG_INFO)
        end
    end
}
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest