(Requires Domoticz v3.8837 or later)
(Platform dependent, requires Linux)
With permission of the original author jmleglise I have translated the Real-time solar data : azimuth, Altitude, Lux sensor script into dzVents and I also made several changes to it.
The most important change is that it won't introduce any delays in the Domoticz event queue while waiting for external servers to respond.
Get it here:
The dzVents version of the Solar Data Script source can be found at the end of this script. The version found in the wiki is obsolete but since I have a problem to access the wiki with my user account I can not update it. (Yes, I've asked for support but I've been given none)
Cloud cover graph: Change log and release notices
Version 2.4.0.
- Various changes to take full advantage of dzVents 2.4.0. This change makes this script require Domoticz v3.8837 or later
- Using the dzVents round() function. This change makes this script require Domoticz v3.8551 or later
- Fixed so that the Cloud Cover sensor gets updated at least once an hour. Without this fix, the GUI representation of the device can get red borders if the cloud cover remains at the same value for more than an hour.
- Now supports Cloud Cover sensor. To use it, create a new Domoticz virtual device (type: Percentage) and enter its idx number in the configuration parameter documented below. Cloud coverage data will update at a maximum rate of once an hour depending on the synop station that you are using.
- New configuration value: idxCloudCover. (Integer) Domoticz Cloud Cover (PERCENTAGE TYPE) sensor device ID
- New configuration value: warnNoCloudDataHours. Warn limit (hours) if no cloud cover report has been received within the time specified, an error will be thrown into the Domoticz log. This is useful because some stations don't report cloud cover regularly and in such cases a notification is needed so that you can change to a better station. (WMOID, Nearest synop station for ogimet)
- New configuration value: jsonLib. Edit this value to show the path to the JSON Lua library on your system.
- Querying the Ogimet API for cloud cover data in a much smarter way and only when needed. As been seen, the cloud cover data part reported by the Ogimet service (surface synopCodetic observations) only updates hourly. (With at least 20 minutes delay ). The script keeps a record of the last received data and won't make unnecessary calls. It drastically reduces the number of calls using the Ogimet API. That's good for both your server and for the Ogimet server and it will not make the results of this script less accurate or less frequently updated. Furthermore the script now handles multiple result lines from the Ogimet API. The script always reads the last report returned by Ogimet. This change has been necessary to handle situations where the Ogimet server doesn't report hourly (Which is common for some stations).
- Various fixes, code improvements etc.
- Reverted previously introduced limitation to fetch Cloud Layer Data from Ogimet only once an hour.
- We shouldn't query the free Weather Information Service provided by www.ogimet.com more often than necessary. Previously a new query was made once every 5 minutes (depending on the interval that you've set) even though new data is available only once per hour. Now we only query the Ogimet API in case we expect that new Ogimet data is available (or in case the saved data in tmp folder is old or gets lost). This change will result in 1 or 2 queries per hour instead of 12 queries. Their service is free and 12 queries per hour is a bit abusive.
- The logging level domoticz.LOG_INFO now produces less information. To see all kind of irrelevant messages, set logging level to domoticz.LOG_DEBUG. For normal use, make the level line in the script to become a comment, e.g. preceed with --
Source:
Code: Select all
--[[
Prerequisits
==================================
Domoticz v3.8837 or later (dzVents version 2.4 or later)
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 = 'FR' -- Country code for Wunderground API
local idxSolarAzimuth = 999 -- (Integer) Virtual Azimuth Device ID
local idxSolarAltitude = 998 -- (Integer) Your virtual Solar Altitude Device I
local wuAPIkey = 'yourkeyhere' -- Weather Underground API Key
local WMOID = '0999' -- (String) Nearest synop station for ogimet.
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 this file if logging to file is activated
local fetchIntervalDayMins = 15 -- Day time scraping interval. Never set this below 4 if you have a free WU API account.
local fetchIntervalNightMins = 30 -- Night time scraping interval.
-- Optional Domoticz devices
--local idxLux = 888 -- (Integer) Domoticz virtual Lux device ID
--local idxCloudCover = 555 -- (Integer) Domoticz Cloud Cover (PERCENTAGE TYPE) sensor device ID
-- (You don't have to configure these if you accept the value of the weather observation station)
local latitude = nil -- Latitude. (Decimal number) Decimal Degrees. E.g. something like 50.748485
local longitude = nil -- Longitude. (Decimal number) Decimal Degrees. E.g.something like 12.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 scriptName = 'solarData'
local scriptVersion = '2.4.0'
return {
active = true,
logging = {
--level = domoticz.LOG_DEBUG, -- Uncomment to override the dzVents global logging setting
marker = scriptName..' '..scriptVersion
},
on = {
timer = {
'every '..tostring(fetchIntervalDayMins)..' minutes at daytime',
'every '..tostring(fetchIntervalNightMins)..' minutes at nighttime',
},
httpResponses = {
scriptName..'Ogimet',
scriptName..'WU',
},
},
data = {
lastOkta = {initial=0},
lastOgimetTime = {initial='198001010000'}
},
execute = function(domoticz, item)
if item.isTimer 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.openURL({url = url, method = 'GET', callback = scriptName..'WU'}).afterSec(10)
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 ogimet 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.openURL({url = url, method = 'GET', callback = scriptName..'Ogimet'}).afterSec(5)
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
end
if not item.isHTTPResponse then return end
local response = item
if (not response.ok) or (not response.isJSON and response.trigger ~= scriptName..'Ogimet') then
domoticz.log('Last http response was not what expected. Trigger: '..response.trigger, domoticz.LOG_ERROR)
domoticz.log(response.data, domoticz.LOG_ERROR)
return
end
if response.trigger == scriptName..'Ogimet' then
domoticz.log('Ogimet data has been received', domoticz.LOG_DEBUG)
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.
-- Find the okta value for the last valid line in the response
-- The response may contain multiple rows and some of them may not be valid.
local okta, ogimetTime
for line in response.data:gmatch("[^\r\n]+") do
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
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.utils.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
return
end
if response.trigger ~= scriptName..'WU' then return end
domoticz.log('Wunderground API json data has been received', domoticz.LOG_DEBUG)
local function leapYear(year)
return year%4==0 and (year%100~=0 or year%400==0)
end
local wuAPIData = response.json
if not wuAPIData then
domoticz.log('Could not find any wuAPIData in the WU API response', domoticz.LOG_ERROR)
return
end
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.utils.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.utils.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.utils.round(sunstrokeDuration,2) ..' H.dd', domoticz.LOG_DEBUG)
domoticz.log('Radiation max in atmosphere = ' .. domoticz.utils.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
local okta = domoticz.data.lastOkta
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.utils.round(directRadiation,2) ..' W/m²', domoticz.LOG_INFO)
domoticz.log('Scattered Radiation = '.. domoticz.utils.round(scatteredRadiation,2) ..' W/m²', domoticz.LOG_DEBUG)
domoticz.log('Total radiation = ' .. domoticz.utils.round(totalRadiation,2) ..' W/m²', domoticz.LOG_DEBUG)
domoticz.log('Total Radiation in lux = '.. domoticz.utils.round(Lux,2)..' Lux', domoticz.LOG_DEBUG)
domoticz.log('Total weighted lux = '.. domoticz.utils.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.utils.round(weightedLux, 0) > 0 then
domoticz.devices(idxLux).updateLux(domoticz.utils.round(weightedLux,0))
end
domoticz.devices(idxSolarAzimuth).updateCustomSensor(domoticz.utils.round(azimuth,0))
domoticz.devices(idxSolarAltitude).updateCustomSensor(domoticz.utils.round(sunAltitude,0))
local oktaPercent = domoticz.utils.round(okta*100/8)
local fetchIntervalMins = (domoticz.time.matchesRule('at daytime') and fetchIntervalDayMins or fetchIntervalNightMins)
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.utils.round(weightedLux,2)
os.execute('echo '..logDebug..' >>'..tmpLogFile) -- compatible Linux & Windows
end
end
}