Code: Select all
--[[
Virtual Okta sensor (only)
from https://www.domoticz.com/forum/viewtopic.php?f=72&t=19220
from V2.4 - BakSeeDaa - dzVents version of the Solar Data Script
see also
http://www.ogimet.com/display_synops2.php?lang=en&lugar=07149&tipo=ALL&ord=REV&nil=SI&fmt=html&ano=2019&mes=03&day=30&hora=10&anof=2019&mesf=03&dayf=31&horaf=23&send=send
see https://www.ogimet.com/getsynop_help.phtml.en
https://www.ogimet.com/synops.phtml.en (web GUI)
to check for Orly => https://www.infoclimat.fr/observations-meteo/temps-reel/orly-athis-mons/07149.html
--05/04/2020: prerequisite: requires dzVents >= 3.0.17 (domoticz 2020.2 build 12702)
]]--
-- Variables to customize ------------------------------------------------
local WMOID = '07149' -- Paris-Orly
--local WMOID = '07156' -- Paris-Montsouris -- (String) Nearest synop station for ogimet
local warnNoCloudDataHours = 12 -- Warn limit (hours) if no cloud cover report has been received.
local idxCloudCover = 1144 -- (Integer) dz Cloud Cover (PERCENTAGE TYPE) sensor device ID
local idxOkta = 1145 -- (Integer) (Custom) sensor device ID
local HTTPCALLBACK = 'getOgimet'..WMOID
return {
logging = {
level =
domoticz.LOG_ERROR, --select one to override system log level normal = LOG_ERROR
--domoticz.LOG_DEBUG,
--domoticz.LOG_INFO,
--domoticz.LOG_ERROR,
--domoticz.LOG_FORCE
},
on = {
--devices = {206}, -- a switch for testing w/o waiting minutes
timer = {'at *:15', 'at *:25', 'at *:30', 'at *:35', 'at *:55'}, -- to tune with your service location to limit errors, http service not available always
httpResponses = {
HTTPCALLBACK,
},
},
data = {
lastOkta = {initial=0},
lastOgimetTime = {initial='198001010000'},
lastOgimetChange = {initial=nill}
},
execute = function(dz, item)
_G.logMarker = dz.moduleLabel -- set logmarker to scriptname
local LOG_LEVEL =
dz.LOG_INFO
--dz.LOG_DEBUG
--dz.LOG_ERROR
--dz.LOG_FORCE
-- FUNCTIONS ----
-- calculate the delta between UTC and local time
local function getTimezone()
local now = os.time()
return math.floor(os.difftime(now, os.time(os.date("!*t", now))))
end
-- convert UTC time to local time
local function UTCtoLocal(p_UTCtime)
-- Convert the string to an epoch time stamp and add timezone delta in seconds
local myLocalTimestamp = dz.time.dateToTimestamp(p_UTCtime,'(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)' ) + getTimezone()
-- Convert the caclulated timestamp to the required format
local localTimeDDMM = dz.time.timestampToDate(myLocalTimestamp,'dd/mm')
local localTimeHHMM = dz.time.timestampToDate(myLocalTimestamp,'hh:MM')
dz.log('UTC time ' .. p_UTCtime .. ' = local Time ' .. localTimeDDMM .. ' ' .. localTimeHHMM, dz.LOG_DEBUG)
return localTimeDDMM, localTimeHHMM
end
-- MAIN -----
local lastOgimetTime = dz.data.lastOgimetTime
local lastOgimetChange = dz.data.lastOgimetChange
dz.log("lastOgimetTime: " .. lastOgimetTime, LOG_LEVEL)
local Time = require('Time')
local theTime = Time()
dz.log("theTime: " .. theTime.raw, LOG_LEVEL)
if lastOgimetChange == nill then
dz.log('lastOgimetChange NIL!!!', dz.LOG_ERROR)
lastOgimetChange = theTime
dz.data.lastOgimetChange = lastOgimetChange
end
dz.log("lastOgimetChange: " .. lastOgimetChange.raw, LOG_LEVEL)
if item.isTimer or item.isDevice then
local ogimetDelay = 1140 -- Minimum anticipated Ogimet data lag (19 minutes)
local qOgimetTime = os.date('!%Y%m%d%H', os.time()- ogimetDelay)..'00' -- in UTC time
dz.log("qOgimetTime1: " .. qOgimetTime, LOG_LEVEL)
if qOgimetTime > lastOgimetTime or item.isDevice then -- item.isDevice is to force the call for testing with a dummy switch (could generate http errors if too often)
-- 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
dz.log("qOgimetTime2: " .. qOgimetTime, LOG_LEVEL)
if lastOgimetTime > qOgimetTime then
qOgimetTime = lastOgimetTime:sub(1, -2)..'1' -- Add 1 minute to it
dz.log( "Add 1 minute to qOgimetTime: " .. qOgimetTime, LOG_LEVEL)
end
-- Get synopCode (surface synopCodetic observations) message from Ogimet web site
url ='http://www.ogimet.com/cgi-bin/getsynop?block='..WMOID..'&begin='..qOgimetTime
dz.log( "url= " .. url, LOG_LEVEL)
dz.log('Requesting new cloud cover data from Ogimet...', LOG_LEVEL)
dz.openURL({url = url, method = 'GET', callback = HTTPCALLBACK}).afterSec(5)
else
dz.log('No need to request new cloud cover data from Ogimet. Using old data with UTC time stamp: '..lastOgimetTime, LOG_LEVEL)
return
end
end
if not item.isHTTPResponse then
dz.log('Response not http' , LOG_LEVEL)
return
end
local response = item
dz.log("response.data", LOG_LEVEL)
dz.log('\n' .. response.data, LOG_LEVEL)
if response == nil then
dz.log('http response null. Trigger: '..response.trigger, LOG_LEVEL)
return
end
if not response.ok then
dz.log('http response not ok. Trigger: '..response.trigger, dz.LOG_ERROR)
return
end
if (response.trigger ~= HTTPCALLBACK) then
dz.log('http response not right trigger. Trigger: '..response.trigger, dz.LOG_ERROR)
return
end
dz.log('Ogimet data has been received', LOG_LEVEL)
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.
-- Total cloud cover in oktas (eighths)
-- which is actually seen by the observer during the observation
-- Zero means absolutely clear, while 8 means absolutely cloudy
-- Code 9 signifies that the sky is obscured by fog, haze, and/or other phenomena
-- and / means that the cloud cover is indiscernable or that the sky observation was not made (but it is a valid answer)
-- 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
local lastOktaOk = false
local OktaOk = false
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 -- the 7th block separated with ,
local x = string.sub(split(s[7], ' ')[5], 1, 1) -- the 5th block separated with blanks
-- okta is the 1st char of this block
-- dz.log('x of line: '.. x, LOG_LEVEL)
if x ~= '/' then
okta = x
ogimetTime = s[2]..s[3]..s[4]..s[5]..s[6]
lastOgimetChange=theTime
OktaOk = true
dz.log('New okta: '.. okta, LOG_LEVEL)
--dz.log('=> ogimetTime: '.. ogimetTime, LOG_LEVEL)
else -- '/' means, the response is correct, but no new value is given, we keep the previous one
lastOktaOk = true
ogimetTime = s[2]..s[3]..s[4]..s[5]..s[6]
dz.log('Last okta', LOG_LEVEL)
end
end
end
end
if ogimetTime ~= nil then
dz.log('ogimetTime: '.. ogimetTime, LOG_LEVEL)
if lastOgimetTime >= ogimetTime then
dz.log('New value got is older than the previous', LOG_LEVEL)
OktaOk = false
end
else
dz.log('ogimetTime: nil', LOG_LEVEL)
OktaOk = false
end
local lastOkta = dz.data.lastOkta
dz.log('lastOkta: '.. lastOkta, LOG_LEVEL)
dz.log('lastOgimetTime: '.. lastOgimetTime, LOG_LEVEL)
dz.log('lastOgimetChange: '.. lastOgimetChange.raw, LOG_LEVEL)
local elapseTimeHours=theTime.compare(lastOgimetChange).hours
dz.log('elapseTimeHours: ' .. elapseTimeHours, LOG_LEVEL)
if elapseTimeHours > warnNoCloudDataHours then
dz.log('! No data from WMOID: ' .. WMOID.. " for more than " .. elapseTimeHours .. " hours", dz.LOG_FORCE)
--to look for a more reliable weather station to query?
return
end
dz.data.lastOgimetChange = lastOgimetChange
if not OktaOk then
--dz.log('!!!!!!!!!!!! ok !!!!!!! Using the saved Okta value: '..lastOkta..' with UTC timestamp: '..lastOgimetTime, dz.LOG_DEBUG)
-- okta = lastOkta
dz.log('No new value', dz.LOG_FORCE)
if lastOktaOk then -- the previous value is ok
okta = lastOkta
OktaOk = true
dz.log('Previous value to keep', dz.LOG_FORCE)
end
end
if OktaOk then
if okta == '9' then okta = '8' end -- max value for cloud cover
--okta = okta == '9' and 8 or okta -- I don't understand the previous line !!
-- We store the last fetched value here to be used as a backup value
dz.log('Okta value: '..okta..' with UTC '..ogimetTime, LOG_LEVEL)
dz.data.lastOkta = okta
dz.data.lastOgimetTime = ogimetTime
local oktaPercent = dz.utils.round(okta*100/8)
local OktaTimeDDMM, OktaTimeHHMM = UTCtoLocal(ogimetTime)
dz.devices(idxCloudCover).updatePercentage(oktaPercent)
local OktaTimeDDMM, OktaTimeHHMM = UTCtoLocal(ogimetTime)
dz.log('OktaTimeDDMM ' .. OktaTimeDDMM, dz.LOG_DEBUG)
dz.log('OktaTimeHHMM ' .. OktaTimeHHMM, dz.LOG_DEBUG)
dz.devices(idxOkta).rename('Okta ' .. OktaTimeHHMM)
dz.devices(idxOkta).updateCustomSensor(okta)
dz.log('Okta to: ' .. dz.devices(idxOkta).name .. "= ".. okta .. ' with UTC '.. OktaTimeDDMM .. OktaTimeHHMM, dz.LOG_INFO)
end
dz.log('SYNOP Station = ' .. WMOID, LOG_LEVEL)
end
}
Except if needed....