Real-time solar data without any hardware sensor : azimuth, Altitude, Lux sensor...

From Domoticz
Jump to navigation Jump to search

Purpose


This LUA script calculates in real-time a lot of 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.
  • Lux : radiation of the sun with real time cloud layer
  • and many more

The calculation is based on the theoretical radiations of the sun according to its location in the sky, calculated according to the time and the number of day in the year, plus the real-time cloud layer and the real-time weather (pressure) :

  • Geographical address
  • Altitude of the geographical point
  • Atmospheric pressure of the place / from api.wunderground.com
  • The cloudiness in Octa (1/8) / from ogimet.com

What is it For ?

  • Manage exactly roller blinds and venetian blind. both to optimise the heating and to prevent dazzle.
  • Manage your light according to the Luminosity
  • Use civil twilight dawn and dusk (-6 deg altitude) instead of sunrise and sunset.
  • Some calculation about photovoltaic production ?
  • Automate the heating of swimming pool
  • Personally I use a roller blind to wake up instead of an alarm clock, with a progressive opening of the roller blind to prevent dazzle.

History

I have improved the huge work of Sébastien Joly (on an another automation system) that Neutrino has migrated to Domoticz and Lua.

I deeply encourage you to visit the very interesting, original post in French :

  • www.domotique-info.fr/2015/09/ou-est-le-soleil-pour-votre-homecenter-2/[1]
  • easydomoticz.com/forum/viewtopic.php?f=17&t=1340#p11528 [2]

And the others documentations and source of the formulae :

  • www.plevenon-meteo.info/technique/theorie/enso/ensoleillement.html[3]
  • herve.silve.pagesperso-orange.fr/solaire.htm[4]
  • en.wikipedia.org/wiki/Azimuth[5]
  • en.wikipedia.org/wiki/Dawn[6]

There was even already a first implementation of these formulae here [7], but I choose to complete the other implementation.

Known limitations

  • Under 1° of Altitude of the sun, the formulae reach their limit of precision. So I implement the theoretical value of the Lux at dawn/dusk defined by the Civil twilight : "when the Sun is between 0 and -6 degrees below the horizon, there is enough light for objects to be distinguishable. The sky is bright, even when cloudy."

  • Between 0h and 2h, no cloud layer data. ogimet web site returns nothing. The previous value is used. Of course, we don't care for the sun at 2h in the morning ... but just in case you want to use the cloud layer for something else.
  • Weather Underground is used. Remember the 500 calls limit per day with the free plan. So run this script at a low frequency.
  • According to your needs, adapt the script to retrieve other data. Run in debug mode to List all data available.

Installation instructions

Create this Uservariable and Virtual sensor :

   Uservariable : name "octa", type integer , initial value 0 (be carefull about the name, case sensitive, as the script will use it).
   (The User variable is not needed if using the dzVents version of the script)
   Virtual sensor LUX : sensor type "Lux", name "Lux".  (be carefull about the name, case sensitive, as the script will use it).
   (This above virtual device is optional if using the dzVents version of the script)
   Virtual sensor Sun Azimuth : sensor type "custom type" / Y axis = "degrees" and then edit it again in the dashboard to choose the icon you want. 
   Virtual sensor Sun Altitude : sensor type "custom type" / Y axis = "degrees" and choose the icon as well. 


(*) custom type exist since beta #4937

You need JSON.lua (now included in DOMOTICZ >3.53xx. Otherwise look here http://www.domoticz.com/wiki/Lua_-_json.lua). Check the path in the script :

json = (loadfile "/home/pi/domoticz/scripts/lua/JSON.lua")()  -- For Linux
--json = (loadfile "D:\\Domoticz\\scripts\\lua\\json.lua")()  -- For Windows



Edit the parameters in the script :

 -- Variables to customize ------------------------------------------------
      local localhost = '127.0.0.1:8080'  -- Set your port. (Not the universal IP).
      local city = "Paris"                -- Your city for Wunderground API
      local countryCode = "FR"            -- Your country code for Wunderground API
      local idxLux ='72'                  -- Your virtual Lux Device ID
      local idxSolarAzimuth ='85'         -- Your virtual Azimuth Device ID
      local idxSolarAltitude ='84'        -- Your virtual Solar Altitude Device ID
      local idxUserVarOcta='2'            -- Your user variable ID , named octa
      local wuAPIkey = "xxxxxxxxe5db6e4c" -- Your Weather Underground API Key
      local latitude = 48.xxxx88          -- your home
      local longitude = 2.xxxx00          -- your home
      local altitude = 27                 -- Your home altitude : run once in debug = 1 to found your altitude in Log and write it here
      local WMOID = '07145'               -- Your nearest SYNOP Station for ogimet. Very important !
      local DEBUG = 0             -- 0 , 1 for domoticz log , 2 for file log


The WMOID parameters is very important. You set up there the nearest SYNOP station. This is to get the Cloud layer in real time. It's the most important to get accurate values. To get the ID number of your nearest station, run once with debug=1. The number will be displayed in the log. But, it's better, to choose it manually there : http://www.ogimet.com/gsynop_nav.phtml.en . If you have several stations in your region. I advise to you to choose the one that is before your home in the "usual" direction of the wind. Thus, the clouds that the station detects, arrives above your home when the script get the data.


How to Use in your scripts

In your Lua script, instead of if minutes=timeofday['SunsetInMinutes'] you can use :

	if tonumber(otherdevices_svalues['Lux'])==0 and timedifference(otherdevices_lastupdate['Lux'])<60
   By the way, The script is for Raspberry system. But, It will work on windows Platform with very few adaptation. (JSON Path. Something else ? )


Lua script

    --[[      Virtual Lux sensor and other real-time solar data

    ~/domoticz/scripts/lua/script_time_SolarSensor.lua

    -- Autors  ----------------------------------------------------------------
    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 - 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)
    ]]--

    -- Variables to customize ------------------------------------------------
       local localhost = '127.0.0.1:8080'  -- Set your port. (Not the universal IP).
       local city = "ISCHAIJK2"                -- Your city for Wunderground API
       local countryCode = "NL"            -- Your country code for Wunderground API
       local idxLux ='72'                  -- Your virtual Lux Device ID
       local idxSolarAzimuth ='882'         -- Your virtual Azimuth Device ID
       local idxSolarAltitude ='883'        -- Your virtual Solar Altitude Device ID
       local idxUserVarOcta='1'            -- Your user variable ID , named octa
       local wuAPIkey = "ea89408fb3e97099" -- Your Weather Underground API Key
       local latitude = 51.748485          -- your home
       local longitude = 5.629728          -- your home
       local altitude = 27                 -- Your home altitude : run once in debug = 1 to found your altitude in Log and write it here
       local WMOID = '06356'               -- Your nearest SYNOP Station for ogimet. Very important !
       local DEBUG = 1             -- 0 , 1 for domoticz log , 2 for file log

    -- Below , edit at your own risk ------------------------------------------

    function leapYear(year)   
       return year%4==0 and (year%100~=0 or year%400==0)
    end

    function split(s, delimiter)   
       result = {};
       for match in (s..delimiter):gmatch("(.-)"..delimiter) do
         table.insert(result, match);
       end
       return result;
    end

    function round(num, dec)
       if num == 0 then
         return 0
       else
         local mult = 10^(dec or 0)
         return math.floor(num * mult + 0.5) / mult
       end
    end

    commandArray = {}

    time = os.date("*t")
    if  ((time.min % 5)==0)  then -- Run every 5 minutes. Check the wundergroud API limitation before changing this

       json = (loadfile "/home/pi/domoticz/scripts/lua/JSON.lua")()  -- For Linux
       --json = (loadfile "D:\\Domoticz\\scripts\\lua\\json.lua")()  -- For Windows

       local arbitraryTwilightLux=6.32     -- W/m² egal 800 Lux     (the theoritical value is 4.74 but I have more accurate result with 6.32...)
       local constantSolarRadiation = 1361 -- Solar Constant W/m²
       
       if (uservariables['octa'] == nil) then print("Error : Did you create the Uservariable octa ?") end
       --  API Wunderground
       local config=assert(io.popen('curl http://api.wunderground.com/api/'..wuAPIkey..'/conditions/q/'..countryCode..'/'..city..'.json'))
       local location = config:read('*all')
       config:close()
       local jsonLocation = json:decode(location)
       if( DEBUG == 1) then
          local latitude = jsonLocation.current_observation.display_location.latitude
          local longitude = jsonLocation.current_observation.display_location.longitude
          local altitude = jsonLocation.current_observation.display_location.elevation
          print('Lat: '..latitude..'Long: '..longitude..'Alt: '..altitude)
       end
       relativePressure = jsonLocation.current_observation.pressure_mb   -- if you have an another way to get the Pressure, (local barometer ...) then you may optimize the script and avoid the call to api.wunderground)
       ----------------------------------
       local year = os.date("%Y")
       local numOfDay = os.date("%j")
       if  leapYear(year) == true then   
          nbDaysInYear = 366  -- How many days in the year ?
       else
          nbDaysInYear = 365
       end

       angularSpeed = 360/365.25
       local Declinaison = math.deg(math.asin(0.3978 * math.sin(math.rad(angularSpeed) *(numOfDay - (81 - 2 * math.sin((math.rad(angularSpeed) * (numOfDay - 2))))))))
       timeDecimal = (os.date("!%H") + os.date("!%M") / 60) -- Coordinated Universal Time  (UTC)
       solarHour = timeDecimal + (4 * longitude / 60 )    -- The solar Hour
       hourlyAngle = 15 * ( 12 - solarHour )          -- hourly Angle of the sun
       sunAltitude = math.deg(math.asin(math.sin(math.rad(latitude))* math.sin(math.rad(Declinaison)) + math.cos(math.rad(latitude)) * math.cos(math.rad(Declinaison)) * math.cos(math.rad(hourlyAngle))))-- the height of the sun in degree, compared with the horizon
          
       local azimuth = math.acos((math.sin(math.rad(Declinaison)) - 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(Declinaison)) * math.sin(math.rad(hourlyAngle))) / math.cos(math.rad(sunAltitude))
       if(sinAzimuth<0) then azimuth=360-azimuth end
       sunstrokeDuration = math.deg(2/15 * math.acos(- math.tan(math.rad(latitude)) * math.tan(math.rad(Declinaison)))) -- duration of sunstroke in the day . Not used in this calculation.
       RadiationAtm = constantSolarRadiation * (1 +0.034 * math.cos( math.rad( 360 * numOfDay / nbDaysInYear )))    -- Sun radiation  (in W/m²) in the entrance of atmosphere.
       -- Coefficient of mitigation M
       absolutePressure = relativePressure - round((altitude/ 8.3),1) -- hPa
       sinusSunAltitude = math.sin(math.rad(sunAltitude))
       M0 = math.sqrt(1229 + math.pow(614 * sinusSunAltitude,2)) - 614 * sinusSunAltitude
       M = M0 * relativePressure/absolutePressure

       if (DEBUG == 1) then
          print('<b style="color:Blue"==============  SUN  LOG ==================</b>')
          print(os.date("%Y-%m-%d %H:%M:%S", os.time()))
          print(city .. ", latitude:" .. latitude .. ", longitude:" .. longitude)
          print("Home altitude = " .. tostring(altitude) .. " m")
          print("number Of Day = " .. numOfDay)     
          if nbDaysInYear==366 then
             print(year .." is a leap year !")
          else
             print(year.." is not a leap year")
          end
          print("Angular Speed = " .. angularSpeed .. " per day")
          print("Declinaison = " .. Declinaison .. "°")
          print("Universel Coordinated Time (UTC)".. timeDecimal .." H.dd")
          print("Solar Hour ".. solarHour .." H.dd")
          print("Altitude of the sun = " .. sunAltitude .. "°")
          print("Angular hourly = ".. hourlyAngle .. "°")
          print("Azimuth of the sun = " .. azimuth .. "°")
          print("Duration of the sunstroke of the day = " .. round(sunstrokeDuration,2) .." H.dd")  -- not used
          print("Radiation max in atmosphere = " .. round(RadiationAtm,2) .. " W/m²")
          print("Local relative pressure = " .. relativePressure .. " hPa")
          print("Absolute pressure in atmosphere = " .. absolutePressure .. " hPa")
          print("Coefficient of mitigation M = " .. M .." M0:"..M0)
       end

       -- Get  SYNOP  message from  Ogimet web  site
       hourUTCminus1 = os.date("!%H")-1
       if string.len(hourUTCminus1) == 1 then
          hourUTCminus1 = "0" .. hourUTCminus1
       end
       UTC = os.date("%Y%m%d").. hourUTCminus1.."00" -- os.date("!%M")
       if (DEBUG == 1) then
          local WMOID = jsonLocation.current_observation.display_location.wmo
       end
       
       cmd='curl "http://www.ogimet.com/cgi-bin/getsynop?block='..WMOID..'&begin='..UTC..'"'
       if( DEBUG == 1) then print(cmd) end
       local ogimet=assert(io.popen(cmd))
       local synop = ogimet:read('*all')
       ogimet:close()
       if( DEBUG == 1) then print('ogimet:'..synop) end
          
       if string.find(synop,"Status: 500") == nil
       then   
          rslt = split(synop,",")
          CodeStation = rslt[1]
          rslt = split(synop, " "..CodeStation.. " ")
          Trame = string.gsub(rslt[2], "=", "")
          Trame = CodeStation .." ".. Trame
          rslt = split(Trame, " ")
          Octa = string.sub(rslt[3], 1, 1)  -- 3rd char is the cloud layer.  0=no cloud , 1-8= cloudy from 1 to 8 max , 9 =Fog , / = no data
          if Octa == "/" then   -- not defined ? take the previous value
             Octa = uservariables['octa']
          elseif Octa == "9" then
             Octa = 8
          end
       else
          Octa = uservariables['octa']
       end

       --os.execute('curl "http://127.0.0.1:8081/json.htm?type=command&param=updateuservariable&idx='..idxUserVarOcta..'&vname=octa&vtype=0&vvalue='..tostring(Octa)..'"')
       commandArray[#commandArray + 1] = {['Variable:octa'] = tostring(Octa)}
       
       Kc=1-0.75*math.pow(Octa/8,3.4)  -- Factor of mitigation for the cloud layer

       if sunAltitude > 1 then -- Below 1° of Altitude , the formulae reach their limit of precision.
          directRadiation = RadiationAtm * math.pow(0.6,M) * sinusSunAltitude
          scatteredRadiation = RadiationAtm * (0.271 - 0.294 * math.pow(0.6,M)) * sinusSunAltitude
          totalRadiation = scatteredRadiation + directRadiation
          Lux = totalRadiation / 0.0079  -- Radiation in Lux. 1 Lux = 0,0079 W/m²
          weightedLux = Lux * Kc   -- radiation of the Sun with the cloud layer
       elseif sunAltitude <= 1 and sunAltitude >= -7  then -- apply theoretical Lux of twilight
          directRadiation = 0
          scatteredRadiation = 0
          arbitraryTwilightLux=arbitraryTwilightLux-(1-sunAltitude)/8*arbitraryTwilightLux
          totalRadiation = scatteredRadiation + directRadiation + arbitraryTwilightLux 
          Lux = totalRadiation / 0.0079  -- Radiation in Lux. 1 Lux = 0,0079 W/m²
          weightedLux = Lux * Kc   -- radiation of the Sun with the cloud layer
       elseif sunAltitude < -7 then  -- no management of nautical and astronomical twilight...
          directRadiation = 0
          scatteredRadiation = 0
          totalRadiation = 0
          Lux = 0
          weightedLux = 0  --  should be around 3,2 Lux for the nautic twilight. Nevertheless.
       end
       
       if (DEBUG == 1) then   
          print("Station SYNOP = " .. WMOID)
          print( Octa .. " Octa")
          print("Kc = " .. Kc)
          print("Direct Radiation = ".. round(directRadiation,2) .." W/m²")
          print("Scattered Radiation = ".. round(scatteredRadiation,2) .." W/m²")
          print("Total radiation = " .. round(totalRadiation,2) .." W/m²")
          print("Total Radiation in lux = ".. round(Lux,2).." Lux")
          print("and at last, Total weighted lux  = ".. round(weightedLux,2).." Lux")   
        end

	if tonumber(otherdevices_svalues['Lux'])+round(weightedLux,0)>0   -- No update if Lux is already 0. So lastUpdate of the Lux sensor will keep the time when Lux has reached 0. (Kind of timeofday['SunsetInMinutes'])
	then
--		commandArray[#commandArray + 1] = {['UpdateDevice'] = idxLux..'|0|'..tostring(round(weightedLux,0))}    --  THis form is not recommended. due to limitation of the eventsystem of Domoticz
		commandArray[#commandArray + 1]={['OpenURL']="http://"..localhost.."/json.htm?type=command&param=udevice&idx="..idxLux.."&nvalue=0&svalue="..tostring(round(weightedLux,0)) }
	end
--      commandArray[#commandArray + 1] = {['UpdateDevice'] = idxSolarAzimuth..'|0|'..tostring(round(azimuth,0))} 
	commandArray[#commandArray + 1]={['OpenURL']="http://"..localhost.."/json.htm?type=command&param=udevice&idx="..idxSolarAzimuth.."&nvalue=0&svalue="..tostring(round(azimuth,0)) }
--      commandArray[#commandArray + 1] = {['UpdateDevice'] = idxSolarAltitude..'|0|'..tostring(round(sunAltitude,0))}
	commandArray[#commandArray + 1]={['OpenURL']="http://"..localhost.."/json.htm?type=command&param=udevice&idx="..idxSolarAltitude.."&nvalue=0&svalue="..tostring(round(sunAltitude,0)) }
       
       if (DEBUG == 2) then
          logDebug=os.date("%Y-%m-%d %H:%M:%S",os.time())
          logDebug=logDebug.." Azimuth:" .. azimuth .. " Height:" .. sunAltitude
          logDebug=logDebug.." Octa:" .. Octa.."  KC:".. Kc
          logDebug=logDebug.." Direct:"..directRadiation.." inDirect:"..scatteredRadiation.." TotalRadiation:"..totalRadiation.." LuxCloud:".. round(weightedLux,2)
          os.execute('echo '..logDebug..' >>logSun.txt')  -- compatible Linux & Windows
       end
    end
    return commandArray


dzVents version of the script

In case you prefer dzVents in instead of traditional Lua, here follows the dzVents version of the above script. You should choose to run one of the scripts only. The dzVents version works a bit differently. You shouldn't create any user variable and the virtual Lux device is optional.

Furthermore the script supports an additional Cloud Coverage sensor. If you'd like to use it, create a Domoticz virtual sensor (Type Procent) and name it "Cloud Cover". Note down the idx number and enter it in the configuration section of the script.

This script works in 2 stages. At first stage the two Internet calls are queued to the operating system. Doing it this way it won't introduce any delays in the Domoticz event queue while waiting for external servers to respond. At the second stage which will occur one minute later, the script reads the data fetched from temporary files on disk. The following 3 minutes the scripts does nothing, it just exits.

There is also another difference compared to the ordinary Lua script. That is that you will need to look up the nearest SYNOP Station (WMOID) by yourself and enter it into the configuration part. Choose the WMOID on the Ogimet web site.

At the start, the script is logging a lot of information in the Domoticz log. When you have checked that everything works well, you might want to comment the following line:

level = domoticz.LOG_INFO

making the script respect the default dzVents logging level.

If you'd like to discuss this dzVents version of the script, there is a topic on the Domoticz's forum for it.

If You are upgrading, please read the Change Log and Release Notices.

--[[
	Prerequisits
	==================================
	Requires Domoticz v3.8551 or later
	Platform dependent, requires Linux

	CHANGE LOG: See http://www.domoticz.com/forum/viewtopic.php?t=19220 

Virtual Lux sensor and other real-time solar data

-- Authors  ----------------------------------------------------------------
	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 - 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.
	V2.0 - BakSeeDaa - Converted to dzVents and changed quite many things.
]]--

-- Variables to customize ------------------------------------------------
local city = 'Paris'					-- City for Wunderground API (You can also use a pws here like 'pws:ISTOCKHO854')
local countryCode = 'SE'							-- Country code for Wunderground API
local idxSolarAzimuth = 625						-- (Integer) Virtual Azimuth Device ID
local idxSolarAltitude = 626					-- (Integer) Your virtual Solar Altitude Device I
local wuAPIkey = 'xxxxxxxxxxxx'		-- Weather Underground API Key
local WMOID = '09999'									-- (String) Your nearest synop station for ogimet.
local tmpFileWU = '/tmp/wuData.json' -- Temporary storage of WU API json result 
local tmpFileOgimet = '/tmp/OgimetData.txt' -- Temporary storage of Ogimet result
local logToFile = false									-- (Boolean) Set to true if you also wish to log to a file. It might get big by time. 
local tmpLogFile = '/tmp/logSun.txt'-- Logging to a file if specified 
local fetchIntervalMins = 5						-- (Integer) (Minutes, Range 5-60) How often Wunderground API is called 

-- Optional Domoticz devices
local idxLux = nil -- (Integer) Domoticz virtual Lux device ID
local idxCloudCover = nil -- (Integer) Domoticz Cloud Cover (PERCENTAGE TYPE) sensor device ID

-- (You don't have to configure these if you accept the values returned by the weather observation station)
local latitude = nil	-- Latitude. (Decimal number) Decimal Degrees. E.g. something like 51.748485
local longitude = nil	-- Longitude. (Decimal number) Decimal Degrees. E.g.something like 5.629728.
local altitude = nil	-- Altitude. (Integer) Meters above sea level.

local warnNoCloudDataHours = 12 -- Warn limit (hours) if no cloud cover report has been received.

-- Please don't make any changes below this line (Except for setting logging level)

local scriptVersion = '2.2.1'

return {
	active = true,
	logging = {
		--level = domoticz.LOG_DEBUG, -- Uncomment to override the dzVents global logging setting
		marker = 'SOLAR '..scriptVersion
	},
	on = {
		timer = {'every minute'} -- Don't change! Verander niet! Ne changez pas!
	},
	data = {
		lastOkta = {initial=0},
		lastOgimetTime = {initial='198001010000'}
	},
	execute = function(domoticz, device)

		if not warnNoCloudDataHours then -- New variable introduced in 2.0.6
			domoticz.log('You must define new variable warnNoCloudDataHours in configuration, please see the Wiki', domoticz.LOG_ERROR)
			return
		end

		local getData = false
		if (os.date('*t').min % fetchIntervalMins) == 0 then
			getData = true
		elseif ((os.date('*t').min -1) % fetchIntervalMins) ~= 0 then
			return
		end

		if getData then

			local url = 'http://api.wunderground.com/api/'..wuAPIkey..'/conditions/q/'..countryCode..'/'..city..'.json'
			domoticz.log('Requesting new weather data from Wunderground...', domoticz.LOG_DEBUG)
			domoticz.log('URL used: '..url, domoticz.LOG_DEBUG)
			os.execute('curl -s "'..url..'" > '..tmpFileWU..'&')

			local ogimetDelay = 1140 -- Minimum anticipated Ogimet data lag (19 minutes)
			local qOgimetTime = os.date('!%Y%m%d%H', os.time()- ogimetDelay)..'00'
			local lastOgimetTime = domoticz.data.lastOgimetTime
			if qOgimetTime > lastOgimetTime then
				-- There might be recent data to fetch
				qOgimetTime = os.date('!%Y%m%d%H', os.time()-(12*3600+ogimetDelay))..'00' -- Twelve hours of data
				if domoticz.data.lastOgimetTime > qOgimetTime then
					qOgimetTime = domoticz.data.lastOgimetTime:sub(1, -2)..'1' -- Add 1 minute to it
				end
				-- Get synopCode (surface synopCodetic observations) message from Ogimet web site
				url ='http://www.ogimet.com/cgi-bin/getsynop?block='..WMOID..'&begin='..qOgimetTime
				domoticz.log('Requesting new cloud cover data from Ogimet...', domoticz.LOG_DEBUG)
				domoticz.log('URL used: '..url, domoticz.LOG_DEBUG)
				os.execute('curl -s "'..url..'" > '..tmpFileOgimet..'&')
			else
				domoticz.log('No need to request new cloud cover data from Ogimet. Using old data with UTC time stamp: '..lastOgimetTime, domoticz.LOG_DEBUG)
			end

			return -- Nothing more to do for now, we'll be back in a minute to read the data!
		end

		local function leapYear(year)   
			return year%4==0 and (year%100~=0 or year%400==0)
		end

		---loads a json file with the provided name and returns it as a table (if it exists)
		local function readLuaFromJsonFile(fileName)
			local file = io.open(fileName, 'r')
			if file then
				package.path = package.path ..";./scripts/lua/?.lua;" -- This can be removed when using dzVents 2.3.1 or later
				local jsonParser = require('JSON')
				local _json = file:read('*a')
				local json = jsonParser:decode(_json)
				io.close(file)
				return json
			end
			return nil
		end

		local wuAPIData = readLuaFromJsonFile(tmpFileWU)
		if not wuAPIData then
			domoticz.log('Could not read wuAPIData from file: '.. tmpFileWU, domoticz.LOG_ERROR)
			return
		end
		domoticz.log('Wunderground API json data file has been read', domoticz.LOG_DEBUG)

		local arbitraryTwilightLux = 6.32 -- W/m² egal 800 Lux (the theoritical value is 4.74 but I have more accurate result with 6.32...)
		local constantSolarRadiation = 1361 -- Solar Constant W/m²

		-- In case of that latitude, longitude and altitude has not been defined in the configuration,
		-- we simply use the values that is returned for the current observation location.
		-- Reading longitude, latitude and altitude from the observation_location instead of from 
		-- display_location. API documentation is not so clear about what display_location is.
		if not latitude then latitude = wuAPIData.current_observation.observation_location.latitude end
		if not longitude then longitude = wuAPIData.current_observation.observation_location.longitude end
		if not altitude then
			altitude = wuAPIData.current_observation.observation_location.elevation
			altitude = domoticz.round(tonumber((altitude:gsub('[%a%s]',''))) / 3.2808, 1)
		end

		local WULocWMO = wuAPIData.current_observation.display_location.wmo
		local relativePressure = wuAPIData.current_observation.pressure_mb -- if you have an another way to get the Pressure, (local barometer ...) then you may optimize the script and avoid the call to api.wunderground)

		local year = os.date('%Y')
		local numOfDay = os.date('%j')
		local nbDaysInYear = (leapYear(year) and 366 or 365)

		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 timeDecimal = (os.date('!%H') + os.date('!%M') / 60) -- Coordinated Universal Time  (UTC)
		local solarHour = timeDecimal + (4 * longitude / 60 )    -- The solar Hour
		local hourlyAngle = 15 * ( 12 - solarHour )          -- hourly Angle of the sun
		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))))-- the height of the sun in degree, compared with the horizon

		local azimuth = 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(hourlyAngle))) / math.cos(math.rad(sunAltitude))
		if(sinAzimuth<0) then azimuth=360-azimuth 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 RadiationAtm = constantSolarRadiation * (1 +0.034 * math.cos( math.rad( 360 * numOfDay / nbDaysInYear ))) -- Sun radiation  (in W/m²) in the entrance of atmosphere.
		-- Coefficient of mitigation M
		local absolutePressure = relativePressure - domoticz.round((altitude/ 8.3),1) -- hPa
		local sinusSunAltitude = math.sin(math.rad(sunAltitude))
		local M0 = math.sqrt(1229 + math.pow(614 * sinusSunAltitude,2)) - 614 * sinusSunAltitude
		local M = M0 * relativePressure/absolutePressure

		domoticz.log('', domoticz.LOG_INFO)
		domoticz.log('==============  SUN  LOG ==================', domoticz.LOG_INFO)
		domoticz.log(city .. ', latitude: ' .. latitude .. ', longitude: ' .. longitude, domoticz.LOG_INFO)
		domoticz.log('Home altitude = ' .. tostring(altitude) .. ' m', domoticz.LOG_DEBUG)
		domoticz.log('WU Location WMO = ' .. WULocWMO, domoticz.LOG_INFO)
		domoticz.log('Angular Speed = ' .. angularSpeed .. ' per day', domoticz.LOG_DEBUG)
		domoticz.log('Declination = ' .. declination .. '°', domoticz.LOG_DEBUG)
		domoticz.log('Universal Coordinated Time (UTC) '.. timeDecimal ..' H.dd', domoticz.LOG_DEBUG)
		domoticz.log('Solar Hour '.. solarHour ..' H.dd', domoticz.LOG_DEBUG)
		domoticz.log('Altitude of the sun = ' .. sunAltitude .. '°', domoticz.LOG_INFO)
		domoticz.log('Angular hourly = '.. hourlyAngle .. '°', domoticz.LOG_DEBUG)
		domoticz.log('Azimuth of the sun = ' .. azimuth .. '°', domoticz.LOG_INFO)
		domoticz.log('Duration of the sun stroke of the day = ' .. domoticz.round(sunstrokeDuration,2) ..' H.dd', domoticz.LOG_DEBUG)
		domoticz.log('Radiation max in atmosphere = ' .. domoticz.round(RadiationAtm,2) .. ' W/m²', domoticz.LOG_DEBUG)
		domoticz.log('Local relative pressure = ' .. relativePressure .. ' hPa', domoticz.LOG_DEBUG)
		domoticz.log('Absolute pressure in atmosphere = ' .. absolutePressure .. ' hPa', domoticz.LOG_DEBUG)
		domoticz.log('Coefficient of mitigation M = ' .. M ..' M0 = '..M0, domoticz.LOG_DEBUG)
		domoticz.log('', domoticz.LOG_INFO)

		local function split(s, delimiter)
			local result = {}
			for match in (s..delimiter):gmatch('(.-)'..delimiter) do
				table.insert(result, match)
			end
			return result
		end

		-- In meteorology, an okta is a unit of measurement used to describe the amount of cloud cover
		-- at any given location such as a weather station. Sky conditions are estimated in terms of how many
		-- eighths of the sky are covered in cloud, ranging from 0 oktas (completely clear sky) through to 8 oktas
		-- (completely overcast). In addition, in the synop code there is an extra cloud cover indicator '9'
		-- indicating that the sky is totally obscured (i.e. hidden from view),
		-- usually due to dense fog or heavy snow.

		-- Return the okta value for the last valid line in the file.
		-- The file may contain multiple rows and some of them may not be valid.
		local function readLastSynopLine(fileName)
			local file = io.open(fileName, 'r')
			local okta, ogimetTime
			if file then
				while true do
					local line = file:read("*l")
					if line == nil then break end
					if (string.find(line,'NIL=') == nil)
					and (string.find(line,'Status: 500') == nil)
					and (string.find(line, WMOID) ~= nil) then
						local s = split(line, ',')
						if s and #s >= 7 then
							local x = string.sub(split(s[7], ' ')[5], 1, 1)
							if x ~= '/' then
								okta = x
								ogimetTime = s[2]..s[3]..s[4]..s[5]..s[6]
							end
						end
					end
				end
				io.close(file)
			end
			return okta, ogimetTime
		end

		local okta, ogimetTime = readLastSynopLine(tmpFileOgimet)
		local lastOgimetTime = domoticz.data.lastOgimetTime 
		if (not ogimetTime) or (lastOgimetTime >= ogimetTime) then 
			local lastOkta = domoticz.data.lastOkta

			domoticz.log('Using the saved Okta value: '..lastOkta..' with UTC timestamp: '..lastOgimetTime, domoticz.LOG_DEBUG)
			okta = lastOkta
			-- No cloud data. Shall we throw an error?
			local tLastOgimetTime = os.time({ year = tonumber(lastOgimetTime:sub(1,4)), month = tonumber(lastOgimetTime:sub(5,6)), 
					day = tonumber(lastOgimetTime:sub(7,8)), hour = tonumber(lastOgimetTime:sub(9,10)), min = tonumber(lastOgimetTime:sub(11,12)) })
			local elapsed_time = os.difftime(os.time(os.date("!*t")), tLastOgimetTime)
			if elapsed_time >= 3600 * warnNoCloudDataHours then
				domoticz.log('We\'ve got no cloud data from WMOID: '..WMOID..' for more than '..tostring(domoticz.round(elapsed_time/3600))..' hours. Maybe you should look for a more reliable weather station to query. Read the Wiki how to do that.', domoticz.LOG_ERROR)
			end
		else
			okta = okta == '9' and 8 or okta
			-- We store the last fetched value here to be used as a backup value
			domoticz.log('Using the newly fetched Okta value: '..okta..' with UTC timestamp: '..ogimetTime, domoticz.LOG_DEBUG)
			domoticz.data.lastOkta = okta
			domoticz.data.lastOgimetTime = ogimetTime

		end

		local Kc = 1-0.75*math.pow(okta/8,3.4)  -- Factor of mitigation for the cloud layer

		local directRadiation, scatteredRadiation, totalRadiation, Lux, weightedLux
		if sunAltitude > 1 then -- Below 1° of Altitude , the formulae reach their limit of precision.
			directRadiation = RadiationAtm * math.pow(0.6,M) * sinusSunAltitude
			scatteredRadiation = RadiationAtm * (0.271 - 0.294 * math.pow(0.6,M)) * sinusSunAltitude
			totalRadiation = scatteredRadiation + directRadiation
			Lux = totalRadiation / 0.0079  -- Radiation in Lux. 1 Lux = 0,0079 W/m²
			weightedLux = Lux * Kc   -- radiation of the Sun with the cloud layer
		elseif sunAltitude <= 1 and sunAltitude >= -7  then -- apply theoretical Lux of twilight
			directRadiation = 0
			scatteredRadiation = 0
			arbitraryTwilightLux=arbitraryTwilightLux-(1-sunAltitude)/8*arbitraryTwilightLux
			totalRadiation = scatteredRadiation + directRadiation + arbitraryTwilightLux 
			Lux = totalRadiation / 0.0079  -- Radiation in Lux. 1 Lux = 0,0079 W/m²
			weightedLux = Lux * Kc   -- radiation of the Sun with the cloud layer
		elseif sunAltitude < -7 then  -- no management of nautical and astronomical twilight...
			directRadiation = 0
			scatteredRadiation = 0
			totalRadiation = 0
			Lux = 0
			weightedLux = 0  --  should be around 3,2 Lux for the nautic twilight. Nevertheless.
		end

		domoticz.log('SYNOP Station = ' .. WMOID, domoticz.LOG_INFO)
		domoticz.log('Okta = '..okta, domoticz.LOG_INFO)
		domoticz.log('Kc = ' .. Kc, domoticz.LOG_DEBUG)
		domoticz.log('Direct Radiation = '.. domoticz.round(directRadiation,2) ..' W/m²', domoticz.LOG_INFO)
		domoticz.log('Scattered Radiation = '.. domoticz.round(scatteredRadiation,2) ..' W/m²', domoticz.LOG_DEBUG)
		domoticz.log('Total radiation = ' .. domoticz.round(totalRadiation,2) ..' W/m²', domoticz.LOG_DEBUG)
		domoticz.log('Total Radiation in lux = '.. domoticz.round(Lux,2)..' Lux', domoticz.LOG_DEBUG)
		domoticz.log('Total weighted lux  = '.. domoticz.round(weightedLux,2)..' Lux', domoticz.LOG_INFO)

		-- No update if Lux is already 0. So lastUpdate of the Lux sensor will keep the time when Lux has reached 0.
		-- (Kind of timeofday['SunsetInMinutes'])
		if idxLux and domoticz.devices(idxLux).lux + domoticz.round(weightedLux, 0) > 0 then
			domoticz.devices(idxLux).updateLux(domoticz.round(weightedLux,0))
		end
		domoticz.devices(idxSolarAzimuth).updateCustomSensor(domoticz.round(azimuth,0))
		domoticz.devices(idxSolarAltitude).updateCustomSensor(domoticz.round(sunAltitude,0))
		local oktaPercent = domoticz.round(okta*100/8)
		if idxCloudCover and ((domoticz.devices(idxCloudCover).percentage ~= oktaPercent)
		or (domoticz.devices(idxCloudCover).lastUpdate.minutesAgo >= (60 - fetchIntervalMins))) then
			domoticz.devices(idxCloudCover).updatePercentage(oktaPercent)
		end 

		if logToFile then
			local logDebug = os.date('%Y-%m-%d %H:%M:%S',os.time())
			logDebug=logDebug..' Azimuth:' .. azimuth .. ' Height:' .. sunAltitude
			logDebug=logDebug..' Okta:' .. okta..'  KC:'.. Kc
			logDebug=logDebug..' Direct:'..directRadiation..' inDirect:'..scatteredRadiation..' TotalRadiation:'..totalRadiation..' LuxCloud:'.. domoticz.round(weightedLux,2)
			os.execute('echo '..logDebug..' >>'..tmpLogFile)  -- compatible Linux & Windows
		end

	end
}

Enjoy!

I hope you will enjoy it and I will be very happy to know the use that you will make of it. The forumtopic about this script can be found here: [8]