Is my solar panel still working as it should? Would be nice to have a domoticz watchdog routine that checks if erveything is still ok on my roof.
I'm using the Growatt plugin, so there's already information how much power is prodcuced.
Simple method for the watchdog would be: check if there has been power produced last 24 hour. If not: notify user.
More elegant method would be: gather lux data from metro service (Weatherunderground) and compare with daily produced solar power.
Before I start programming: anyone here with advice?
solar panel watchdog
Moderators: leecollings, remb0
-
willemd
- Posts: 739
- Joined: Saturday 21 September 2019 17:55
- Target OS: Raspberry Pi / ODroid
- Domoticz version: 2024.1
- Location: The Netherlands
- Contact:
Re: solar panel watchdog
Here is a script that I use to calculate a theoretical PV panel production, based on weather data (in particular sun power measured at a nearby weather station to determine sky cover), sun position and PV panel placement. This is then compared to the actual PV panel production.
This is based on work of others for calculation of sun azimuth and altitude etc, as published in this forum, and has been modified further with calculation of solar irradiance on the PV panels.
The final results are also made visible in Dashticz so it is easy to compare and spot anomalies.
This is based on work of others for calculation of sun azimuth and altitude etc, as published in this forum, and has been modified further with calculation of solar irradiance on the PV panels.
The final results are also made visible in Dashticz so it is easy to compare and spot anomalies.
Code: Select all
--[[
This script calculate in real-time some usefull solar data without any hardware sensor :
· Azimuth : Angle between the Sun and the north, in degree. (north vector and the perpendicular projection of the sun down onto the horizon)
· Altitude : Angle of the Sun with the horizon, in degree.
-- Installation & Documentation -----------------------------------------------------------
https://www.domoticz.com/wiki/index.php?title=Lua_dzVents_-_Solar_Data:_Azimuth_Altitude_Lux
-- Prerequisits -----------------------------------------------------------
Requires Domoticz v3.8551 or later.
Work with lua 5.3 too.
No Platform dependent; Linux & Windows
-- Contributors ----------------------------------------------------------------
V1.0 - Sébastien Joly - Great original work
V1.1 - Neutrino - Adaptation to Domoticz
V1.2 - Jmleglise - An acceptable approximation of the lux below 1° altitude for Dawn and dusk + translation + several changes to be more userfriendly.
V1.3 - Jmleglise - No update of the Lux data when <=0 to get the sunset and sunrise with lastUpdate
V1.4 - Jmleglise - use the API instead of updateDevice to update the data of the virtual sensor to be able of using devicechanged['Lux'] in our scripts. (Due to a bug in Domoticz that doesn't catch the devicechanged event of the virtual sensor)
V1.5 - xces - UTC time calculation.
- waaren - pow function for lua 5.3 compatibility
V2.4 - BakSeeDaa - Converted to dzVents and changed quite many things.
V2.41 - Oredin - Use Dark Sky API instead of WU API
V3.0 - Bram Vreugdenhil/Hestia - Weather Api independent. Use OpenWeathermaps devices or your own sensors
V3.1 - Jmleglise - Merge the different fork. Some clean Up, comment and wiki.
]]
--
-- Variables to customize ------------------------------------------------
-- Devices for Input Data
local idxPVpanel = 3 -- actual PVpanel total production, measured by homewizard kWh meter
local idxTempHumBaro = 4 -- barometer device, imported from buienradar API (KNMI weather station Hoek van Holland)
local idxWind = 7
local idxSunpower = 8 -- sunpower as provided by buienradar, measured by pyranonmeter, so it is GHI, i.e. total radiation on horizontal surface
-- Devices for Output Data (can be nil if you dont want some data)------------------------------------------
local idxSolarAltitude = 23 -- Virtual custom sensor for Solar Altitude (angle with horizon)
local idxSolarAzimuth = 24 -- Virtual custom sensor for Solar Azimuth (angle versus north)
local idxAOI = 25 -- Virtual custom sensor for AOI, angle of incidence of sunbeam on PVpanel
local idxClarityIndex = 26 -- Virtual sensor for clarity = actualGHI/theoreticalGHI
local idxTheoreticalGHI = 27
local idxActualDHI = 28
local idxActualDNI = 29
local idxIrradianceBeam = 30
local idxIrradianceGround = 31
local idxIrradianceSky = 32
local idxIrradianceTotal = 33
local idxTheoreticalProduction = 34
local idxRatioActTheorProduction = 35
local idxRatioActMaxProduction = 36
-- Other parameters -----------------------------------------------------
local intervalMins = 5 -- The interval of running this script. No need to be faster than the data source. (For example it is 10 min)
local altitude = 0 -- Meters above sea level of your location. (Integer) Can be found from coordinates on https://www.advancedconverter.com/map-tools/find-altitude-by-coordinates
local latitude = nil -- Keep nil if you have defined your lat. and long. in the settings. Otherwise you can overwrite it here. E.g. something like 51.748485
local longitude = nil -- idem. E.g.something like 5.629728.
local logToFile = false -- Set to true if you also want to log to a file. It might get big by time.
local tmpLogFile = '/tmp/logSun.txt' -- Logging to a file if specified
local PVpanelTiltAngle = 55 -- Angle of PV panels versus horizontal (tilt angle, 0 is horizontal, 90 is vertical)
local PVpanelAzimuth = 238 -- Azimuth of PVpanel , 238 is almost exactly Sout-West direction
return {
active = true,
logging = {
level = domoticz.LOG_INFO, -- Uncomment to override the dzVents global logging setting
marker = 'solar data'
},
on = {
-- devices = {'testSwitch'}, -- a switch for testing w/o waiting minutes
timer = {'every '..tostring(intervalMins)..' minutes between sunrise and 30 minutes after sunset'} -- There is no more limit to worry about as there is no API called
},
execute = function(domoticz, device)
local function leapYear(year) -- function to determine whether year is leapyear, true or false
return year%4==0 and (year%100~=0 or year%400==0)
end
function math.pow(x, y) -- Function math.pow(x, y) has been deprecated in Lua 5.3. therefore defined here
return x^y
end
-- pick up location coordinates from domoticz settings
if latitude == nil then
latitude = domoticz.settings.location.latitude
end
if longitude == nil then
longitude = domoticz.settings.location.longitude
end
-- base solar data calculations
-- reference http://www.plevenon-meteo.info/technique/theorie/enso/ensoleillement.html
local year = os.date('%Y')
local nbDaysInYear = (leapYear(year) and 366 or 365) -- total number of days in current year
local numOfDay = os.date('%j') -- number of current day in the year
local angularSpeed = 360/365.25
local timeDecimal = (os.date('!%H') + os.date('!%M') / 60 -25/60) -- Coordinated Universal Time (UTC)
-- note a 25 minute time shift is applied because buienradar sunpower measurement is on average 25 minutes delayed
local solarHour = timeDecimal + (4 * longitude / 60 ) -- The solar hour, time with correction for longitude, approx.
local hourAngle = 15 * ( 12 - solarHour ) -- angle of position of sun in its orbit, solar noon is 0
local declination = math.deg(math.asin(0.3978 * math.sin(math.rad(angularSpeed) *(numOfDay - (81 - 2 * math.sin((math.rad(angularSpeed) * (numOfDay - 2))))))))
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(hourAngle))))-- the height of the sun in degree, compared with the horizon
local sunZenith = 90 - sunAltitude -- angle versus vertical instead of horizontal
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 -- deviation of the sun from the North, in degree
local sinAzimuth = (math.cos(math.rad(declination)) * math.sin(math.rad(hourAngle))) / math.cos(math.rad(sunAltitude))
if(sinAzimuth<0) then sunAzimuth=360-sunAzimuth end
local sunstrokeDuration = math.deg(2/15 * math.acos(- math.tan(math.rad(latitude)) * math.tan(math.rad(declination)))) -- duration of sunstroke in the day . Not used in this calculation.
local IncidenceAngle = math.deg(math.acos(math.cos(math.rad(sunZenith))*math.cos(math.rad(PVpanelTiltAngle))+math.sin(math.rad(sunZenith))*math.sin(math.rad(PVpanelTiltAngle))*math.cos(math.rad(sunAzimuth-PVpanelAzimuth)))) -- angle sunbeam on PVpanel
domoticz.devices(idxAOI).updateCustomSensor(domoticz.utils.round(IncidenceAngle,0))
domoticz.devices(idxSolarAzimuth).updateCustomSensor(domoticz.utils.round(sunAzimuth,0))
domoticz.devices(idxSolarAltitude).updateCustomSensor(domoticz.utils.round(sunAltitude,0))
if sunAltitude>5 then
-- first calculate theoretical maximum radiation on horizontal surface with clear sky
local constantSolarRadiation = 1367 -- Solar Constant W/m² produced by the sun, avg at entrance
-- influence of season : seasonRadiation=Solar radiation (in W/m²) in the entrance of atmosphere, varying 3.4% depending on day of year (due to elliptical orbit around the sun)
local seasonRadiation = constantSolarRadiation * (1 +0.034 * math.cos( math.rad( 360 * numOfDay / nbDaysInYear )))
-- influence of atmosphere
local sinusSunAltitude = math.sin(math.rad(sunAltitude)) -- just for easy calculation, repititive factor
local atmosphereFactor=1/(math.cos(math.rad(sunZenith))+0.50572*math.pow((96.07995-sunZenith),-1.6364)) -- pveducation.org
-- influence of altitude ; get barometer pressure from weather station
local relativePressure = domoticz.devices(idxTempHumBaro).barometer
local absolutePressure = relativePressure - domoticz.utils.round((altitude/ 8.3),1) -- hPa
local M = atmosphereFactor * relativePressure/absolutePressure -- influence of location altitude
-- split Global Irradiance on horizontal surface in direct and diffuse factor, for a clear sky
local directNormalFactor=math.pow(0.7,math.pow(atmosphereFactor,0.678)) -- on a perpendicular surface
local directHorizontalFactor=directNormalFactor*sinusSunAltitude -- if sunAltitude=90 then sunZenith=0 and sinusSunAltitude=1 therefore directHorizontalFactor=directNormalFactor
local diffuseFactor=(0.271 - 0.294 * math.pow(0.6,M)) * sinusSunAltitude
-- first calculate theoretical radiation on horizontal surface with a clear sky
local directRadiation, scatteredRadiation, totalRadiation
directRadiation = seasonRadiation * directHorizontalFactor
scatteredRadiation = seasonRadiation * diffuseFactor
totalRadiation = scatteredRadiation + directRadiation
local theoreticalGHI=totalRadiation
-- now compare theoretical GHI (=totalRadiation) to measured GHI (=sunpower=actualGHI)
-- pyranometer measurement by weather station, is actual global direct and indirect irradiance on horizontal surface (GHI)
local actualGHI = domoticz.devices(idxSunpower).sensorValue
local clarityIndex = nil
if theoreticalGHI > 0 then
clarityIndex=actualGHI/theoreticalGHI
end
-- then split the actual GHI in components by using the clarityIndex
local diffuseFraction = 0
if clarityIndex <= 0.3 -- many clouds
then
diffuseFraction = 1.02 - 0.248 * clarityIndex -- diffuse fraction high
else
if (clarityIndex > 0.3 and clarityIndex < 0.78)
then
diffuseFraction = 1.45 - 1.67 * clarityIndex
else -- light/no clouds
diffuseFraction = 0.147 -- diffuse fraction low
end
end
local actualDiffuseHorizontalIrradianceDHI = diffuseFraction * actualGHI
local actualDirectNormalIrradianceDNI = (actualGHI - actualDiffuseHorizontalIrradianceDHI)/math.cos(math.rad(sunZenith))
-- and finally calculate the components for the solar panels
-- AOI = angle of incidence of sunlight on PV panels
local directPVFactor=directNormalFactor*math.sin(math.rad(90-IncidenceAngle)) -- if AOI=0 then sinus(90-AOI)=1 therefore directHorizontalFactor=directNormalFactor
local IrradianceBeam = 0
local IrradianceGround = 0
local IrradianceSky = 0
local IrradianceTotal = 0
if sunAltitude>0
then
IrradianceBeam=actualDirectNormalIrradianceDNI * math.cos(math.rad(IncidenceAngle))
if IrradianceBeam < 0 then -- AOI >90, i.e. no beam directly on panel
IrradianceBeam = 0
end
IrradianceGround=actualGHI * 0.2 * (1-math.cos(math.rad(PVpanelTiltAngle)))/2
IrradianceSky=actualDiffuseHorizontalIrradianceDHI * (1+math.cos(math.rad(PVpanelTiltAngle)))/2 + actualGHI * ((0.012 * sunZenith - 0.04 ) * (1-math.cos(math.rad(PVpanelTiltAngle))))/2 -- should include DHI , soo too low
IrradianceTotal=IrradianceBeam+IrradianceGround+IrradianceSky
else
IrradianceBeam = 0
IrradianceGround = 0
IrradianceSky = 0
IrradianceTotal = 0
end
local RefractionIndex=(1-0.07*(1/math.cos(math.rad(IncidenceAngle)) - 1))
if IncidenceAngle>90 or RefractionIndex<0 then
RefractionIndex=0
end
--print ('RefractionIndex',RefractionIndex)
local Windspeed = domoticz.devices(idxWind).speedMs
--print('windspeed ',Windspeed)
local actualTemperature=domoticz.devices(idxTempHumBaro).temperature
--print('temp ',actualTemperature)
local PVTemperature=actualTemperature+IrradianceTotal*math.exp(-3.47-.0594*Windspeed)
--print('pvtemp ',PVTemperature)
local tempLossFactor=1
if PVTemperature>25 then
tempLossFactor= 1-(0.35 * (PVTemperature - 25))/100
end
--print ('tempLossFactor ',tempLossFactor)
local PVpanelsize = 19.48 -- 12 x 1.64 m x 0.99 m total square Meters
local PVpanelefficiency = 0.154 -- factory specified PVpanelefficiency
local TheoreticalPVpanelProduction = PVpanelsize * PVpanelefficiency * IrradianceBeam *RefractionIndex * tempLossFactor + PVpanelsize * PVpanelefficiency * (IrradianceTotal - IrradianceBeam) * tempLossFactor -- RefractionIndex only applied to beam
local ActualPVpanelProduction = domoticz.devices(idxPVpanel).actualWatt
local RatioActualTheoreticalPVpanel=ActualPVpanelProduction / TheoreticalPVpanelProduction
-- also compare actual to maximum production in case of clear sky
local MaxBeam = 0
local MaxGround = 0
local MaxSky = 0
local MaxTotal = 0
if sunAltitude>0
then
MaxBeam=directRadiation / math.cos(math.rad(sunZenith)) * math.cos(math.rad(IncidenceAngle))
if MaxBeam < 0 then -- AOI >90, i.e. no beam directly on panel
MaxBeam = 0
end
MaxGround=theoreticalGHI * 0.2 * (1-math.cos(math.rad(PVpanelTiltAngle)))/2
MaxSky=scatteredRadiation * (1+math.cos(math.rad(PVpanelTiltAngle)))/2 + theoreticalGHI * ((0.012 * sunZenith - 0.04 ) * (1-math.cos(math.rad(PVpanelTiltAngle))))/2 -- should include DHI , soo too low
MaxTotal=MaxBeam+MaxGround+MaxSky
else
MaxBeam = 0
MaxGround = 0
MaxSky = 0
MaxTotal = 0
end
--print('directRadiation ',directRadiation)
--print('MaxBeam ',MaxBeam)
--print('MaxGround ',MaxGround)
--print('MaxSky ',MaxSky)
local PVpanelsize = 19.48 -- 12 x 1.64 m x 0.99 m total square Meters
local PVpanelefficiency = 0.154 -- factory specified PVpanelefficiency
local MaxPVpanelProduction = PVpanelsize * PVpanelefficiency * MaxBeam *RefractionIndex * tempLossFactor + PVpanelsize * PVpanelefficiency * (MaxTotal-MaxBeam) * tempLossFactor -- RefractionIndex only applied to beam
--print('clear sky production ', MaxPVpanelProduction)
--local ActualPVpanelProduction = domoticz.devices(idxPVpanel).actualWatt
local RatioActualMaxPVpanel=ActualPVpanelProduction / MaxPVpanelProduction
domoticz.devices(idxClarityIndex).updateCustomSensor(domoticz.utils.round(clarityIndex,2))
domoticz.devices(idxTheoreticalGHI).updateCustomSensor(domoticz.utils.round(theoreticalGHI,0))
domoticz.devices(idxActualDHI).updateCustomSensor(domoticz.utils.round(actualDiffuseHorizontalIrradianceDHI,0))
domoticz.devices(idxActualDNI).updateCustomSensor(domoticz.utils.round(actualDirectNormalIrradianceDNI,0))
domoticz.devices(idxIrradianceBeam).updateCustomSensor(domoticz.utils.round(IrradianceBeam,0))
domoticz.devices(idxIrradianceGround).updateCustomSensor(domoticz.utils.round(IrradianceGround,0))
domoticz.devices(idxIrradianceSky).updateCustomSensor(domoticz.utils.round(IrradianceSky,0))
domoticz.devices(idxIrradianceTotal).updateCustomSensor(domoticz.utils.round(IrradianceTotal,0))
domoticz.devices(idxTheoreticalProduction).updateCustomSensor(domoticz.utils.round(TheoreticalPVpanelProduction,0))
domoticz.devices(idxRatioActTheorProduction).updateCustomSensor(domoticz.utils.round(RatioActualTheoreticalPVpanel,2))
domoticz.devices(idxRatioActMaxProduction).updateCustomSensor(domoticz.utils.round(RatioActualMaxPVpanel,2))
domoticz.log('', domoticz.LOG_INFO)
domoticz.log('================== solar data ==================', domoticz.LOG_INFO)
domoticz.log('Altitude:'..tostring(altitude)..', latitude: ' .. latitude .. ', longitude: ' .. longitude, domoticz.LOG_INFO)
domoticz.log('Angular Speed = ' .. angularSpeed .. ' per day', domoticz.LOG_INFO)
domoticz.log('Declination = ' .. declination .. '°', domoticz.LOG_INFO)
domoticz.log('Universal Coordinated Time (UTC) '.. timeDecimal ..' H.dd', domoticz.LOG_INFO)
domoticz.log('Solar Hour '.. solarHour ..' H.dd', domoticz.LOG_INFO)
domoticz.log('Altitude of the sun = ' .. sunAltitude .. '°', domoticz.LOG_INFO)
domoticz.log('Angular hour = '.. hourAngle .. '°', domoticz.LOG_INFO)
domoticz.log('Azimuth of the sun = ' .. sunAzimuth .. '°', domoticz.LOG_INFO)
domoticz.log('Duration of the sun stroke of the day = ' .. domoticz.utils.round(sunstrokeDuration,2) ..' H.dd', domoticz.LOG_INFO)
domoticz.log('Local relative pressure = ' .. relativePressure .. ' hPa', domoticz.LOG_INFO)
domoticz.log('Absolute pressure in atmosphere = ' .. absolutePressure .. ' hPa', domoticz.LOG_INFO)
--domoticz.log('Coefficient of mitigation M = ' .. M ..' M0 = '..M0, domoticz.LOG_INFO)
domoticz.log('directNormalFactor = ' ..directNormalFactor , domoticz.LOG_INFO)
domoticz.log('diffuseFactor = ' ..diffuseFactor , domoticz.LOG_INFO)
domoticz.log('directHorizontalFactor = ' ..directHorizontalFactor , domoticz.LOG_INFO)
domoticz.log('directPVFactor = ' ..directPVFactor , domoticz.LOG_INFO)
domoticz.log('', domoticz.LOG_INFO)
domoticz.log('directRadiation = ' ..directRadiation , domoticz.LOG_INFO)
domoticz.log('scatteredRadiation = ' ..scatteredRadiation , domoticz.LOG_INFO)
domoticz.log('totalRadiation = ' ..totalRadiation , domoticz.LOG_INFO)
else
domoticz.devices(idxClarityIndex).updateCustomSensor(0)
domoticz.devices(idxTheoreticalGHI).updateCustomSensor(0)
domoticz.devices(idxActualDHI).updateCustomSensor(0)
domoticz.devices(idxActualDNI).updateCustomSensor(0)
domoticz.devices(idxIrradianceBeam).updateCustomSensor(0)
domoticz.devices(idxIrradianceGround).updateCustomSensor(0)
domoticz.devices(idxIrradianceSky).updateCustomSensor(0)
domoticz.devices(idxIrradianceTotal).updateCustomSensor(0)
domoticz.devices(idxTheoreticalProduction).updateCustomSensor(0)
domoticz.devices(idxRatioActTheorProduction).updateCustomSensor(0)
domoticz.devices(idxRatioActMaxProduction).updateCustomSensor(0)
end
end
}
-
willemd
- Posts: 739
- Joined: Saturday 21 September 2019 17:55
- Target OS: Raspberry Pi / ODroid
- Domoticz version: 2024.1
- Location: The Netherlands
- Contact:
Re: solar panel watchdog
You can also have a look at https://forecast.solar.
You can launch a URL with your longitude and lattitude as well as your PV panel size and angle to get a forecast of production.
The feedback can be loaded onto a domoticz device.
Here is my script (with my position and PV data replaced by **)
You can launch a URL with your longitude and lattitude as well as your PV panel size and angle to get a forecast of production.
The feedback can be loaded onto a domoticz device.
Here is my script (with my position and PV data replaced by **)
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
if (item.isTimer) then
domoticz.openURL({
url = 'https://api.forecast.solar/estimate/watthours/period/**lat**/**long**/**decl**/**azi**/**kw**', -- replace with your data, have a look at the API specification at forecast.solar
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
local messagetype=item.json.message["type"]
domoticz.log("message type" .. messagetype, domoticz.LOG_INFO)
if messagetype=="success" then
local oneHRahead=os.date("%Y-%m-%d %H:00:00",os.time()+2*60*60)
local twoHRahead=os.date("%Y-%m-%d %H:00:00",os.time()+3*60*60)
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 two hours :" .. forecastOneHR .. " + " .. forecastTwoHR .. " WattHR", domoticz.LOG_INFO)
domoticz.devices(idxSolarForecast).updateCustomSensor(forecastOneHR)
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
}
Who is online
Users browsing this forum: No registered users and 1 guest