From: Roman Bazalevsky Date: Mon, 12 Nov 2018 17:18:30 +0000 (+0300) Subject: - Добавлен процесс для чтения iio-датчиков X-Git-Url: https://git.rvb.name/weathermon.git/commitdiff_plain/e32107a7fe79ce34f3bdf860410a6d5455efdca7?ds=inline;hp=7edb3771717d15f7c36d8459fa12b3d6f76d7d9a - Добавлен процесс для чтения iio-датчиков - Добавлено сохранение неотосланных данных в очереди - Добавлено локальное хранилище данных и легковесный веб-интерфейс - Добавлен автоматический бэкап (регулярно и при перезапуске) --- 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/bin/weathermon b/bin/weathermon new file mode 100755 index 0000000..03fad42 --- /dev/null +++ b/bin/weathermon @@ -0,0 +1,408 @@ +#!/usr/bin/lua + +local json = require("json") +local socket = require("socket") + +local http = require("socket.http") + +require "wm_util" + +function getConfig(configname) + + local command,f + + local uci=require("uci") + local cur=uci.cursor() + local config + if configname then + config=configname + else + config="weathermon" + end + + 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 + + command = '/sbin/ifconfig '..web_iface..' | grep \'\\\' | sed -n \'1p\' | tr -s \' \' | cut -d \' \' -f3 | cut -d \':\' -f2' + f=io.popen(command,'r') + ip_addr=f:read() + + end + + if not web_devid then + + if web_iface then + io.input("/sys/class/net/"..web_iface.."/address") + else + io.input("/sys/class/net/eth0/address") + end + + local mac = io.read("*line") + mac = mac:gsub(":","") + mac = mac:upper() + + web_devid = mac + + end + + 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") + + input_file = cur.get(config,"input","file") + input_exec = cur.get(config,"input","exec") + alarm_exec = cur.get(config,"alarm","exec") + + if serial_port then + + command = "stty -F "..serial_port.." "..serial_baud + capture(command) + + end + + mqtt_host = cur.get(config,"mqtt","host") + mqtt_port = cur.get(config,"mqtt","port") + mqtt_id = cur.get(config,"mqtt","id") + mqtt_topic = cur.get(config,"mqtt","topic") + mqtt_alarm_topic = cur.get(config,"mqtt","alarm_topic") + + mqtt_user = cur.get(config,"mqtt","user") + mqtt_passwd = cur.get(config,"mqtt","password") + + if mqtt_host and not mqtt_id then + mqtt_id="weather-"..web_devid + end + + if mqtt_host and not mqtt_port then + mqtt_port = 1883 + end + + if mqtt_host and not mqtt_topic then + mqtt_topic = 'weathermon/{dev}/{type}/{id}/{param}' + end + + if mqtt_host and not mqtt_alarm_topic then + mqtt_alarm_topic = 'alarm/{dev}/{type}/{id}' + end + + dump_file = cur.get(config,"process","dump_file") + +end + +function printLog(str) + if logging=="on" then + capture("logger -t weathermon "..str) + print(str) + elseif logging=="syslog" then + capture("logger -t weathermon "..str) + elseif logging=="stdout" then + print(str) + end +end + +function submitValue(type,id,param,val) + + if web_url then + + local url = web_url.."?stype="..url_encode(type).."&sid="..url_encode(id).."¶m="..url_encode(param).."&value="..url_encode(val) + + 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 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 + + if touch_file then + touch(touch_file) + end + +end + +function storeRecord(id,sensor,param,value) + + 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 + +function processJson(str) + + msg=json.decode(str) + + sensor={} + + for key,value in pairs(msg) do + if value then + if key=="model" or key=="device" then + sensor_type=value + elseif key=="id" then + sensor_id=value + elseif key=='time' then + sensor_time=value + else + sensor[key]=value + end + end + end + + if not sensor_id then + sensor_id = web_devid + end + + if not (sensor_type==nil or sensor_id==nil or sensor_type=='' or sensor_id=='') then + 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 + mqtt_path=string.gsub(mqtt_topic,"{(.-)}", + function (name) + if name=="dev" then + return mqtt_encode(web_devid) + elseif name=="type" then + return mqtt_encode(sensor_type) + elseif name=="id" then + return mqtt_encode(sensor_id) + elseif name=="param" then + return k + else + return '{'..name..'}' + end + end) + mqtt_client:connect(mqtt_host,mqtt_port) + mqtt_client:publish(mqtt_path,v) + mqtt_client:disconnect() + end + end + else + printLog("Cannot parse sensor input: "..str) + end + +end + +function processLine(str) + + msg=split(line,':') + msg_type=msg[1] or '' + msg_body=msg[2] or '' + if msg_type=="STATUS" then + printLog("Status: "..msg_body) + elseif msg_type=="ERROR" then + printLog("Error: "..msg_body) + elseif msg_type=="SENSOR" then + printLog("SENSOR: "..msg_body) + sens = split(msg_body,",") + sensor = {} + idx = 1 + sensor_type = nil + sensor_id = web_devid + for i,rec in ipairs(sens) do + recrd=split(rec,'=') + key=recrd[1] or '' + value=recrd[2] or '' + if value then + if key=="TYPE" then + sensor_type=value + elseif key=="ID" then + sensor_id=value + else + sensor[key]=value + end + 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 + mqtt_path=string.gsub(mqtt_topic,"{(.-)}", + function (name) + if name=="dev" then + return web_devid + elseif name=="type" then + return sensor_type + elseif name=="id" then + return sensor_id + elseif name=="param" then + return k + else + return '{'..name..'}' + end + end) + mqtt_client:connect(mqtt_host,mqtt_port) + mqtt_client:publish(mqtt_path,v) + mqtt_client:disconnect() + end + end + else + printLog("Cannot parse sensor input: "..msg_body) + end + elseif msg_type=="ALARM" then + printLog("ALARM: "..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=split(rec,'=') + key=recrd[1] or '' + value=recrd[2] or '' + if value then + if key=="TYPE" then + alarm_type=value + elseif key=="ID" then + alarm_id=value + end + end + end + if not (alarm_type==nil or alarm_id==nil or alarm_type=='' or alarm_id=='') then + if mqtt_client then + mqtt_path=string.gsub(mqtt_alarm_topic,"{(.-)}", + function (name) + if name=="dev" then + return web_devid + elseif name=="type" then + return sensor_type + elseif name=="id" then + return sensor_id + else + 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.. + " \""..string.gsub(alarm_type,"\"","\\\"").. + "\" \""..string.gsub(alarm_id,"\"","\\\"").. + "\" \""..string.gsub(msg_body,"\"","\\\"").."\"" + capture(command) + end + else + printLog("Cannot parse alarm input: "..msg_body) + end + end + +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 +end + +if serial_port then + serialin=io.open(serial_port,"r") +elseif input_file == "-" then + serialin=io.stdin; +elseif input_file then + serialin=io.open(input_file,"r") +elseif input_exec then + serialin=io.popen(input_exec,"r") +else + printLog("No input selected") + return +end + +records = {} + +while 1 do + line=serialin:read("*l") + if line == nil then + break + end + printLog("Received: "..line); + if startswith(line,'{') then + processJson(line) + else + 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/bin/weathermon-iio b/bin/weathermon-iio new file mode 100755 index 0000000..7fe5f68 --- /dev/null +++ b/bin/weathermon-iio @@ -0,0 +1,205 @@ +#!/usr/bin/lua + +require "uci" +cur = uci.cursor() + +lfs = require "lfs" +json = require "json" +socket = require "socket" + +require "wm_util" + +function get_device_list(config_name) + + local devices + devices = {} + + cur.foreach(config_name, "device", function(s) + devices[#devices+1] = s[".name"] + end) + + return devices + +end + +function get_device(config_name,device_name) + + local device + device = {} + + cur.foreach(config_name, "device", function(s) + if s[".name"] == device_name then + device = s + end + end) + + return device + +end + +function find_device(name,subsystem) + + local search_base + + if subsystem == "iio" then + search_base = "/sys/bus/iio/devices" + for file in lfs.dir(search_base) do + if get_file_content(search_base.."/"..file.."/name") == name then + return search_base.."/"..file + end + end + elseif subsystem == "hwmon" then + search_base = "/sys/class/hwmon" + for file in lfs.dir(search_base) do + if get_file_content(search_base.."/"..file.."/device/name") == name then + return search_base.."/"..file.."/device" + end + end + end + + return nil + +end + +function init_device(device,parameters,i2c_bus) + + if not i2c_bus then + i2c_bus = 0 + end + + if device["module"] then + os.execute("modprobe "..device["module"]) + end + + if device["type"] then + + devtype = split(device["type"],":") + bus = devtype[1] + subsystem = devtype[2] + + if (bus == "i2c") and device["address"] and device["name"] then + pcall(function () + local f = io.open("/sys/class/i2c-dev/i2c-"..i2c_bus.."/device/new_device","w") + io.output(f) + io.write(device["name"].." "..device["address"]) + io.close(f) + end) + end + + device_path=find_device(device["name"],subsystem) + + if device_path and device["set_param"] then + for key,record in pairs(device["set_param"]) do + setparam = split(record,":") + setpath = device_path.."/"..setparam[1] + pcall(function () + local f = io.open(setpath,"w") + io.output(f) + io.write(setparam[2]) + io.close(f) + end) + end + end + + if device_path and device["parameter"] then + + for key,record in pairs(device["parameter"]) do + + getparam = split(record,":") + getparameter = {} + getparameter["path"] = device_path.."/"..getparam[1] + getparameter["name"] = getparam[2] + getscale = getparam[3] + getcorrection = getparam[4] + if not getscale then + getscale = 1 + end + if not getcorrection then + getcorrection = 0 + end + getparameter["scale"] = tonumber(getscale) + getparameter["sensor"] = device["name"]:upper() + getparameter["correction"] = tonumber(getcorrection) + + parameters[#parameters+1] = getparameter + + end + + end + + end + +end + +function init(config_name) + + local parameters= {} + + i2c_bus = uci.get(config_name,"hardware","i2c_bus") + + local devices = get_device_list(config_name) + + for key,devname in pairs(devices) do + + device = get_device(config_name,devname) + + if device then + init_device(device,parameters,i2c_bus) + end + + end + + return parameters + +end + +function get_parameter(parameter) + return tonumber(get_file_content(parameter["path"])) * parameter["scale"] + parameter["correction"] +end + +function get_parameters(parameters) + local results = {} + for key,record in pairs(parameters) do + if not results[record["sensor"]] then + results[record["sensor"]] = {} + end + results[record["sensor"]][record["name"]] = get_parameter(record) + end + return results +end + +config_name = arg[1] +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") +if working_dir then + lfs.mkdir(working_dir) +end + +if not delay then + delay = 60 +end + +while true do + values = get_parameters(parameters) + records = {} + records[web_id] = {} + for key,record in pairs(values) do + 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/filter_meteo.py b/filter_meteo.py deleted file mode 100755 index fc497db..0000000 --- a/filter_meteo.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/python - -import MySQLdb -import ConfigParser -import sys - -from pprint import pprint -import datetime - -import numpy as np - -import scipy.signal - -global database - -def GetTables(): - if database: - c = database.cursor() - c.execute("SELECT s.id sid,p.id pid FROM sensors s,st_parameters p where s.st_id=p.st_id and p.id>=0") - return c.fetchall() - else: - print "No connection to DB" - exit() - -def Today(): - dt = datetime.datetime.now() - d_truncated = datetime.date(dt.year, dt.month, dt.day) - return d_truncated - -def Tomorrow(): - dt = Today() - return dt + datetime.timedelta(days=1) - -def Yesterday(): - dt = Today() - return dt - datetime.timedelta(days=1) - -def Prehistoric(): - dt = datetime.date(2000,01,01) - return dt - -def GetData(sid,pid,fromDate=Yesterday(),toDate=Today()): - if database: - c = database.cursor() - c.execute("SELECT id,timestamp,value FROM sensor_values WHERE sensor_id=%s and parameter_id=%s and timestamp>=%s AND timestamp<%s",[sid,pid,fromDate.strftime('%Y-%m-%d %H:%M:%S'),toDate.strftime('%Y-%m-%d %H:%M:%S')]) - return c.fetchall() - else: - print "No connection to DB" - exit() - -def FixRecord(id,value): - if database: - c = database.cursor() - command="UPDATE sensor_values SET value={} WHERE id='{}'".format(value,id) - c.execute(command) - else: - print "No connection to DB" - exit() - -def ProcessTable(sid,pid): - - if process_all: - data=GetData(sid,pid,Prehistoric(),Today()) - elif not current: - data=GetData(sid,pid) - else: - data=GetData(sid,pid,Today(),Tomorrow()) - - if not data: - return - - sID=[] - sTime=[] - sValue=[] - for rec in data: - sID.append(rec[0]) - sTime.append(rec[1]) - sValue.append(rec[2]) - sValue=np.array(sValue) - - sValueFilt=scipy.signal.medfilt(sValue,5) - - sValueDiff=abs(sValue-sValueFilt) - - avg=np.mean(sValueDiff) - - for i in range(0,len(sTime)-1): - if sValueDiff[i]>avg*filterThreshold: - print "fixing %s : %5.2f %5.2f %5.2f" % (sTime[i],sValue[i],sValueFilt[i],sValueDiff[i]) - FixRecord(sID[i],sValueFilt[i]) - - database.commit() - -if len(sys.argv)==2 and sys.argv[1]=='current': - current=True -else: - current=False - -if len(sys.argv)==2 and sys.argv[1]=='all': - process_all=True -else: - process_all=False - -try: - - cfg = ConfigParser.RawConfigParser(allow_no_value=True) - cfg.readfp(open('/etc/weathermon.conf')) - dbhost = cfg.get("mysql","host") - dbuser = cfg.get("mysql","user") - dbpasswd = cfg.get("mysql","passwd") - dbdb = cfg.get("mysql","db") - - filterWindow = int(cfg.get("filter","window")) - filterThreshold = float(cfg.get("filter","threshold")) - -except: - - print "Error reading configuration file" - exit() - -try: - - database = MySQLdb.connect(host=dbhost,user=dbuser,passwd=dbpasswd,db=dbdb,use_unicode=True) - database.set_character_set('utf8') - c = database.cursor() - c.execute('SET NAMES utf8;') - - print "Connected..." - -except: - - print "Error connecting database" - exit() - -tables = GetTables() - -for sid,pid in tables: - - print "Processing sensor %d, parameter %d " % (sid,pid) - - ProcessTable(sid,pid) - -print "Processed " - \ No newline at end of file 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/server/filter_meteo.py b/server/filter_meteo.py new file mode 100755 index 0000000..fc497db --- /dev/null +++ b/server/filter_meteo.py @@ -0,0 +1,144 @@ +#!/usr/bin/python + +import MySQLdb +import ConfigParser +import sys + +from pprint import pprint +import datetime + +import numpy as np + +import scipy.signal + +global database + +def GetTables(): + if database: + c = database.cursor() + c.execute("SELECT s.id sid,p.id pid FROM sensors s,st_parameters p where s.st_id=p.st_id and p.id>=0") + return c.fetchall() + else: + print "No connection to DB" + exit() + +def Today(): + dt = datetime.datetime.now() + d_truncated = datetime.date(dt.year, dt.month, dt.day) + return d_truncated + +def Tomorrow(): + dt = Today() + return dt + datetime.timedelta(days=1) + +def Yesterday(): + dt = Today() + return dt - datetime.timedelta(days=1) + +def Prehistoric(): + dt = datetime.date(2000,01,01) + return dt + +def GetData(sid,pid,fromDate=Yesterday(),toDate=Today()): + if database: + c = database.cursor() + c.execute("SELECT id,timestamp,value FROM sensor_values WHERE sensor_id=%s and parameter_id=%s and timestamp>=%s AND timestamp<%s",[sid,pid,fromDate.strftime('%Y-%m-%d %H:%M:%S'),toDate.strftime('%Y-%m-%d %H:%M:%S')]) + return c.fetchall() + else: + print "No connection to DB" + exit() + +def FixRecord(id,value): + if database: + c = database.cursor() + command="UPDATE sensor_values SET value={} WHERE id='{}'".format(value,id) + c.execute(command) + else: + print "No connection to DB" + exit() + +def ProcessTable(sid,pid): + + if process_all: + data=GetData(sid,pid,Prehistoric(),Today()) + elif not current: + data=GetData(sid,pid) + else: + data=GetData(sid,pid,Today(),Tomorrow()) + + if not data: + return + + sID=[] + sTime=[] + sValue=[] + for rec in data: + sID.append(rec[0]) + sTime.append(rec[1]) + sValue.append(rec[2]) + sValue=np.array(sValue) + + sValueFilt=scipy.signal.medfilt(sValue,5) + + sValueDiff=abs(sValue-sValueFilt) + + avg=np.mean(sValueDiff) + + for i in range(0,len(sTime)-1): + if sValueDiff[i]>avg*filterThreshold: + print "fixing %s : %5.2f %5.2f %5.2f" % (sTime[i],sValue[i],sValueFilt[i],sValueDiff[i]) + FixRecord(sID[i],sValueFilt[i]) + + database.commit() + +if len(sys.argv)==2 and sys.argv[1]=='current': + current=True +else: + current=False + +if len(sys.argv)==2 and sys.argv[1]=='all': + process_all=True +else: + process_all=False + +try: + + cfg = ConfigParser.RawConfigParser(allow_no_value=True) + cfg.readfp(open('/etc/weathermon.conf')) + dbhost = cfg.get("mysql","host") + dbuser = cfg.get("mysql","user") + dbpasswd = cfg.get("mysql","passwd") + dbdb = cfg.get("mysql","db") + + filterWindow = int(cfg.get("filter","window")) + filterThreshold = float(cfg.get("filter","threshold")) + +except: + + print "Error reading configuration file" + exit() + +try: + + database = MySQLdb.connect(host=dbhost,user=dbuser,passwd=dbpasswd,db=dbdb,use_unicode=True) + database.set_character_set('utf8') + c = database.cursor() + c.execute('SET NAMES utf8;') + + print "Connected..." + +except: + + print "Error connecting database" + exit() + +tables = GetTables() + +for sid,pid in tables: + + print "Processing sensor %d, parameter %d " % (sid,pid) + + ProcessTable(sid,pid) + +print "Processed " + \ No newline at end of file diff --git a/server/weathermon-mqtt b/server/weathermon-mqtt new file mode 100755 index 0000000..c6b6a82 --- /dev/null +++ b/server/weathermon-mqtt @@ -0,0 +1,94 @@ +#!/usr/bin/python -u + +import paho.mqtt.client as paho +import sys +from ConfigParser import ConfigParser +import MySQLdb +from pprint import pprint +import json +from dateutil.parser import parser +tparser=parser() + +def on_message(mosq, obj, msg): + topic=msg.topic + payload=json.loads(msg.payload) + timestamp=tparser.parse(payload['Time']) + for sensor_type in payload: + if sensor_type != 'Time' and sensor_type != 'TempUnit': + sensor_data=payload[sensor_type] + for param in sensor_data: + try: + value=sensor_data[param] + try: + c = database.cursor() + c.execute('CALL meteo.submit_mqtt(%s,%s,%s,%s,NULL)', (topic,sensor_type,param,value)) + database.commit() + print topic,sensor_type,param,value + except: + print "Failed to submit data" + except: + None + +def Topics(): + + c = database.cursor() + c.execute( + ''' + select topic from mqtt_topics where topic<>"" + ''' + ) + + topics=c.fetchall() + return topics + + +def Init(): + + global client,database + + conffile = sys.argv[1] + + config = ConfigParser() + config.add_section('mqtt') + # set defaults for anonymous auth + config.set('mqtt', 'username', '') + config.set('mqtt', 'password', '') + config.set('mqtt', 'port', '1883') + config.read(conffile) + + mqtt_server = config.get('mqtt', 'server') + mqtt_port = config.getint('mqtt', 'port') + mqtt_username = config.get('mqtt', 'username') + mqtt_password = config.get('mqtt', 'password') + + mysql_server = config.get('mysql', 'server') + mysql_username = config.get('mysql','username') + mysql_password = config.get('mysql','password') + mysql_db = config.get('mysql','db') + + client = paho.Client('weather') + client.username_pw_set(mqtt_username, mqtt_password) + client.on_message=on_message + client.connect(mqtt_server, port=mqtt_port) + + database = MySQLdb.connect(host=mysql_server,user=mysql_username,passwd=mysql_password,db=mysql_db,use_unicode=True,connect_timeout=10) + database.set_character_set('utf8') + c = database.cursor() + c.execute('SET NAMES utf8;') + + topics=[] + for topic in Topics(): + topics.append((topic[0].encode('UTF-8'),1)) + + client.subscribe(topics) + +Init() + +try: + while True: + try: + client.loop() + except: + break +finally: + exit() diff --git a/server/weathermon_cron b/server/weathermon_cron new file mode 100755 index 0000000..bc4eb88 --- /dev/null +++ b/server/weathermon_cron @@ -0,0 +1,209 @@ +#!/usr/bin/python + +from pprint import pprint +from os import system; + +import pycurl +from urllib import urlencode +from StringIO import StringIO + +import MySQLdb +import ConfigParser + +import uuid + +debug = True + +def print_log(str): + global logging,debug + if debug>0: + print str + if logging == "on": + system("logger -t weathermon \""+str+"\"") + +def submit_narodmon(): + + global debug + + param = { 'ID':devid } + + c = database.cursor() + c.execute( + ''' +select nm_id,avg(value) from meteo.ext_sensors e,meteo.sensor_values v +where +e.sensor_id=v.sensor_id and e.param_id=v.parameter_id +and v.timestamp>DATE_SUB(CURRENT_TIMESTAMP(),INTERVAL 15 MINUTE) +and e.nm_id is not null +group by e.sensor_id,e.param_id,e.nm_id,e.owm_id + ''' + ) + + queue=c.fetchall() + + if debug>1: + pprint(queue) + + for (sensor,value) in queue: + param[sensor] = value + + if debug>1: + pprint (param) + + url = "http://narodmon.ru/post.php" + + try: + + response_buffer = StringIO() + curl = pycurl.Curl() + + curl.setopt(curl.URL, url) + curl.setopt(curl.WRITEFUNCTION, response_buffer.write) + curl.setopt(curl.POST, 1) + curl.setopt(curl.POSTFIELDS, urlencode(param)) + + curl.perform() + curl.close() + + response_value = response_buffer.getvalue() + + print_log('Narodmon response: '+response_value) + + return True + + except: + + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) + traceback.print_exception(exc_type, exc_value, exc_traceback, + limit=2, file=sys.stdout) + return False + +def submit_owm(): + + global debug + + url = "http://openweathermap.org/data/post" + params = {'name':owm_station, 'lat':owm_lat, 'long':owm_lon} + + c = database.cursor() + c.execute( + ''' + select owm_id,value from + ( + SELECT sensor_id,parameter_id,avg(timestamp) timestamp,round(avg(value),1) value FROM meteo.sensor_values + where + timestamp>=date_sub(current_timestamp(), INTERVAL 15 MINUTE) + group by sensor_id,parameter_id + ) v,meteo.ext_sensors e + where v.sensor_id=e.sensor_id and v.parameter_id=e.param_id + and owm_id is not null + ''' + ) + + queue=c.fetchall() + + if debug>1: + pprint(queue) + + for (sensor,value) in queue: + params[sensor]=value + if debug>1: + pprint (params) + + try: + + response_buffer = StringIO() + curl = pycurl.Curl() + + curl.setopt(curl.URL, url) + curl.setopt(curl.USERPWD, '%s:%s' % (owmuser, owmpasswd)) + curl.setopt(curl.WRITEFUNCTION, response_buffer.write) + curl.setopt(curl.POST, 1) + curl.setopt(curl.POSTFIELDS, urlencode(params)) + + curl.perform() + curl.close() + + response_value = response_buffer.getvalue() + + print_log('Openweathermap response: '+response_value) + + return True + + except: + + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) + traceback.print_exception(exc_type, exc_value, exc_traceback, + limit=2, file=sys.stdout) + return False + +def main(): + submit_narodmon() + submit_owm() + +def init(): + + global logging,debug; + global devid; + global dbhost,dbuser,dbpasswd; + global owmuser,owmpasswd,owm_temp,owm_pres,owm_humi,owm_lat,owm_lon,owm_station; + + try: + + cfg = ConfigParser.RawConfigParser(allow_no_value=True) + cfg.readfp(open('/etc/weathermon.conf')) + + try: + logging = cfg.get("logging","enabled") + except: + logging = None + try: + debug = int(cfg.get("logging","debug")) + except: + debug = 0 + + dbhost = cfg.get("mysql","host") + dbuser = cfg.get("mysql","user") + dbpasswd = cfg.get("mysql","passwd") + try: + narmon = cfg.get("narodmon","enable") + except: + narmon = 'off' + try: + devid = cfg.get("narodmon","devid") + except: + devid = "{:0>12X}".format(getnode()) + try: + owmuser = cfg.get("openweathermap","user") + owmpasswd = cfg.get("openweathermap",'passwd') + except: + owmuser = None + if owmuser: + try: + owm_station=cfg.get("openweathermap","station") + owm_lat=cfg.get("openweathermap","lat") + owm_lon=cfg.get("openweathermap","lon") + except: + None + + global database + database = MySQLdb.connect(host=dbhost,user=dbuser,passwd=dbpasswd,use_unicode=True,connect_timeout=10) + database.set_character_set('utf8') + c = database.cursor() + c.execute('SET NAMES utf8;') + print_log("Database connected...") + connected = True + + except: + + print_log("Cannot intialize system") + exit() + +if __name__ == "__main__": + import sys + reload(sys) + sys.setdefaultencoding('utf-8') + init() + main() diff --git a/weathermon-iio b/weathermon-iio deleted file mode 100755 index 93b9cb5..0000000 --- a/weathermon-iio +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/lua - -require "uci" -cur = uci.cursor() - -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 - -function get_device_list(config_name) - - local devices - devices = {} - - cur.foreach(config_name, "device", function(s) - devices[#devices+1] = s[".name"] - end) - - return devices - -end - -function get_device(config_name,device_name) - - local device - device = {} - - cur.foreach(config_name, "device", function(s) - if s[".name"] == device_name then - device = s - end - end) - - return device - -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 - - if subsystem == "iio" then - search_base = "/sys/bus/iio/devices" - for file in lfs.dir(search_base) do - if get_file_content(search_base.."/"..file.."/name") == name then - return search_base.."/"..file - end - end - elseif subsystem == "hwmon" then - search_base = "/sys/class/hwmon" - for file in lfs.dir(search_base) do - if get_file_content(search_base.."/"..file.."/device/name") == name then - return search_base.."/"..file.."/device" - end - end - end - - return nil - -end - -function init_device(device,parameters,i2c_bus) - - if not i2c_bus then - i2c_bus = 0 - end - - if device["module"] then - os.execute("modprobe "..device["module"]) - end - - if device["type"] then - - devtype = split(device["type"],":") - bus = devtype[1] - subsystem = devtype[2] - - if (bus == "i2c") and device["address"] and device["name"] then - pcall(function () - local f = io.open("/sys/class/i2c-dev/i2c-"..i2c_bus.."/device/new_device","w") - io.output(f) - io.write(device["name"].." "..device["address"]) - io.close(f) - end) - end - - device_path=find_device(device["name"],subsystem) - - if device_path and device["set_param"] then - for key,record in pairs(device["set_param"]) do - setparam = split(record,":") - setpath = device_path.."/"..setparam[1] - pcall(function () - local f = io.open(setpath,"w") - io.output(f) - io.write(setparam[2]) - io.close(f) - end) - end - end - - if device_path and device["parameter"] then - - for key,record in pairs(device["parameter"]) do - - getparam = split(record,":") - getparameter = {} - getparameter["path"] = device_path.."/"..getparam[1] - getparameter["name"] = getparam[2] - getscale = getparam[3] - getcorrection = getparam[4] - if not getscale then - getscale = 1 - end - if not getcorrection then - getcorrection = 0 - end - getparameter["scale"] = tonumber(getscale) - getparameter["sensor"] = device["name"]:upper() - getparameter["correction"] = tonumber(getcorrection) - - parameters[#parameters+1] = getparameter - - end - - end - - end - -end - -function init(config_name) - - local parameters= {} - - i2c_bus = uci.get(config_name,"hardware","i2c_bus") - - local devices = get_device_list(config_name) - - for key,devname in pairs(devices) do - - device = get_device(config_name,devname) - - if device then - init_device(device,parameters,i2c_bus) - end - - end - - return parameters - -end - -function get_parameter(parameter) - return tonumber(get_file_content(parameter["path"])) * parameter["scale"] + parameter["correction"] -end - -function get_parameters(parameters) - local results = {} - for key,record in pairs(parameters) do - if not results[record["sensor"]] then - results[record["sensor"]] = {} - end - results[record["sensor"]][record["name"]] = get_parameter(record) - end - return results -end - -config_name = arg[1] -if not config_name then - config_name = "weathermon" -end - -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 - -if not delay then - delay = 60 -end - -while true do - values = get_parameters(parameters) - 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) - end - socket.sleep(delay) -end - \ No newline at end of file diff --git a/weathermon-mqtt b/weathermon-mqtt deleted file mode 100755 index c6b6a82..0000000 --- a/weathermon-mqtt +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/python -u - -import paho.mqtt.client as paho -import sys -from ConfigParser import ConfigParser -import MySQLdb -from pprint import pprint -import json -from dateutil.parser import parser -tparser=parser() - -def on_message(mosq, obj, msg): - topic=msg.topic - payload=json.loads(msg.payload) - timestamp=tparser.parse(payload['Time']) - for sensor_type in payload: - if sensor_type != 'Time' and sensor_type != 'TempUnit': - sensor_data=payload[sensor_type] - for param in sensor_data: - try: - value=sensor_data[param] - try: - c = database.cursor() - c.execute('CALL meteo.submit_mqtt(%s,%s,%s,%s,NULL)', (topic,sensor_type,param,value)) - database.commit() - print topic,sensor_type,param,value - except: - print "Failed to submit data" - except: - None - -def Topics(): - - c = database.cursor() - c.execute( - ''' - select topic from mqtt_topics where topic<>"" - ''' - ) - - topics=c.fetchall() - return topics - - -def Init(): - - global client,database - - conffile = sys.argv[1] - - config = ConfigParser() - config.add_section('mqtt') - # set defaults for anonymous auth - config.set('mqtt', 'username', '') - config.set('mqtt', 'password', '') - config.set('mqtt', 'port', '1883') - config.read(conffile) - - mqtt_server = config.get('mqtt', 'server') - mqtt_port = config.getint('mqtt', 'port') - mqtt_username = config.get('mqtt', 'username') - mqtt_password = config.get('mqtt', 'password') - - mysql_server = config.get('mysql', 'server') - mysql_username = config.get('mysql','username') - mysql_password = config.get('mysql','password') - mysql_db = config.get('mysql','db') - - client = paho.Client('weather') - client.username_pw_set(mqtt_username, mqtt_password) - client.on_message=on_message - client.connect(mqtt_server, port=mqtt_port) - - database = MySQLdb.connect(host=mysql_server,user=mysql_username,passwd=mysql_password,db=mysql_db,use_unicode=True,connect_timeout=10) - database.set_character_set('utf8') - c = database.cursor() - c.execute('SET NAMES utf8;') - - topics=[] - for topic in Topics(): - topics.append((topic[0].encode('UTF-8'),1)) - - client.subscribe(topics) - -Init() - -try: - while True: - try: - client.loop() - except: - break -finally: - exit() 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.lua b/weathermon.lua deleted file mode 100755 index 3208e30..0000000 --- a/weathermon.lua +++ /dev/null @@ -1,388 +0,0 @@ -#!/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 - -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 getConfig(configname) - - local uci=require("uci") - local cur=uci.cursor() - local config - if configname then - config=configname - else - config="weathermon" - end - - web_url = cur.get(config,"web","url") - web_user = cur.get(config,"web","user") - web_pass = cur.get(config,"web","password") - web_devid = cur.get(config,"web","devid") - - web_iface = cur.get(config,"web","iface") - - if web_iface then - - command = '/sbin/ifconfig '..web_iface..' | grep \'\\\' | sed -n \'1p\' | tr -s \' \' | cut -d \' \' -f3 | cut -d \':\' -f2' - f=io.popen(command,'r') - ip_addr=f:read() - - end - - if not web_devid then - - if web_iface then - io.input("/sys/class/net/"..web_iface.."/address") - else - io.input("/sys/class/net/eth0/address") - end - - mac = io.read("*line") - mac = mac:gsub(":","") - mac = mac:upper() - - web_devid = mac - - end - - logging = cur.get(config,"logging","enabled") - touch_file = cur.get(config,"logging","touch_file") - - serial_port = cur.get(config,"serial","port") - serial_baud = cur.get(config,"serial","baud") - - input_file = cur.get(config,"input","file") - input_exec = cur.get(config,"input","exec") - alarm_exec = cur.get(config,"alarm","exec") - - if serial_port then - - command = "stty -F "..serial_port.." "..serial_baud - capture(command) - - end - - mqtt_host = cur.get(config,"mqtt","host") - mqtt_port = cur.get(config,"mqtt","port") - mqtt_id = cur.get(config,"mqtt","id") - mqtt_topic = cur.get(config,"mqtt","topic") - mqtt_alarm_topic = cur.get(config,"mqtt","alarm_topic") - - mqtt_user = cur.get(config,"mqtt","user") - mqtt_passwd = cur.get(config,"mqtt","password") - - if mqtt_host and not mqtt_id then - mqtt_id="weather-"..web_devid - end - - if mqtt_host and not mqtt_port then - mqtt_port = 1883 - end - - if mqtt_host and not mqtt_topic then - mqtt_topic = 'weathermon/{dev}/{type}/{id}/{param}' - end - - if mqtt_host and not mqtt_alarm_topic then - 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 - -end - -function printLog(str) - if logging=="on" then - capture("logger -t weathermon "..str) - print(str) - elseif logging=="syslog" then - capture("logger -t weathermon "..str) - elseif logging=="stdout" then - print(str) - end -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) - - command = "curl" - - if web_iface then - command = command.." --interface "..ip_addr - end - - if web_user then - command = command.." -u "..web_user..":"..web_pass - end - - command = command.." \""..url.."\" 2>&1" - - result = capture(command) - - touch() - -end - -function processJson(str) - - msg=json.decode(str) - - sensor={} - - for key,value in pairs(msg) do - if value then - if key=="model" or key=="device" then - sensor_type=value - elseif key=="id" then - sensor_id=value - elseif key=='time' then - sensor_time=value - else - sensor[key]=value - end - end - end - - if not sensor_id then - sensor_id = web_devid - end - - if not (sensor_type==nil or sensor_id==nil or sensor_type=='' or sensor_id=='') then - if next(sensor)==nil then - sensor["command"]="alarm" - end - for k,v in pairs(sensor) do - printLog("Type = "..sensor_type..", ID = "..sensor_id..", Param = "..k..", Value = \""..v.."\"") - submitValue(sensor_type,sensor_id,k,v) - if mqtt_client then - mqtt_path=string.gsub(mqtt_topic,"{(.-)}", - function (name) - if name=="dev" then - return mqtt_encode(web_devid) - elseif name=="type" then - return mqtt_encode(sensor_type) - elseif name=="id" then - return mqtt_encode(sensor_id) - elseif name=="param" then - return k - else - return '{'..name..'}' - end - end) - mqtt_client:publish(mqtt_path,v) - end - end - else - printLog("Cannot parse sensor input: "..str) - end - -end - -function processLine(str) - - msg=splitStr(line,':') - msg_type=msg[1] or '' - msg_body=msg[2] or '' - if msg_type=="STATUS" then - printLog("Status: "..msg_body) - elseif msg_type=="ERROR" then - printLog("Error: "..msg_body) - elseif msg_type=="SENSOR" then - printLog("SENSOR: "..msg_body) - sens = splitStr(msg_body,",") - sensor = {} - idx = 1 - sensor_type = nil - sensor_id = web_devid - for i,rec in ipairs(sens) do - recrd=splitStr(rec,'=') - key=recrd[1] or '' - value=recrd[2] or '' - if value then - if key=="TYPE" then - sensor_type=value - elseif key=="ID" then - sensor_id=value - else - sensor[key]=value - end - end - end - if not (sensor_type==nil or sensor_id==nil or sensor_type=='' or sensor_id=='') then - for k,v in pairs(sensor) do - printLog("Type = "..sensor_type..", ID = "..sensor_id..", Param = "..k..", Value = "..v) - submitValue(sensor_type,sensor_id,k,v) - if mqtt_client then - mqtt_path=string.gsub(mqtt_topic,"{(.-)}", - function (name) - if name=="dev" then - return web_devid - elseif name=="type" then - return sensor_type - elseif name=="id" then - return sensor_id - elseif name=="param" then - return k - else - return '{'..name..'}' - end - end) - mqtt_client:publish(mqtt_path,v) - end - end - else - printLog("Cannot parse sensor input: "..msg_body) - end - elseif msg_type=="ALARM" then - printLog("ALARM: "..msg_body) - sens = splitStr(msg_body,",") - sensor = {} - idx = 1 - sensor_type = nil - sensor_id = web_devid - mqtt_param = {} - for i,rec in ipairs(sens) do - recrd=splitStr(rec,'=') - key=recrd[1] or '' - value=recrd[2] or '' - if value then - if key=="TYPE" then - alarm_type=value - elseif key=="ID" then - alarm_id=value - end - end - end - if not (alarm_type==nil or alarm_id==nil or alarm_type=='' or alarm_id=='') then - if mqtt_client then - mqtt_path=string.gsub(mqtt_alarm_topic,"{(.-)}", - function (name) - if name=="dev" then - return web_devid - elseif name=="type" then - return sensor_type - elseif name=="id" then - return sensor_id - else - return '{'..name..'}' - end - end) - mqtt_client:publish(mqtt_path,msg_body) - end - if alarm_exec then - command=alarm_exec.. - " \""..string.gsub(alarm_type,"\"","\\\"").. - "\" \""..string.gsub(alarm_id,"\"","\\\"").. - "\" \""..string.gsub(msg_body,"\"","\\\"").."\"" - capture(command) - end - else - printLog("Cannot parse alarm input: "..msg_body) - end - end - -end - -getConfig(arg[1]) - -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 - serialin=io.open(serial_port,"r") -elseif input_file == "-" then - serialin=io.stdin; -elseif input_file then - serialin=io.open(input_file,"r") -elseif input_exec then - serialin=io.popen(input_exec,"r") -else - printLog("No input selected") - return -end -while 1 do - line=serialin:read("*l") - if line == nil then - break - end - printLog("Received: "..line); - if startswith(line,'{') then - processJson(line) - else - processLine(line) - end -end 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" diff --git a/weathermon_cron b/weathermon_cron deleted file mode 100755 index bc4eb88..0000000 --- a/weathermon_cron +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/python - -from pprint import pprint -from os import system; - -import pycurl -from urllib import urlencode -from StringIO import StringIO - -import MySQLdb -import ConfigParser - -import uuid - -debug = True - -def print_log(str): - global logging,debug - if debug>0: - print str - if logging == "on": - system("logger -t weathermon \""+str+"\"") - -def submit_narodmon(): - - global debug - - param = { 'ID':devid } - - c = database.cursor() - c.execute( - ''' -select nm_id,avg(value) from meteo.ext_sensors e,meteo.sensor_values v -where -e.sensor_id=v.sensor_id and e.param_id=v.parameter_id -and v.timestamp>DATE_SUB(CURRENT_TIMESTAMP(),INTERVAL 15 MINUTE) -and e.nm_id is not null -group by e.sensor_id,e.param_id,e.nm_id,e.owm_id - ''' - ) - - queue=c.fetchall() - - if debug>1: - pprint(queue) - - for (sensor,value) in queue: - param[sensor] = value - - if debug>1: - pprint (param) - - url = "http://narodmon.ru/post.php" - - try: - - response_buffer = StringIO() - curl = pycurl.Curl() - - curl.setopt(curl.URL, url) - curl.setopt(curl.WRITEFUNCTION, response_buffer.write) - curl.setopt(curl.POST, 1) - curl.setopt(curl.POSTFIELDS, urlencode(param)) - - curl.perform() - curl.close() - - response_value = response_buffer.getvalue() - - print_log('Narodmon response: '+response_value) - - return True - - except: - - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) - traceback.print_exception(exc_type, exc_value, exc_traceback, - limit=2, file=sys.stdout) - return False - -def submit_owm(): - - global debug - - url = "http://openweathermap.org/data/post" - params = {'name':owm_station, 'lat':owm_lat, 'long':owm_lon} - - c = database.cursor() - c.execute( - ''' - select owm_id,value from - ( - SELECT sensor_id,parameter_id,avg(timestamp) timestamp,round(avg(value),1) value FROM meteo.sensor_values - where - timestamp>=date_sub(current_timestamp(), INTERVAL 15 MINUTE) - group by sensor_id,parameter_id - ) v,meteo.ext_sensors e - where v.sensor_id=e.sensor_id and v.parameter_id=e.param_id - and owm_id is not null - ''' - ) - - queue=c.fetchall() - - if debug>1: - pprint(queue) - - for (sensor,value) in queue: - params[sensor]=value - if debug>1: - pprint (params) - - try: - - response_buffer = StringIO() - curl = pycurl.Curl() - - curl.setopt(curl.URL, url) - curl.setopt(curl.USERPWD, '%s:%s' % (owmuser, owmpasswd)) - curl.setopt(curl.WRITEFUNCTION, response_buffer.write) - curl.setopt(curl.POST, 1) - curl.setopt(curl.POSTFIELDS, urlencode(params)) - - curl.perform() - curl.close() - - response_value = response_buffer.getvalue() - - print_log('Openweathermap response: '+response_value) - - return True - - except: - - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) - traceback.print_exception(exc_type, exc_value, exc_traceback, - limit=2, file=sys.stdout) - return False - -def main(): - submit_narodmon() - submit_owm() - -def init(): - - global logging,debug; - global devid; - global dbhost,dbuser,dbpasswd; - global owmuser,owmpasswd,owm_temp,owm_pres,owm_humi,owm_lat,owm_lon,owm_station; - - try: - - cfg = ConfigParser.RawConfigParser(allow_no_value=True) - cfg.readfp(open('/etc/weathermon.conf')) - - try: - logging = cfg.get("logging","enabled") - except: - logging = None - try: - debug = int(cfg.get("logging","debug")) - except: - debug = 0 - - dbhost = cfg.get("mysql","host") - dbuser = cfg.get("mysql","user") - dbpasswd = cfg.get("mysql","passwd") - try: - narmon = cfg.get("narodmon","enable") - except: - narmon = 'off' - try: - devid = cfg.get("narodmon","devid") - except: - devid = "{:0>12X}".format(getnode()) - try: - owmuser = cfg.get("openweathermap","user") - owmpasswd = cfg.get("openweathermap",'passwd') - except: - owmuser = None - if owmuser: - try: - owm_station=cfg.get("openweathermap","station") - owm_lat=cfg.get("openweathermap","lat") - owm_lon=cfg.get("openweathermap","lon") - except: - None - - global database - database = MySQLdb.connect(host=dbhost,user=dbuser,passwd=dbpasswd,use_unicode=True,connect_timeout=10) - database.set_character_set('utf8') - c = database.cursor() - c.execute('SET NAMES utf8;') - print_log("Database connected...") - connected = True - - except: - - print_log("Cannot intialize system") - exit() - -if __name__ == "__main__": - import sys - reload(sys) - sys.setdefaultencoding('utf-8') - init() - main()