From e32107a7fe79ce34f3bdf860410a6d5455efdca7 Mon Sep 17 00:00:00 2001 From: Roman Bazalevsky Date: Mon, 12 Nov 2018 20:18:30 +0300 Subject: [PATCH 1/1] =?utf8?q?-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?utf8?q?=D0=B5=D0=BD=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=20?= =?utf8?q?=D0=B4=D0=BB=D1=8F=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F=20iio-?= =?utf8?q?=D0=B4=D0=B0=D1=82=D1=87=D0=B8=D0=BA=D0=BE=D0=B2=20-=20=D0=94?= =?utf8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE?= =?utf8?q?=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5?= =?utf8?q?=D0=BE=D1=82=D0=BE=D1=81=D0=BB=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?utf8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20=D0=BE=D1=87?= =?utf8?q?=D0=B5=D1=80=D0=B5=D0=B4=D0=B8=20-=20=D0=94=D0=BE=D0=B1=D0=B0?= =?utf8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB?= =?utf8?q?=D1=8C=D0=BD=D0=BE=D0=B5=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB?= =?utf8?q?=D0=B8=D1=89=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?utf8?q?=D0=B8=20=D0=BB=D0=B5=D0=B3=D0=BA=D0=BE=D0=B2=D0=B5=D1=81=D0=BD?= =?utf8?q?=D1=8B=D0=B9=20=D0=B2=D0=B5=D0=B1-=D0=B8=D0=BD=D1=82=D0=B5=D1=80?= =?utf8?q?=D1=84=D0=B5=D0=B9=D1=81=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?utf8?q?=D0=BB=D0=B5=D0=BD=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82?= =?utf8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B9=20=D0=B1=D1=8D=D0=BA?= =?utf8?q?=D0=B0=D0=BF=20(=D1=80=D0=B5=D0=B3=D1=83=D0=BB=D1=8F=D1=80=D0=BD?= =?utf8?q?=D0=BE=20=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5?= =?utf8?q?=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- bin/dump-current | 7 + bin/dump-yesterday | 13 + bin/weather-backlog | 77 ++++++ bin/weather-backup | 15 + bin/weather-display | 262 ++++++++++++++++++ bin/weather-filter | 242 ++++++++++++++++ bin/weather-init | 33 +++ weathermon.lua => bin/weathermon | 192 +++++++------ weathermon-iio => bin/weathermon-iio | 54 +--- config/weathermon.uci | 158 +++++++++++ init.d/weather-display | 28 ++ init.d/weather-init | 10 + init.d/weathermon | 47 ++++ lib/wm_util.lua | 106 +++++++ openwrt-web/cgi-bin/meteo.lua | 110 ++++++++ openwrt-web/meteo/archive.html | 48 ++++ openwrt-web/meteo/archive.js | 322 ++++++++++++++++++++++ openwrt-web/meteo/graph.html | 38 +++ openwrt-web/meteo/graph.js | 206 ++++++++++++++ openwrt-web/meteo/index.html | 47 ++++ openwrt-web/meteo/meteo.css | 200 ++++++++++++++ openwrt-web/meteo/meteo.js | 85 ++++++ openwrt-web/meteo/properties.json | 44 +++ openwrt-web/meteo/weather.svg | 39 +++ filter_meteo.py => server/filter_meteo.py | 0 weathermon-mqtt => server/weathermon-mqtt | 0 weathermon_cron => server/weathermon_cron | 0 weathermon.conf | 21 -- weathermon.uci | 64 ----- 29 files changed, 2254 insertions(+), 214 deletions(-) create mode 100755 bin/dump-current create mode 100755 bin/dump-yesterday create mode 100755 bin/weather-backlog create mode 100755 bin/weather-backup create mode 100755 bin/weather-display create mode 100755 bin/weather-filter create mode 100755 bin/weather-init rename weathermon.lua => bin/weathermon (70%) rename weathermon-iio => bin/weathermon-iio (81%) create mode 100644 config/weathermon.uci create mode 100755 init.d/weather-display create mode 100755 init.d/weather-init create mode 100755 init.d/weathermon create mode 100644 lib/wm_util.lua create mode 100755 openwrt-web/cgi-bin/meteo.lua create mode 100644 openwrt-web/meteo/archive.html create mode 100644 openwrt-web/meteo/archive.js create mode 100644 openwrt-web/meteo/graph.html create mode 100644 openwrt-web/meteo/graph.js create mode 100644 openwrt-web/meteo/index.html create mode 100644 openwrt-web/meteo/meteo.css create mode 100644 openwrt-web/meteo/meteo.js create mode 100644 openwrt-web/meteo/properties.json create mode 100644 openwrt-web/meteo/weather.svg rename filter_meteo.py => server/filter_meteo.py (100%) rename weathermon-mqtt => server/weathermon-mqtt (100%) rename weathermon_cron => server/weathermon_cron (100%) delete mode 100644 weathermon.conf delete mode 100644 weathermon.uci diff --git a/bin/dump-current b/bin/dump-current new file mode 100755 index 0000000..c7670b9 --- /dev/null +++ b/bin/dump-current @@ -0,0 +1,7 @@ +#!/bin/sh + +logdb=`uci get weathermon.process.logdb` +dumpdir=/var/weather/www +mkdir -p $dumpdir +/usr/bin/weather-filter $logdb dump-compacted - $dumpdir + diff --git a/bin/dump-yesterday b/bin/dump-yesterday new file mode 100755 index 0000000..457792f --- /dev/null +++ b/bin/dump-yesterday @@ -0,0 +1,13 @@ +#!/bin/sh + +yesterday=`echo "print(os.date(\"%Y-%m-%d\",os.time()-24*60*60))" | /usr/bin/lua` + +logdb=`uci get weathermon.process.logdb` + +dumpdir=`uci get weathermon.process.archive_dir`/`echo "print(os.date(\"%Y/%m/%d\",os.time()-22*60*60))" | /usr/bin/lua` + +mkdir -p $dumpdir + +/usr/bin/weather-filter $logdb dump-compacted $yesterday $dumpdir + +sqlite3 $logdb "delete from log where time_stamp=range[1] and value (os.time() - mute_time)) + + if mod then + + if last < mod then + last = mod + res,sensor_data = pcall(function () + local f = io.open(watch_file,"r") + content = f:read("*all") + io.close(f) + return json.decode(content) + end) + + write_file(w_led,w_engage) + + else + + write_file(w_led,w_disengage) + + end + + local values = {} + local printable = {} + + for sensor,sensor_params in pairs(sensor_data[web_id]) do + if sensor ~= "timestamp" then + for param,value in pairs(sensor_params) do + local name = sensor.."."..param + values[name]=value + if a_formats[name] then + printable[name]=string.format("%"..a_formats[name][1],value*a_formats[name][2]) + end + end + end + end + + level = 1 + alarms = "" + + if not muted_beep then + for key,value in pairs(values) do + value_level = check_limit(key,value,limits) + if value_level > level then + level = value_level + alarms = a_names[key] + elseif value_level == level then + alarms = alarms.." "..a_names[key] + end + end + end + + leds_engage = {} + + for key,value in pairs(a_engage[level]) do + leds_engage[value] = a_engage_mode[level] + end + + for led,mode in pairs(a_leds) do + if leds_engage[led] then + write_file(led,leds_engage[led]) + else + write_file(led,mode) + end + end + + alarms = trim(alarms) + alarmstr = a_levels[level]..": "..alarms + + if alarmstr:len()>alarmstr_len then + alarmstr=alarmstr:sub(1,alarmstr_len) + elseif alarmstr:len()=alarm_raise) then + data = alarmstr..sensstr + else + data = timestr..sensstr + end + + if old_data ~= data then + + old_data = data + print(data) + write_file(out_file,data) + + end + + end + + socket.sleep(1) + +end diff --git a/bin/weather-filter b/bin/weather-filter new file mode 100755 index 0000000..1fc798f --- /dev/null +++ b/bin/weather-filter @@ -0,0 +1,242 @@ +#!/usr/bin/lua + +local uci = require("uci") +local cur = uci.cursor() +local json = require "json" + +local logdb = arg[1] + +require "wm_util" + +if not logdb then + + print("no sqlite log defined!") + return + +end + +function shallowcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in pairs(orig) do + copy[orig_key] = orig_value + end + else -- number, string, boolean, etc + copy = orig + end + return copy +end + +function median(dataset) + + table.sort(shallowcopy(dataset)) + return dataset[math.floor(#dataset/2)] + +end + +function filter_data(dataset,width) + + if not width then + width = 7 + end + + local result = {} + + local window_spread = math.floor(width/2) + local window = {} + + for i = 1,window_spread do + window[#window+1] = dataset[i]["y"] + end + + for key,value in pairs(dataset) do + nextelem = dataset[key+window_spread] + if nextelem then + window[#window+1] = nextelem["y"] + end + if not nextelem or #window>width then + table.remove(window,1) + end + row = {} + row["t"]=value["t"] + row["y"]=median(window) + result[#result+1] = row + end + + return result + +end + +function average_results(dataset,con) + local name = os.tmpname() + touch(name) + local tmpcon = assert(env:connect(name)) + assert(tmpcon:execute("create table series(time_stamp datetime,value float)")) + for key,value in pairs(dataset) do + assert(tmpcon:execute(string.format("INSERT INTO series(time_stamp,value) VALUES ('%s','%s')",value["t"],value["y"]))) + end + local sql = "select rounded as t,avg(value) as y from (select substr(strftime('%Y-%m-%dT%H:%M',time_stamp),1,15)||'5:00' rounded,value from series) group by rounded order by rounded" + results = run_sql(sql,tmpcon) + tmpcon:close() + os.remove(name) + return results +end + +function run_sql(sql,con) + local result = {} + + cursor = assert(con:execute(sql)) + row = cursor:fetch ({}, "a") + while row do + result[#result+1] = row + row = cursor:fetch ({}, "a") + end + + return result +end + +function get_list(day,con) + if day == "-" then + sql = string.format("SELECT DISTINCT sensor_id,sensor,param FROM log WHERE time_stamp>=datetime('now','-1 day','localtime') ORDER BY sensor_id,sensor,param") + else + sql = string.format("SELECT DISTINCT sensor_id,sensor,param FROM log WHERE time_stamp>='%s' and time_stamp=datetime('now','-1 day','localtime') and sensor_id='%s' and sensor='%s' and param='%s' ORDER BY time_stamp",format,sensor_id,sensor_type,param) + else + sql = string.format("SELECT strftime('%s',time_stamp) as t,value as y FROM log WHERE time_stamp>='%s' and time_stamp=datetime('now','-1 day','localtime') and sensor_id='%s' and sensor='%s' and param='%s' ORDER BY time_stamp",format,sensor_id,sensor_type,param) + else + sql = string.format("SELECT strftime('%s',time_stamp) as t,value as y FROM log WHERE time_stamp>='%s' and time_stamp /sys/class/leds/linkit-smart-7688:orange\:wifi/trigger + +if [ ! -e /dev/lcdi2c ]; then + insmod lcdi2c busno=0 address=0x27 cursor=0 blink=0 topo=1 +fi + +echo 1 > /sys/class/alphalcd/lcdi2c/clear +echo \00\00 > /sys/class/alphalcd/lcdi2c/position + +if [ ! -e /dev/rtc0 ]; then + + modprobe rtc-ds1307 + echo ds1307 0x$DS3221 > /sys/class/i2c-dev/i2c-$I2C_BUS/device/new_device + sleep 1 + if [ -e /dev/rtc0 ]; then + ln -sf /dev/rtc0 /dev/rtc + logger -t hardware RTC initialized + fi + +fi + +if [ -e /dev/rtc0 ]; then + /sbin/hwclock -u --rtc=/dev/rtc0 --hctosys + echo "Hardware RTC OK" > /dev/lcdi2c +else + echo "Hardware RTC FAIL" > /dev/lcdi2c +fi diff --git a/weathermon.lua b/bin/weathermon similarity index 70% rename from weathermon.lua rename to bin/weathermon index 3208e30..03fad42 100755 --- a/weathermon.lua +++ b/bin/weathermon @@ -1,47 +1,16 @@ #!/usr/bin/lua -json = require("json") -socket = require("socket") - -function startswith(String,Start) - if String then - return string.sub(String,1,string.len(Start))==Start - else - return False - end -end +local json = require("json") +local socket = require("socket") -function url_encode(str) - if (str) then - str = string.gsub (str, "\n", "\r\n") - str = string.gsub (str, "([^%w %-%_%.%~])", - function (c) return string.format ("%%%02X", string.byte(c)) end) - str = string.gsub (str, " ", "+") - end - return str -end +local http = require("socket.http") -function capture(cmd, raw) - local f = assert(io.popen(cmd, 'r')) - local s = assert(f:read('*a')) - f:close() - if raw then return s end - s = string.gsub(s, '^%s+', '') - s = string.gsub(s, '%s+$', '') - s = string.gsub(s, '[\n\r]+', ' ') - return s -end - -function mqtt_encode(str) - if (str) then - str = string.gsub (str, "\n", "") - str = string.gsub (str, "/", "-") - end - return str -end +require "wm_util" function getConfig(configname) + local command,f + local uci=require("uci") local cur=uci.cursor() local config @@ -53,10 +22,15 @@ function getConfig(configname) web_url = cur.get(config,"web","url") web_user = cur.get(config,"web","user") + web_timeout = cur.get(config,"web","timeout") web_pass = cur.get(config,"web","password") web_devid = cur.get(config,"web","devid") web_iface = cur.get(config,"web","iface") + + if not web_timeout then + web_timeout = 10 + end if web_iface then @@ -74,7 +48,7 @@ function getConfig(configname) io.input("/sys/class/net/eth0/address") end - mac = io.read("*line") + local mac = io.read("*line") mac = mac:gsub(":","") mac = mac:upper() @@ -85,6 +59,9 @@ function getConfig(configname) logging = cur.get(config,"logging","enabled") touch_file = cur.get(config,"logging","touch_file") + backlogdb = cur.get(config,"process","backlogdb") + logdb = cur.get(config,"process","logdb") + serial_port = cur.get(config,"serial","port") serial_baud = cur.get(config,"serial","baud") @@ -124,37 +101,7 @@ function getConfig(configname) mqtt_alarm_topic = 'alarm/{dev}/{type}/{id}' end -end - -function touch() - if touch_file then - local file = io.open(touch_file, 'w') - file:close() - end -end - -function sleep(sec) - socket.select(nil, nil, sec) -end - -function splitStr(str,char) - - local res = {} - local idx = 1 - - while str:len()>0 do - pos = str:find(char); - if pos == nil then - res[idx]=str - str="" - else - res[idx]=str:sub(1,pos-1) - idx=idx+1 - str=str:sub(pos+1) - end - end - - return res + dump_file = cur.get(config,"process","dump_file") end @@ -171,23 +118,51 @@ end function submitValue(type,id,param,val) - url = web_url.."?stype="..url_encode(type).."&sid="..url_encode(id).."¶m="..url_encode(param).."&value="..url_encode(val) + if web_url then - command = "curl" + local url = web_url.."?stype="..url_encode(type).."&sid="..url_encode(id).."¶m="..url_encode(param).."&value="..url_encode(val) - if web_iface then - command = command.." --interface "..ip_addr - end + if web_user then + url = url:gsub("//","//"..web_user..":"..web_pass.."@",1) + end + + local result,code = http.request ({ + url=url, create=function() + local req_sock = socket.tcp() + req_sock:settimeout(web_timeout) + return req_sock + end}) - if web_user then - command = command.." -u "..web_user..":"..web_pass + if code ~= 200 then + print("writing record to backlog...") + backlog_con:execute(string.format("INSERT INTO queue(time_stamp,sensor_id,sensor,param,value) VALUES (datetime('now','localtime'),'%s','%s','%s',%f)",id,type,param,val)) + end + + end + + if logdb then + log_con:execute(string.format("INSERT INTO log(time_stamp,sensor_id,sensor,param,value) VALUES (datetime('now','localtime'),'%s','%s','%s',%f)",id,type,param,val)) end - command = command.." \""..url.."\" 2>&1" + if touch_file then + touch(touch_file) + end + +end - result = capture(command) +function storeRecord(id,sensor,param,value) - touch() + if not records[id] then + records[id] = {} + end + + records[id]["timestamp"] = os.date("%Y-%m-%dT%H:%M:%S") + + if not records[id][sensor] then + records[id][sensor] = {} + end + + records[id][sensor][param] = value end @@ -219,7 +194,9 @@ function processJson(str) if next(sensor)==nil then sensor["command"]="alarm" end + local record = {} for k,v in pairs(sensor) do + storeRecord(sensor_id,sensor_type,k,v) printLog("Type = "..sensor_type..", ID = "..sensor_id..", Param = "..k..", Value = \""..v.."\"") submitValue(sensor_type,sensor_id,k,v) if mqtt_client then @@ -237,7 +214,9 @@ function processJson(str) return '{'..name..'}' end end) + mqtt_client:connect(mqtt_host,mqtt_port) mqtt_client:publish(mqtt_path,v) + mqtt_client:disconnect() end end else @@ -248,7 +227,7 @@ end function processLine(str) - msg=splitStr(line,':') + msg=split(line,':') msg_type=msg[1] or '' msg_body=msg[2] or '' if msg_type=="STATUS" then @@ -257,13 +236,13 @@ function processLine(str) printLog("Error: "..msg_body) elseif msg_type=="SENSOR" then printLog("SENSOR: "..msg_body) - sens = splitStr(msg_body,",") + sens = split(msg_body,",") sensor = {} idx = 1 sensor_type = nil sensor_id = web_devid for i,rec in ipairs(sens) do - recrd=splitStr(rec,'=') + recrd=split(rec,'=') key=recrd[1] or '' value=recrd[2] or '' if value then @@ -277,7 +256,9 @@ function processLine(str) end end if not (sensor_type==nil or sensor_id==nil or sensor_type=='' or sensor_id=='') then + local record = {} for k,v in pairs(sensor) do + storeRecord(sensor_id,sensor_type,k,v) printLog("Type = "..sensor_type..", ID = "..sensor_id..", Param = "..k..", Value = "..v) submitValue(sensor_type,sensor_id,k,v) if mqtt_client then @@ -295,7 +276,9 @@ function processLine(str) return '{'..name..'}' end end) + mqtt_client:connect(mqtt_host,mqtt_port) mqtt_client:publish(mqtt_path,v) + mqtt_client:disconnect() end end else @@ -303,14 +286,14 @@ function processLine(str) end elseif msg_type=="ALARM" then printLog("ALARM: "..msg_body) - sens = splitStr(msg_body,",") + sens = split(msg_body,",") sensor = {} idx = 1 sensor_type = nil sensor_id = web_devid mqtt_param = {} for i,rec in ipairs(sens) do - recrd=splitStr(rec,'=') + recrd=split(rec,'=') key=recrd[1] or '' value=recrd[2] or '' if value then @@ -335,7 +318,9 @@ function processLine(str) return '{'..name..'}' end end) + mqtt_client:connect(mqtt_host,mqtt_port) mqtt_client:publish(mqtt_path,msg_body) + mqtt_client:disconnect() end if alarm_exec then command=alarm_exec.. @@ -353,13 +338,38 @@ end getConfig(arg[1]) +if backlogdb or logdb then + local dbdriver = require "luasql.sqlite3" + env = assert(dbdriver.sqlite3()) +end + +if backlogdb then + if not file_exists(backlogdb) then + touch(backlogdb) + backlog_con = assert(env:connect(backlogdb)) + backlog_con:execute("CREATE TABLE queue(time_stamp datetime,sensor_id varchar(16),sensor varchar(16),param varchar(16),value float)") + else + backlog_con = assert(env:connect(backlogdb)) + end +end + +if logdb then + if not file_exists(logdb) then + touch(logdb) + log_con = assert(env:connect(logdb)) + log_con:execute("CREATE TABLE log(time_stamp datetime,sensor_id varchar(16),sensor varchar(16),param varchar(16),value float)") + log_con:execute("CREATE INDEX log_idx ON log(sensor_id,sensor,param,time_stamp)") + else + log_con = assert(env:connect(logdb)) + end +end + if mqtt_host then MQTT = require "mosquitto" mqtt_client = MQTT.new(mqtt_id) if mqtt_user then mqtt_client:login_set(mqtt_user, mqtt_passwd) end - mqtt_client:connect(mqtt_host,mqtt_port) end if serial_port then @@ -374,6 +384,9 @@ else printLog("No input selected") return end + +records = {} + while 1 do line=serialin:read("*l") if line == nil then @@ -383,6 +396,13 @@ while 1 do if startswith(line,'{') then processJson(line) else - processLine(line) + processLine(line) + end + + if dump_file then + local f = io.open(dump_file,"w") + io.output(f) + io.write(json.encode(records)) + io.close(f) end end diff --git a/weathermon-iio b/bin/weathermon-iio similarity index 81% rename from weathermon-iio rename to bin/weathermon-iio index 93b9cb5..7fe5f68 100755 --- a/weathermon-iio +++ b/bin/weathermon-iio @@ -7,28 +7,7 @@ lfs = require "lfs" json = require "json" socket = require "socket" -function split(str, pat) - local t = {} -- NOTE: use {n = 0} in Lua-5.0 - local fpat = "(.-)" .. pat - local last_end = 1 - local s, e, cap = str:find(fpat, 1) - while s do - if s ~= 1 or cap ~= "" then - table.insert(t,cap) - end - last_end = e+1 - s, e, cap = str:find(fpat, last_end) - end - if last_end <= #str then - cap = str:sub(last_end) - table.insert(t, cap) - end - return t -end - -function trim(s) - return (s:gsub("^%s*(.-)%s*$", "%1")) -end +require "wm_util" function get_device_list(config_name) @@ -58,17 +37,6 @@ function get_device(config_name,device_name) end -function get_file_content(name) - local f = io.open(name,"r") - if f ~= nil then - local content = trim(f:read("*all")) - io.close(f) - return content - else - return false - end -end - function find_device(name,subsystem) local search_base @@ -205,12 +173,13 @@ if not config_name then config_name = "weathermon" end +web_id = uci.get(config_name,"web","devid") + parameters = init(config_name) local delay = uci.get(config_name,"process","delay") local working_dir = uci.get(config_name,"process","working_dir") -local dump_file = uci.get(config_name,"process","dump_file") if working_dir then lfs.mkdir(working_dir) end @@ -221,17 +190,16 @@ end while true do values = get_parameters(parameters) + records = {} + records[web_id] = {} for key,record in pairs(values) do - dump = record - dump["device"] = key - print(json.encode(dump)) - end - if dump_file then - local f = io.open(dump_file,"w") - io.output(f) - io.write(json.encode(values)) - io.close(f) + records[web_id][key] = record + records[web_id]["timestamp"] = os.date("%Y-%m-%dT%H:%M:%S") end + for key,value in pairs(values) do + value["device"] = key + print(json.encode(value)) + end socket.sleep(delay) end \ No newline at end of file diff --git a/config/weathermon.uci b/config/weathermon.uci new file mode 100644 index 0000000..3f97040 --- /dev/null +++ b/config/weathermon.uci @@ -0,0 +1,158 @@ +config internal 'web' + option url http://host-to-send/meteo/send.php + option user meteo + option password some-strong-pwd + option iface eth0 + option timeout 5 + option devid "9C65F920F34E" # mac-addr usually + +config internal 'input' + option exec "/usr/bin/stdbuf -o0 /usr/bin/weathermon-iio" + +# config internal 'serial' +# option port /dev/ttyATH0 +# option timeout 100 +# option baud 57600 + +config internal 'logging' + option enabled stdout +# option touch_file /var/run/weathermon/weathermon.last + +config internal 'mqtt' + option host mqtt-host-to-snd + option user mqtt-user + option password some-strong-pwd + +config internal 'alarm' +# option exec /usr/local/bin/alarm_received + +config internal 'hardware' + option i2c_bus 0 + +config internal 'process' + option delay 48 + option working_dir "/var/weather/" + option dump_file "/var/weather/weather.state" + option logdb "/var/weather/weather.db" + option backlogdb "/var/weather/backlog.db" + option engage "/sys/class/leds/some-led" + option engage_mode "default-on" + option disengage_mode "none" + option mute_file "/var/weather/mute" # for mute button + option mute_time 900 + option backup_dir "/srv/backup" + option archive_dir "/srv/history" + +config device "bme280" + option module "bmp280_i2c" + option address "0x76" + option type "i2c:iio" + option name "bme280" + list set_param "in_humidityrelative_oversampling_ratio:4" + list set_param "in_temp_oversampling_ratio:8" + list set_param "in_pressure_oversampling_ratio:8" + list parameter "in_temp_input:TEMPERATURE:0.001:-4" + list parameter "in_pressure_input:PRESSURE:10" + list parameter "in_humidityrelative_input:HUMIDITY:0.001" + +config device "ads1115" + option module "ads1015" + option address "0x48" + option type "i2c:hwmon" + option name "ads1115" + list parameter "in4_input:CO" + list parameter "in5_input:CH4" + list parameter "in6_input:AIR" + +config alarm "green" + option name "OK" + list engage "/sys/class/leds/led:green/trigger" + option engage_mode "heartbeat" + option disengage_mode "none" + +config alarm "yellow" + option name "Warning" + list engage "/sys/class/leds/led:yellow/trigger" + option engage_mode "heartbeat" + option disengage_mode "none" + +config alarm "red" + option name "High" + list engage "/sys/class/leds/led:red/trigger" + option engage_mode "heartbeat" + option disengage_mode "none" + +config alarm "beep" + option name "ALARM!" + list engage "/sys/class/leds/led:red/trigger" + list engage "/sys/class/leds/led:beep/trigger" + option engage_mode "heartbeat" + option disengage_mode "none" + +config internal "display" + option timestr " %d.%m.%Y %H:%M " + option formatstr " {BME280.TEMPERATURE}~C {BME280.HUMIDITY}% {BME280.PRESSURE}mm CO:{ADS1115.CO} CH:{ADS1115.CH4} AI:{ADS1115.AIR}" + option file "/sys/class/alphalcd/lcdi2c/data" + +config params + option param "BME280.TEMPERATURE" + option name "Temp." + option format "4.1f" + option scale 1 + list limits "green:12:27" + list limits "yellow:8:12" + list limits "yellow:27:35" + list limits "red:-50:8" + list limits "red:35:50" + +config params + option param "BME280.HUMIDITY" + option name "Hum." + option format "2.0f" + option scale 1 + list limits "green:20:60" + list limits "yellow:10:20" + list limits "yellow:60:80" + list limits "red:0:20" + list limits "red:80:100" + +config params + option param "BME280.PRESSURE" + option name "Press." + option format "3.0f" + option scale 0.75 + list limits "green:970:1030" + list limits "yellow:950:970" + list limits "yellow:1030:1050" + list limits "red:800:950" + list limits "red:1050:1100" + +config params + option param "ADS1115.CO" + option name "CO" + option format "3.0f" + option scale 0.1 + list limits "green:0:1400" + list limits "yellow:1400:1800" + list limits "red:1800:2000" + list limits "beep:2000:10000" + +config params + option param "ADS1115.CH4" + option name "Metane" + option format "3.0f" + option scale 0.1 + list limits "green:0:800" + list limits "yellow:800:1000" + list limits "red:1000:1200" + list limits "beep:1200:10000" + +config params + option param "ADS1115.AIR" + option name "Air" + option format "3.0f" + option scale 0.1 + list limits "green:0:1100" + list limits "yellow:1100:1500" + list limits "red:1500:2000" + list limits "beep:2000:10000" diff --git a/init.d/weather-display b/init.d/weather-display new file mode 100755 index 0000000..ed9c44f --- /dev/null +++ b/init.d/weather-display @@ -0,0 +1,28 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2007-2014 OpenWrt.org + +START=99 + +USE_PROCD=1 + +PROG=/usr/bin/weather-display +NICEPRIO=3 + +start_service() { + + sleep 2 + echo 1 > /sys/class/alphalcd/lcdi2c/clear + printf '\x00\x00' > /sys/class/alphalcd/lcdi2c/position + + procd_open_instance + procd_set_param command "$PROG" + procd_set_param nice "$NICEPRIO" + procd_set_param respawn ${respawn_threshold:-600} ${respawn_timeout:-5} ${respawn_retry:-5} + procd_close_instance +} + +reload_service() +{ + stop + start +} diff --git a/init.d/weather-init b/init.d/weather-init new file mode 100755 index 0000000..fd61a78 --- /dev/null +++ b/init.d/weather-init @@ -0,0 +1,10 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2007-2014 OpenWrt.org + +START=45 + +boot() { + + /usr/bin/weather-init + +} diff --git a/init.d/weathermon b/init.d/weathermon new file mode 100755 index 0000000..cb1822a --- /dev/null +++ b/init.d/weathermon @@ -0,0 +1,47 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2007-2014 OpenWrt.org + +START=93 +STOP=45 + +USE_PROCD=1 + +PROG=/usr/bin/weathermon +NICEPRIO=-1 + +BACKUP_DIR=`uci get weathermon.process.backup_dir` +BACKLOGDB=`uci get weathermon.process.backlogdb` +LOGDB=`uci get weathermon.process.logdb` +WORK_DIR=`uci get weathermon.process.working_dir` + +start_service() { + + mkdir -p "$WORK_DIR" + + if [ ! -z "$BACKLOGDB" ] && [ ! -f "$BACKLOGDB" ]; then + BACKLOG_BASE=$(basename "$BACKLOGDB") + cp "$BACKUP_DIR/$BACKLOG_BASE" "$BACKLOGDB" + fi + + if [ ! -z "$LOGDB" ] && [ ! -f "$LOGDB" ]; then + LOG_BASE=$(basename "$LOGDB") + cp "$BACKUP_DIR/$LOG_BASE" "$LOGDB" + fi + + procd_open_instance + procd_set_param command "$PROG" + procd_set_param nice "$NICEPRIO" + procd_set_param respawn ${respawn_threshold:-600} ${respawn_timeout:-5} ${respawn_retry:-5} + procd_close_instance +} + +stop_service() { + killall weathermon + killall weathermon-iio + if [ ! -z "$BACKLOGDB" ] && [ -f "$BACKLOGDB" ]; then + cp "$BACKLOGDB" "$BACKUP_DIR"/ + fi + if [ ! -z "$LOGDB" ] && [ -f "$BACKLOGDB" ]; then + cp "$LOGDB" "$BACKUP_DIR"/ + fi +} diff --git a/lib/wm_util.lua b/lib/wm_util.lua new file mode 100644 index 0000000..bb5d8ee --- /dev/null +++ b/lib/wm_util.lua @@ -0,0 +1,106 @@ +#!/usr/bin/lua + +local socket = require "socket" + +function startswith(String,Start) + if String then + return string.sub(String,1,string.len(Start))==Start + else + return False + end +end + +function trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + +function url_decode(str) + if not str then return nil end + str = string.gsub (str, "+", " ") + str = string.gsub (str, "%%(%x%x)", function(h) return + string.char(tonumber(h,16)) end) + str = string.gsub (str, "\r\n", "\n") + return str +end + +function url_encode(str) + if (str) then + str = string.gsub (str, "\n", "\r\n") + str = string.gsub (str, "([^%w %-%_%.%~])", + function (c) return string.format ("%%%02X", string.byte(c)) end) + str = string.gsub (str, " ", "+") + end + return str +end + +function capture(cmd, raw) + local f = assert(io.popen(cmd, 'r')) + local s = assert(f:read('*a')) + f:close() + if raw then return s end + s = string.gsub(s, '^%s+', '') + s = string.gsub(s, '%s+$', '') + s = string.gsub(s, '[\n\r]+', ' ') + return s +end + +function mqtt_encode(str) + if (str) then + str = string.gsub (str, "\n", "") + str = string.gsub (str, "/", "-") + end + return str +end + +function touch(name) + local file = io.open(name, 'a') + file:close() +end + +function write_file(name,data) + pcall(function () + local f = io.open(name,"w") + io.output(f) + io.write(data) + io.close(f) + end) +end + +function file_exists(name) + local f=io.open(name,"r") + if f~=nil then io.close(f) return true else return false end +end + +function get_file_content(name) + local f = io.open(name,"r") + if f ~= nil then + local content = trim(f:read("*all")) + io.close(f) + return content + else + return false + end +end + +function sleep(sec) + socket.sleep(sec) +end + +function split(s, delimiter) + local result = {}; + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + result[#result+1] = match + end + return result; +end + +function list_dir(name) + local lfs = require "lfs" + local result = {} + for name in lfs.dir(name) do + if not startswith(name,".") then + result[#result+1] = name + end + end + return result +end diff --git a/openwrt-web/cgi-bin/meteo.lua b/openwrt-web/cgi-bin/meteo.lua new file mode 100755 index 0000000..8e38a49 --- /dev/null +++ b/openwrt-web/cgi-bin/meteo.lua @@ -0,0 +1,110 @@ +#!/usr/bin/lua + +require "uci" + +cursor = uci.cursor() + +archive_dir = uci.get("weathermon","process","archive_dir") + +require "wm_util" + +command = url_decode(os.getenv('QUERY_STRING')) + +print("Content-Type: text/plain\r\n") + +if command == "state" or command == "" or not command then + print(get_file_content(uci.get("weathermon","process","dump_file"))) + +elseif command == "props" then + print(get_file_content("/www/meteo/properties.json")) + +elseif command == "years" then + json = require "json" + local result = list_dir(archive_dir) + table.sort(result, function(a, b) return a > b; end) + print(json.encode(result)) + +elseif startswith(command,"months/") then + json = require "json" + local year = string.match(command,"months/(%d+)") + local result = list_dir(archive_dir.."/"..year) + table.sort(result, function(a, b) return a > b; end) + print(json.encode(result)) + +elseif startswith(command,"days/") then + json = require "json" + local year,month = string.match(command,"days/(%d+)/(%d+)") + local result = list_dir(archive_dir.."/"..year.."/"..month) + table.sort(result, function(a, b) return a > b; end) + print(json.encode(result)) + +elseif startswith(command,"sensors/") then + json = require "json" + local year,month,day = string.match(command,"sensors/(%d+)/(%d+)/(%d+)") + local files = list_dir(archive_dir.."/"..year.."/"..month.."/"..day) + result = {} + for key,value in pairs(files) do + local id, sensor, param = string.match(value,"(%w+)%.(%w+)%.(%w+).json") + if id then + result[#result+1] = id.."."..sensor.."."..param + end + end + table.sort(result) + print(json.encode(result)) + +elseif startswith(command,"list/") then + + sensor = string.sub(command,string.len("list/")+1) + lfs = require "lfs" + json = require "json" + dir = uci.get("weathermon","process","working_dir").."/www" + + params = {} + + for name in lfs.dir(dir) do + param=name:match(devid.."\."..sensor.."\.(%w+)\.json") + if param then + params[#params+1] = param + end + end + + print(json.encode(params)) + +elseif startswith(command,"get/") then + + sensor_path=string.sub(command,string.len("get/")+1) + devid,sensor,param = sensor_path:match("(%w+)/(%w+)/(.+)") + + if param=="*" then + + lfs = require "lfs" + dir = uci.get("weathermon","process","working_dir").."/www" + + result = "" + + for name in lfs.dir(dir) do + param=name:match(devid.."\."..sensor.."\.(%w+)\.json") + if param then + content = "\""..param.."\":"..get_file_content(dir.."/"..name) + if result=="" then + result = content + else + result = result..","..content + end + end + end + + print("{"..result.."}") + + else + print(get_file_content(uci.get("weathermon","process","working_dir").."/www/"..devid.."."..sensor.."."..param..".json")) + end + +elseif startswith(command,"get-archive/") then + + local sensor_path=string.sub(command,string.len("get-archive/")+1) + local year,month,day,devid,sensor,param = sensor_path:match("(%d+)/(%d+)/(%d+)/(%w+)/(%w+)/(%w+)") + + print(get_file_content(archive_dir.."/"..year.."/"..month.."/"..day.."/"..devid.."."..sensor.."."..param..".json")) + +end diff --git a/openwrt-web/meteo/archive.html b/openwrt-web/meteo/archive.html new file mode 100644 index 0000000..129bb18 --- /dev/null +++ b/openwrt-web/meteo/archive.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+
Год
+
+
Месяц
+
+
День
+
+
Параметр
+
+
+
+ + +
+
+
+
+ +
Powered by OpenWRT
+ + + diff --git a/openwrt-web/meteo/archive.js b/openwrt-web/meteo/archive.js new file mode 100644 index 0000000..20aa7aa --- /dev/null +++ b/openwrt-web/meteo/archive.js @@ -0,0 +1,322 @@ +urlbase="/meteo/cgi?"; + +archive_base="/meteo/archive"; +current_base = "/meteo/graph"; + +var url = window.location.pathname; + +if (url.startsWith(archive_base)) { + url = url.substr(archive_base.length); +} + +var params = url.split('/'); + +var year = params[1]; +var month = params[2]; +var day = params[3]; +var devid = params[4]; +var sensor = params[5]; +var param = params[6]; + +var properties; +var years; +var months; +var days; +var sensors; + +function composeUrl() { + url = window.location.protocol+"//"+window.location.host+archive_base+"/"; + if (year) { + url = url + year; + if (month) { + url = url + "/" + month; + if (day) { + url = url + "/" + day; + if (devid) { + url = url + "/" + devid; + if (sensor) { + url = url + "/" + sensor; + if (param) { + url = url + "/" + param; + } + } + } + } + } + } + history.pushState({}, null, url); +} + +function selectChange() { + new_year = document.getElementById("year").value; + new_month = document.getElementById("month").value; + new_day = document.getElementById("day").value; + sensor_id = document.getElementById("sensor").value.split("."); + new_devid = sensor_id[0]; + new_sensor = sensor_id[1]; + new_param = sensor_id[2]; + if (year != new_year) { + year = new_year; + GetMonths(); + } else if (month != new_month) { + month = new_month; + GetDays(); + } else if (day != new_day) { + day = new_day; + GetSensors(); + } else if ((new_devid != devid) || (new_sensor != sensor) || (new_param != param)) { + devid = new_devid; + sensor = new_sensor; + param = new_param; + RefreshGraph(); + } + composeUrl(); +} + +function processDataset(dataset,devid,sensorname,paramname) { + var scale = properties["scale"][devid+"."+sensorname+"."+paramname] + if (scale) { + var result=[]; + for (idx in dataset) { + newRec = {}; + newRec.t = dataset[idx].t + newRec.y = dataset[idx].y * scale[0]; + result.push(newRec); + } + return result; + } else { + return dataset; + } +} + +function drawGraph(graphData) { + + document.getElementById("current").href = current_base+"/"+devid + "/" + sensor + "/" + param; + + var div = document.getElementById("chartdiv"); + var canvas = document.getElementById("chart"); + + canvas.width = div.style.width; + canvas.height = div.style.height; + + var ctx = canvas.getContext('2d'); + var color = Chart.helpers.color; + + var cfg = { + type: 'bar', + data: { + datasets: [ + { + label: properties["names"][devid+"."+sensor+"."+param], + backgroundColor: color(properties["colors"][devid+"."+sensor+"."+param]).alpha(0.5).rgbString(), + borderColor: properties["colors"][devid+"."+sensor+"."+param], + data: processDataset(graphData,devid,sensor,param), + type: 'line', + pointRadius: 0, + fill: true, + borderWidth: 2 + } + ] + }, + options: { + animation: { + duration: 0, + }, + hover: { + animationDuration: 0, + }, + responsiveAnimationDuration: 0, + legend: { + labels: { + fontColor: properties["fonts"]["legend"]["color"], + fontSize: properties["fonts"]["legend"]["size"], + fontStyle: properties["fonts"]["legend"]["style"], + } + }, + scales: { + xAxes: [{ + type: 'time', + distribution: 'series', + scaleLabel: { + fontColor: properties["fonts"]["axes"]["color"], + fontSize: properties["fonts"]["axes"]["size"], + fontStyle: properties["fonts"]["axes"]["style"], + } + }], + yAxes: [{ + scaleLabel: { + display: true, + labelString: properties["names"][sensor+"."+param], + fontColor: properties["fonts"]["axes"]["color"], + fontSize: properties["fonts"]["axes"]["size"], + fontStyle: properties["fonts"]["axes"]["style"], + } + }] + } + } + + } + var chart = new Chart(ctx, cfg); +} + +function RefreshGraph() { + + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(RefreshGraph,30000); + return; + } + var graphData = JSON.parse(this.responseText); + drawGraph(graphData); + }; + + req.open("GET", urlbase+"get-archive/"+year+"/"+month+"/"+day+"/"+devid+"/"+sensor+"/"+param, true); + req.send(); + +} + +function GetProperties() { + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(GetProperties,30000); + return; + } + properties = JSON.parse(this.responseText); + GetYears(); + }; + + req.open("GET", urlbase+"props", true); + req.send(); + +} + +function fillSelector(id,data,value,nameFunc) { + + var element = document.getElementById(id); + var html = ""; + var line; + + for (i in data) { + if (nameFunc) { + line = "" + } else { + line = "" + } + html = html + line; + } + + element.innerHTML = html; + if (value) { + element.value = value; + } + return element.value; + +} + +function GetYears() { + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(GetYears,30000); + return; + } + years = JSON.parse(this.responseText); + year = fillSelector("year", years, year); + composeUrl(); + if (year) { + GetMonths(); + } + }; + + req.open("GET", urlbase+"years", true); + req.send(); + +} + +function GetMonths() { + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(GetMonths,30000); + return; + } + months = JSON.parse(this.responseText); + month = fillSelector("month", months, month); + composeUrl(); + if (month) { + GetDays(); + } + }; + + req.open("GET", urlbase+"months/"+year, true); + req.send(); + +} + +function GetDays() { + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(GetDays,30000); + return; + } + days = JSON.parse(this.responseText); + day = fillSelector("day", days, day); + composeUrl(); + if (day) { + GetSensors(); + } + }; + + req.open("GET", urlbase+"days/"+year+"/"+month, true); + req.send(); + +} + +function SensorName(id) { + return properties["names"][id]; +} + +function GetSensors() { + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(GetSensors,30000); + return; + } + sensors = JSON.parse(this.responseText); + if (devid && sensor && param) { + sensor_id = devid+"."+sensor+"."+param; + } else { + sensor_id = null; + } + sensor_id = fillSelector("sensor", sensors, sensor_id, SensorName).split("."); + devid = sensor_id[0]; + sensor = sensor_id[1]; + param = sensor_id[2]; + composeUrl(); + if (sensor_id) { + RefreshGraph(); + } + }; + + req.open("GET", urlbase+"sensors/"+year+"/"+month+"/"+day, true); + req.send(); + +} + +setTimeout(GetProperties,100); diff --git a/openwrt-web/meteo/graph.html b/openwrt-web/meteo/graph.html new file mode 100644 index 0000000..da1f863 --- /dev/null +++ b/openwrt-web/meteo/graph.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+ + +
+
+
+
+ +
Powered by OpenWRT
+ + + diff --git a/openwrt-web/meteo/graph.js b/openwrt-web/meteo/graph.js new file mode 100644 index 0000000..ed74328 --- /dev/null +++ b/openwrt-web/meteo/graph.js @@ -0,0 +1,206 @@ +urlbase="/meteo/cgi?" +archive_base="/meteo/archive"; + +var params = window.location.pathname.split('/').slice(-4); +var sensor_id = params[1]; +var sensor = params[2]; +var param = params[3]; + +var sensor_path = sensor_id + "." + sensor + "." + param; + +var properties; + +function processDataset(dataset,sensorid,sensorname,paramname) { + var scale = properties["scale"][sensorid+"."+sensorname+"."+paramname] + if (scale) { + var result=[]; + for (idx in dataset) { + newRec = {}; + newRec.t = dataset[idx].t + newRec.y = dataset[idx].y * scale[0]; + result.push(newRec); + } + return result; + } else { + return dataset; + } +} + +function drawGraph(graphData) { + + document.getElementById("archive").href = archive_base+"////"+sensor_id + "/" + sensor + "/" + param; + + var div = document.getElementById("chartdiv"); + var canvas = document.getElementById("chart"); + + canvas.width = div.style.width; + canvas.height = div.style.height; + + var ctx = canvas.getContext('2d'); + var color = Chart.helpers.color; + + if (param=="*") { + + var cfg = { + type: 'bar', + data: { + datasets: [ + ] + }, + options: { + animation: { + duration: 0, + }, + hover: { + animationDuration: 0, + }, + responsiveAnimationDuration: 0, + legend: { + labels: { + fontColor: properties["fonts"]["legend"]["color"], + fontSize: properties["fonts"]["legend"]["size"], + fontStyle: properties["fonts"]["legend"]["style"], + } + }, + scales: { + xAxes: [{ + type: 'time', + distribution: 'series', + scaleLabel: { + fontColor: properties["fonts"]["axes"]["color"], + fontSize: properties["fonts"]["axes"]["size"], + fontStyle: properties["fonts"]["axes"]["style"], + } + }], + yAxes: [{ + scaleLabel: { + display: true, + fontColor: properties["fonts"]["axes"]["color"], + fontSize: properties["fonts"]["axes"]["size"], + fontStyle: properties["fonts"]["axes"]["style"], + } + }] + } + } + } + + for (paramname in graphData) { + + cfg.data.datasets.push( + { + label: properties["names"][sensor_id+"."+sensor+"."+paramname], + data: processDataset(graphData[paramname],sensor_id,sensor,paramname), + borderColor: properties["colors"][sensor_id+"."+sensor+"."+paramname], + type: 'line', + fill: false, + pointRadius: 0, + borderWidth: 2 + } + ) + + } + + } else { + + var y_label = properties["names"][sensor_path]; + if (properties["units"][sensor_path]) { + y_label = y_label + ", " + properties["units"][sensor_path]; + } + + var cfg = { + type: 'bar', + data: { + datasets: [ + { + label: properties["names"][sensor_path], + backgroundColor: color(properties["colors"][sensor_path]).alpha(0.5).rgbString(), + borderColor: properties["colors"][sensor_path], + data: processDataset(graphData,sensor_id,sensor,param), + type: 'line', + pointRadius: 0, + fill: true, + borderWidth: 2 + } + ] + }, + options: { + animation: { + duration: 0, + }, + hover: { + animationDuration: 0, + }, + responsiveAnimationDuration: 0, + legend: { + labels: { + fontColor: properties["fonts"]["legend"]["color"], + fontSize: properties["fonts"]["legend"]["size"], + fontStyle: properties["fonts"]["legend"]["style"], + } + }, + scales: { + xAxes: [{ + type: 'time', + distribution: 'series', + scaleLabel: { + fontColor: properties["fonts"]["axes"]["color"], + fontSize: properties["fonts"]["axes"]["size"], + fontStyle: properties["fonts"]["axes"]["style"], + } + }], + yAxes: [{ + scaleLabel: { + display: true, + labelString: y_label, + fontColor: properties["fonts"]["axes"]["color"], + fontSize: properties["fonts"]["axes"]["size"], + fontStyle: properties["fonts"]["axes"]["style"], + } + }] + } + } + } + + } + var chart = new Chart(ctx, cfg); +} + +function RefreshGraph() { + + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(RefreshGraph,30000); + return; + } + var graphData = JSON.parse(this.responseText); + drawGraph(graphData); + setTimeout(RefreshGraph,30000) + }; + + req.open("GET", urlbase+"get/"+sensor_id+"/"+sensor+"/"+param, true); + req.send(); + +} + +function GetProperties() { + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(GetProperties,30000); + return; + } + properties = JSON.parse(this.responseText); + setTimeout(RefreshGraph,100) + }; + + req.open("GET", urlbase+"props", true); + req.send(); + +} + +setTimeout(GetProperties,100) diff --git a/openwrt-web/meteo/index.html b/openwrt-web/meteo/index.html new file mode 100644 index 0000000..fbd7ce0 --- /dev/null +++ b/openwrt-web/meteo/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+ +
+ +
+ +
+ +
Powered by OpenWRT
+ + + diff --git a/openwrt-web/meteo/meteo.css b/openwrt-web/meteo/meteo.css new file mode 100644 index 0000000..ff3fe2f --- /dev/null +++ b/openwrt-web/meteo/meteo.css @@ -0,0 +1,200 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-size: 0.8rem; + background-color: #EEE; +} + +html, body { + margin: 0px; + padding: 0px; + height: 100%; + font-family: Microsoft Yahei, WenQuanYi Micro Hei, sans-serif, "Helvetica Neue", Helvetica, Hiragino Sans GB; +} + +header { + background: darkgreen; + color: white; +} + +header, .main { + width: 100%; + position: absolute; +} + +header { + height: 5rem; + box-shadow: 0 2px 5px rgba(0, 0, 0, .26); + transition: box-shadow .2s; + float: left; + position: fixed; + top: 0px; + z-index: 2000; +} + +header > .fill > .container { + padding-top: 0.25rem; + padding-right: 1rem; + padding-bottom: 0.25rem; + display: flex; + align-items: center; + height: 5rem; +} + +header > .fill > .container > img{ + max-height: 4.5rem; + margin: 1rem; + padding: 0.5rem; +} + +header > .fill > .container > .brand { + font-size: 1.4rem; + color: white; + text-decoration: none; + margin-left: 1rem; +} + +.main { + top: 4rem; + bottom: 0rem; + position: relative; + height: calc(100% - 5rem); +} + +.main > #maincontent { + background-color: #EEE; + height: calc(100% - 5rem); +} + +#maincontent > .container { + margin: 1rem 2rem 1rem 2rem; + padding-top: 1px; + height: 100%; +} + +.section { + margin: 1rem 1rem 1rem 1rem; + padding: 2rem; + border: 0; + font-weight: normal; + font-style: normal; + line-height: 1; + font-family: inherit; + float: left; + min-width: inherit; + border-radius: 0; + color: #404040; + background-color: #FFF; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); + -webkit-overflow-scrolling: touch; +} + +.wide-section { + margin: 1rem 1rem 1rem 1rem; + padding: 2rem; + border: 0; + font-weight: normal; + font-style: normal; + line-height: 1; + font-family: inherit; + float: left; + min-width: calc(100% - 2rem); + border-radius: 0; + color: #404040; + background-color: #FFF; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); + -webkit-overflow-scrolling: touch; +} + +.large-section { + margin: 1rem 1rem 1rem 1rem; + padding: 2rem; + border: 0; + font-weight: normal; + font-style: normal; + line-height: 1; + font-family: inherit; + min-width: inherit; + border-radius: 0; + height: 100%; + background-color: #FFF; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); + -webkit-overflow-scrolling: touch; +} + +.bottom-section { + margin: 1rem 1rem 1rem 1rem; + padding: 2rem; + border: 0; + font-weight: normal; + font-style: normal; + line-height: 1; + font-family: inherit; + float: left; + min-width: calc(100% - 2rem); + border-radius: 0; + height: 80%; + background-color: #FFF; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); + -webkit-overflow-scrolling: touch; +} + +footer { + text-align: right; + padding: 1rem; + color: #aaa; + font-size: 0.8rem; + text-shadow: 0px 0px 2px #BBB; +} + +footer > a { + color: #aaa; + text-decoration: none; +} + +.reference { + padding: 1rem 1rem; + text-decoration: bold; + float: left; + font-size: 6rem; +} + +.reference-unit { + padding: 0.5rem 1rem; + text-decoration: bold; + float: left; + font-size: 4rem; +} + +.section:hover { + color: white; + background: darkgreen; +} + +.reference-header { + padding: 0.5rem 0.2rem; + text-decoration: none; + font-size: 2rem; + text-align: left; +} + +.selector-header { + padding: 1rem 1rem; + text-decoration: bold; + float: left; + font-size: 1.5rem; +} + +.selector { + padding: 1rem 1rem; + text-decoration: bold; + float: left; +} + +option { + font-size: 1.2rem; +} diff --git a/openwrt-web/meteo/meteo.js b/openwrt-web/meteo/meteo.js new file mode 100644 index 0000000..f56bc2d --- /dev/null +++ b/openwrt-web/meteo/meteo.js @@ -0,0 +1,85 @@ +urlbase="/meteo/cgi?" + +currentState="" + +function RefreshPageState() { + + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + + if (this.readyState != 4) return; + if (this.status == 200) { + + var returnedData = JSON.parse(this.responseText); + + var template = document.getElementById("template").innerHTML; + var value_color = document.getElementById("value").style.color; + + var html = ""; + + for (var sensor_id in returnedData) { + var timestamp = returnedData[sensor_id]["timestamp"]; + for (var sensor in returnedData[sensor_id]) { + if (sensor != "timestamp") { + for (var param in returnedData[sensor_id][sensor]) { + sensor_path = sensor_id+"."+sensor+"."+param; + value = returnedData[sensor_id][sensor][param]; + name = properties["names"][sensor_path]; + if (! name) { name = sensor_path; } + units = properties["units"][sensor_path]; + scale = properties["scale"][sensor_path]; + color = properties["colors"][sensor_path]; + if (scale) { + value = (scale[0] * value).toFixed(scale[1]); + } + if (! color) { + color = value_color; + } + var section = template.replace(/\$SENSOR_ID/g,sensor_id); + section = section.replace(/\$SENSOR/g,sensor); + section = section.replace(/\$PARAM/g,param); + section = section.replace(/\$NAME/g,name); + section = section.replace(/\$UNITS/g,units); + section = section.replace(/\$VALUE/g,value); + section = section.replace(/\$COLOR/g,color); + section = section.replace(/\$TIMESTAMP/g,timestamp); + html = html + section; + } + } + } + } + + document.getElementById("meteo").innerHTML = html; + + setTimeout(RefreshPageState,5000) + } else { + setTimeout(RefreshPageState,15000) + } + }; + + req.open("GET", urlbase+"state", true); + req.send(); + +} + +function GetProperties() { + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4) return; + if (this.status != 200) { + setTimeout(GetProperties,30000); + return; + } + properties = JSON.parse(this.responseText); + RefreshPageState(); + }; + + req.open("GET", urlbase+"props", true); + req.send(); + +} + + +setTimeout(GetProperties,100) diff --git a/openwrt-web/meteo/properties.json b/openwrt-web/meteo/properties.json new file mode 100644 index 0000000..54c0e66 --- /dev/null +++ b/openwrt-web/meteo/properties.json @@ -0,0 +1,44 @@ +{ + "names":{ + "9C65F920F34E.ADS1115.CO":"Угарный газ", + "9C65F920F34E.ADS1115.CH4":"Горючие газы", + "9C65F920F34E.ADS1115.AIR":"Загрязнения воздуха", + "9C65F920F34E.BME280.TEMPERATURE":"Температура", + "9C65F920F34E.BME280.HUMIDITY":"Влажность", + "9C65F920F34E.BME280.PRESSURE":"Давление", + "9C65F920F34E.HMC5843.MX":"Магнитное поле (X)", + "9C65F920F34E.HMC5843.MY":"Магнитное поле (Y)", + "9C65F920F34E.HMC5843.MZ":"Магнитное поле (Z)" + }, + "colors":{ + "9C65F920F34E.ADS1115.CO":"black", + "9C65F920F34E.ADS1115.CH4":"darkgray", + "9C65F920F34E.ADS1115.AIR":"teal", + "9C65F920F34E.BME280.TEMPERATURE":"red", + "9C65F920F34E.BME280.HUMIDITY":"blue", + "9C65F920F34E.BME280.PRESSURE":"green", + "9C65F920F34E.HMC5843.MX":"orange", + "9C65F920F34E.HMC5843.MY":"cyan", + "9C65F920F34E.HMC5843.MZ":"magenta" + }, + "units":{ + "9C65F920F34E.ADS1115.CO":"Ед.", + "9C65F920F34E.ADS1115.CH4":"Ед.", + "9C65F920F34E.ADS1115.AIR":"Ед.", + "9C65F920F34E.BME280.TEMPERATURE":"°C", + "9C65F920F34E.BME280.HUMIDITY":"%", + "9C65F920F34E.BME280.PRESSURE":"мм", + "9C65F920F34E.HMC5843.MX":"Ед.", + "9C65F920F34E.HMC5843.MY":"Ед.", + "9C65F920F34E.HMC5843.MZ":"Ед." + }, + "scale": { + "9C65F920F34E.BME280.PRESSURE": [ 0.75, 0 ], + "9C65F920F34E.BME280.TEMPERATURE": [ 1, 1 ], + "9C65F920F34E.BME280.HUMIDITY": [ 1, 0 ] + }, + "fonts": { + "axes": { "color": "black", "size": 16, "style": "normal" }, + "legend": { "color": "black", "size": 16, "style": "normal" } + } +} diff --git a/openwrt-web/meteo/weather.svg b/openwrt-web/meteo/weather.svg new file mode 100644 index 0000000..dc49122 --- /dev/null +++ b/openwrt-web/meteo/weather.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/filter_meteo.py b/server/filter_meteo.py similarity index 100% rename from filter_meteo.py rename to server/filter_meteo.py diff --git a/weathermon-mqtt b/server/weathermon-mqtt similarity index 100% rename from weathermon-mqtt rename to server/weathermon-mqtt diff --git a/weathermon_cron b/server/weathermon_cron similarity index 100% rename from weathermon_cron rename to server/weathermon_cron diff --git a/weathermon.conf b/weathermon.conf deleted file mode 100644 index 9018a24..0000000 --- a/weathermon.conf +++ /dev/null @@ -1,21 +0,0 @@ -[mysql] -host = dbhost -user = meteo -passwd = password -db = meteo -[filter] -window=5 -threshold=10 -[serial] -port = /dev/ttyATH0 -[openweathermap] -user = user -passwd = password -temp = OUTDOOR.44F.TEMPERATURE -pres = BARO.DEFAULT.PRESSURE -humi = OUTDOOR.44F.HUMIDITY -lat = 55.403266 -lon = 37.561651 -station = station.name -[logging] -enabled=on diff --git a/weathermon.uci b/weathermon.uci deleted file mode 100644 index 7a4162f..0000000 --- a/weathermon.uci +++ /dev/null @@ -1,64 +0,0 @@ -config internal 'web' - option url http://url-to-submit-meteo-data - option user meteo - option password some-password - option iface wlan0 - -config internal 'input' - option exec "/usr/bin/stdbuf -o0 /usr/bin/lua /usr/bin/weathermon-iio" -# option port /dev/ttyATH0 -# option timeout 100 -# option baud 57600 - -config internal 'logging' - option enabled off # on/stdout/syslog -# option touch_file /var/run/weathermon/weathermon.last - -config internal 'mqtt' - option host mqtt.host.name - option user meteo-user - option password some-password - -config internal 'alarm' -# option exec /usr/local/bin/alarm_received - -config internal 'hardware' - option i2c_bus 0 - -config internal 'process' - option delay 48 - option working_dir "/var/weather/" - option dump_file "/var/weather/weather.state" - -config device "bme280" - option module "bmp280_i2c" - option address "0x76" - option type "i2c:iio" - option name "bme280" - list set_param "in_humidityrelative_oversampling_ratio:4" - list set_param "in_temp_oversampling_ratio:8" - list set_param "in_pressure_oversampling_ratio:8" - list parameter "in_temp_input:T:0.001:-4" # source, name, scale, correction - list parameter "in_pressure_input:P:10" - list parameter "in_humidityrelative_input:H:0.001" - -config device "hmc5843" - option module "hmc5843_i2c" - option address "0x1e" - option type "i2c:iio" - option name "hmc5843" - list set_param "in_magn_meas_conf:normal" - list set_param "in_magn_sampling_frequency:2" - list set_param "in_magn_scale:0.000007692" - list parameter "in_magn_x_raw:MX" - list parameter "in_magn_y_raw:MY" - list parameter "in_magn_z_raw:MZ" - -config device "ads1115" - option module "ads1015" - option address "0x48" - option type "i2c:hwmon" - option name "ads1115" - list parameter "in4_input:CO" - list parameter "in5_input:CH4" - list parameter "in6_input:AIR" -- 2.34.1