From e32107a7fe79ce34f3bdf860410a6d5455efdca7 Mon Sep 17 00:00:00 2001
From: Roman Bazalevsky <rvb@rvb.name>
Date: Mon, 12 Nov 2018 20:18:30 +0300
Subject: [PATCH] =?utf8?q?-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
 =?utf8?q?=D0=BD=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=20=D0=B4?=
 =?utf8?q?=D0=BB=D1=8F=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F=20iio-=D0=B4?=
 =?utf8?q?=D0=B0=D1=82=D1=87=D0=B8=D0=BA=D0=BE=D0=B2=20-=20=D0=94=D0=BE?=
 =?utf8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE=D1=85?=
 =?utf8?q?=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=BE?=
 =?utf8?q?=D1=82=D0=BE=D1=81=D0=BB=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4?=
 =?utf8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20=D0=BE=D1=87=D0=B5?=
 =?utf8?q?=D1=80=D0=B5=D0=B4=D0=B8=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?=
 =?utf8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C?=
 =?utf8?q?=D0=BD=D0=BE=D0=B5=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8?=
 =?utf8?q?=D1=89=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=20?=
 =?utf8?q?=D0=BB=D0=B5=D0=B3=D0=BA=D0=BE=D0=B2=D0=B5=D1=81=D0=BD=D1=8B?=
 =?utf8?q?=D0=B9=20=D0=B2=D0=B5=D0=B1-=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84?=
 =?utf8?q?=D0=B5=D0=B9=D1=81=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?=
 =?utf8?q?=D0=B5=D0=BD=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8?=
 =?utf8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B9=20=D0=B1=D1=8D=D0=BA=D0=B0?=
 =?utf8?q?=D0=BF=20(=D1=80=D0=B5=D0=B3=D1=83=D0=BB=D1=8F=D1=80=D0=BD=D0=BE?=
 =?utf8?q?=20=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7?=
 =?utf8?q?=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<date('now','-1 day')"
diff --git a/bin/weather-backlog b/bin/weather-backlog
new file mode 100755
index 0000000..84844cc
--- /dev/null
+++ b/bin/weather-backlog
@@ -0,0 +1,77 @@
+#!/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")
+ 
+  if not web_timeout then
+    web_timeout = 10
+  end
+
+  backlogdb = cur.get(config,"process","backlogdb")
+
+end
+
+function submitValue(timestamp,type,id,param,val)
+
+  local url = web_url.."?stype="..url_encode(type).."&sid="..url_encode(id).."&param="..url_encode(param).."&value="..url_encode(val).."&time="..url_encode(timestamp)
+
+  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
+    return false
+  end
+
+  return true
+
+end
+
+getConfig(arg[1])
+
+if backlogdb then
+
+  local dbdriver = require "luasql.sqlite3"
+  env = assert(dbdriver.sqlite3())
+  if file_exists(backlogdb) then
+    backlog_con = assert(env:connect(backlogdb))
+  end
+
+  cursor = assert(backlog_con:execute("SELECT rowid,time_stamp,sensor_id,sensor,param,value FROM queue LIMIT 400"))
+  row = cursor:fetch ({}, "a")
+  while row do
+    if submitValue(row.time_stamp,row.sensor,row.sensor_id,row.param,row.value) then
+      backlog_con:execute(string.format("DELETE FROM queue WHERE rowid='%s'",row.rowid))
+    end
+    row = cursor:fetch (row, "a")
+  end
+
+end
diff --git a/bin/weather-backup b/bin/weather-backup
new file mode 100755
index 0000000..fd219f7
--- /dev/null
+++ b/bin/weather-backup
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+BACKUP_DIR=`uci get weathermon.process.backup_dir`
+BACKLOGDB=`uci get weathermon.process.backlogdb` 
+LOGDB=`uci get weathermon.process.logdb` 
+
+if [ ! -z "$BACKLOGDB" ]; then
+	BACKLOG_BASE=$(basename "$BACKLOGDB")
+	sqlite3 "$BACKLOGDB" ".backup $BACKUP_DIR/$BACKLOG_BASE" 
+fi
+
+if [ ! -z "$LOGDB" ]; then
+	LOG_BASE=$(basename "$LOGDB")
+	sqlite3 "$LOGDB" ".backup $BACKUP_DIR/$LOG_BASE" 
+fi
diff --git a/bin/weather-display b/bin/weather-display
new file mode 100755
index 0000000..1e0993b
--- /dev/null
+++ b/bin/weather-display
@@ -0,0 +1,262 @@
+#!/usr/bin/lua
+
+require "uci"
+local cur = uci.cursor()
+local socket = require "socket"
+local lfs = require "lfs"
+local json = require "json"
+
+require "wm_util"
+
+function get_levels_list(config_name)
+  local levels, engage, engage_mode, disengage_mode, levels_idx
+  levels = {}
+  levels_idx = {}
+  engage = {}
+  engage_mode = {}
+  disengage_mode = {}
+  cur.foreach(config_name, "alarm", function(s)
+    local idx = #levels+1
+    levels[idx] = s["name"]
+    engage[idx] = s["engage"]
+    engage_mode[idx] = s["engage_mode"]
+    disengage_mode[idx] = s["disengage_mode"]
+    levels_idx[s[".name"]] = idx
+  end)
+  return levels, engage, engage_mode, disengage_mode, levels_idx
+end
+
+function get_params(config_name,levels_idx)
+  local a_names, a_formats, a_limits
+  a_names = {}
+  a_formats = {}
+  a_limits = {}
+  cur.foreach(config_name,"params", function(s)
+    if s["name"] then
+      a_names[s["param"]] = s["name"]
+    end
+    if s["format"] or s["scale"] then
+      local format, scale
+      if s["scale"] then
+        scale = s["scale"]
+      else
+        scale = 1  
+      end
+      if s["format"] then
+        format = s["format"]
+      else
+        format = "4s"
+      end
+      a_formats[s["param"]] = {format,scale}
+    end  
+    if s["limits"] then
+      for i,record in pairs(s["limits"]) do
+        rec = split(record,":")
+        idx = levels_idx[rec[1]]
+        low = tonumber(rec[2])
+        high = tonumber(rec[3])
+        if not a_limits[s["param"]] then
+          a_limits[s["param"]] = {}
+        end
+        if not a_limits[s["param"]][idx] then
+          a_limits[s["param"]][idx] = {}
+        end
+        rec = a_limits[s["param"]][idx]
+        a_limits[s["param"]][idx][#rec+1] = {low,high}
+      end  
+    end
+  end)
+  return a_names,a_formats,a_limits;
+end
+
+function check_limit(param,value,limits)
+
+  limit = limits[param]
+  if limit then
+  
+    for level,ranges in pairs(limit) do
+      for key,range in pairs(ranges) do
+        if value>=range[1] and value<range[2] then
+          return level
+        end  
+      end
+    end
+
+    return 0
+  
+  else
+  
+    return 0
+  
+  end
+
+end
+
+local config_name = arg[1]
+
+if not config_name then
+  config_name = "weathermon"
+end
+
+print("loading config...")
+
+local a_levels, a_engage, a_engage_mode, a_disengage_mode, levels_idx = get_levels_list(config_name)
+
+a_leds = {}
+
+for level,leds in pairs(a_engage) do
+
+  for key,led in pairs(leds) do
+  
+    a_leds[led] = a_disengage_mode[level]
+  
+  end
+
+end
+
+local a_names,a_formats,limits = get_params(config_name,levels_idx)
+
+local w_led = cur.get(config_name, "process", "engage")
+local w_engage = cur.get(config_name, "process", "engage_mode")
+local w_disengage = cur.get(config_name, "process", "disengage_mode")
+
+local senstemplate = string.gsub(cur.get(config_name, "display", "formatstr"),"~",string.char(223))
+local out_file = cur.get(config_name, "display", "file")
+
+local watch_file = cur.get(config_name,"process","dump_file")
+
+if not watch_file then
+  return
+end  
+
+local last = 0
+
+local data = ""
+local old_data = ""
+local sensor_data
+local res
+
+local mute_file=cur.get(config_name, "process", "mute_file")
+local mute_time=tonumber(cur.get(config_name, "process", "mute_time"))
+local alarm_raise = 2
+local onoff=true
+
+local timestr_template = cur.get(config_name, "display", "timestr")
+if not timestr_template then
+  timestr_template = "  %d.%m.%y   %H:%M  "
+end
+
+local alarmstr_len = cur.get(config_name, "display", "strlen")
+if not alarmstr_len then
+  alarmstr_len = 20
+end
+alarmstr_len=tonumber(alarmstr_len)
+
+web_id = uci.get(config_name,"web","devid")
+
+while true do
+
+  onoff = not onoff
+
+  local timestr = os.date(timestr_template)
+
+  local mod = lfs.attributes(watch_file,"modification")
+  local muted_mod = lfs.attributes(mute_file,"modification")
+  local muted_beep = muted_mod and (muted_mod > (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()<alarmstr_len then
+      local delta = alarmstr_len - alarmstr:len()
+      local before = math.floor(delta/2)
+      local after = delta - before
+      alarmstr = string.rep(" ",before)..alarmstr..string.rep(" ",after)
+    end
+
+    sensstr = string.gsub(senstemplate,"%{.-%}", function (s) return printable[s:sub(2,s:len()-1)]; end)
+
+    if onoff and (level>=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<date('%s','+1 day') ORDER BY sensor_id,sensor,param",day,day)
+  end  
+  return run_sql(sql,con)
+end
+
+function get_raw(day,con,sensor_id,sensor_type,param)
+  format = '%Y-%m-%dT%H:%M:%S'
+  if day == "-" then
+    sql = string.format("SELECT strftime('%s',time_stamp) as t,value as y FROM log WHERE 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<date('%s','+1 day') and sensor_id='%s' and sensor='%s' and param='%s' ORDER BY time_stamp",format,day,day,sensor_id,sensor_type,param)
+  end
+  return run_sql(sql,con)
+end
+
+function get_filtered(day,con,sensor_id,sensor_type,param,width)
+  format = '%Y-%m-%dT%H:%M:%S'
+  if day == "-" then
+    sql = string.format("SELECT strftime('%s',time_stamp) as t,value as y FROM log WHERE 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<date('%s','+1 day') and sensor_id='%s' and sensor='%s' and param='%s' ORDER BY time_stamp",format,day,day,sensor_id,sensor_type,param)
+  end
+  return filter_data(run_sql(sql,con),width)
+end
+
+function dump_json(dataset,file)
+  local f
+  if file then
+    f = io.open(file,"w")
+    io.output(f)
+  end
+  io.write(json.encode(dataset))
+  if f then
+    io.close(f)
+  end   
+end
+
+function dump_txt(dataset,file)
+  local f
+  if file then
+    f = io.open(file,"w")
+    io.output(f)
+  end
+  for key,row in pairs(dataset) do
+    io.write(row["t"].." "..row["y"].."\n")
+  end
+  if f then
+    io.close(f)
+  end   
+end
+
+function dump_list(dataset,file)
+  local f
+  if file then
+    f = io.open(file,"w")
+    io.output(f)
+  end
+  for key,row in pairs(dataset) do
+    io.write(row["sensor_id"].." "..row["sensor"].." "..row["param"].."\n")
+  end
+  if f then
+    io.close(f)
+  end   
+end
+
+local command = arg[2]
+local day = arg[3]
+
+local dbdriver = require "luasql.sqlite3"
+env = assert(dbdriver.sqlite3())
+con = assert(env:connect(logdb))
+
+if command == "list" then
+
+  dump_list(get_list(day,con))
+
+elseif command == "get" then
+
+  sensor_id = arg[4]
+  sensor_type = arg[5]
+  param = arg[6]
+
+  dump_txt(get_raw(day,con,sensor_id,sensor_type,param))
+  
+elseif command == "get-filtered" then
+
+  sensor_id = arg[4]
+  sensor_type = arg[5]
+  param = arg[6]
+
+  width = arg[7]
+  if not width then
+    width = 5
+  end  
+
+  dataset =get_filtered(day,con,sensor_id,sensor_type,param)
+  dump_txt(dataset)
+
+elseif command == "get-compacted" then
+
+  sensor_id = arg[4]
+  sensor_type = arg[5]
+  param = arg[6]
+
+  width = arg[7]
+  if not width then
+    width = 5
+  end  
+
+  dataset =get_filtered(day,con,sensor_id,sensor_type,param)
+  dump_txt(average_results(dataset))
+
+elseif command == "dump" then
+
+  local directory = arg[4]
+  if not directory then 
+    directory = "."
+  end  
+  
+  list = get_list(day,con)
+  dump_json(list,directory.."/sensors.json")
+  for key,value in pairs(list) do
+    dump_json(get_filtered(day,con,value["sensor_id"],value["sensor"],value["param"]),directory.."/"..value["sensor_id"].."."..value["sensor"].."."..value["param"]..".json")
+  end
+
+elseif command == "dump-compacted" then
+
+  local directory = arg[4]
+  if not directory then 
+    directory = "."
+  end  
+  
+  list = get_list(day,con)
+  dump_json(list,directory.."/sensors.json")
+  for key,value in pairs(list) do
+    dump_json(average_results(get_filtered(day,con,value["sensor_id"],value["sensor"],value["param"])),directory.."/"..value["sensor_id"].."."..value["sensor"].."."..value["param"]..".json")
+  end
+  
+end
diff --git a/bin/weather-init b/bin/weather-init
new file mode 100755
index 0000000..d009c67
--- /dev/null
+++ b/bin/weather-init
@@ -0,0 +1,33 @@
+# Put your custom commands here that should be executed once
+# the system init finished. By default this file does nothing.
+
+I2C_BUS=0
+DS3221=68
+
+echo heartbeat > /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).."&param="..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).."&param="..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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="Cache-Control" content="no-cache" />
+    <link rel="shortcut icon" href="/luci-static/material/favicon.ico">
+    <link rel="stylesheet" href="/meteo/meteo.css" type="text/css" />
+    <script type="text/javascript" src="/js/Chart.bundle.min.js"></script>
+    <script type="text/javascript" src="/meteo/archive.js"></script>
+</head>
+<body>
+
+<header>
+    <div class="fill">
+        <div class="container">
+            <a class="brand" href="/"><img width="48" src="/meteo/weather.svg"/></a>
+            <a class="brand" href="/meteo">Метеостанция</a>
+            <a class="brand" id="current" href="/meteo">Последние сутки</a>
+        </div>
+    </div>
+</header>
+
+<div class="main">
+    <div id="maincontent">
+        <div class="container">
+            <div class="wide-section">
+              <div class="selector-header">Год</div>
+              <div class="selector"><select id="year" onchange="selectChange();"></select></div>
+              <div class="selector-header">Месяц</div>
+              <div class="selector"><select id="month" onchange="selectChange();"></select></div>
+              <div class="selector-header">День</div>
+              <div class="selector"><select id="day" onchange="selectChange();"></select></div>
+              <div class="selector-header">Параметр</div>
+              <div class="selector"><select id="sensor" onchange="selectChange();"></select></div>
+            </div>
+            <div class="bottom-section" id="chartdiv">
+              <canvas id="chart">
+              </canvas>
+            </div>
+        </div>
+    </div>
+</div>
+
+<footer>Powered by OpenWRT</footer>
+
+</body>
+</html>
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 = "<option value=\""+data[i]+"\">"+nameFunc(data[i])+"</option>"
+    } else {
+      line = "<option value=\""+data[i]+"\">"+data[i]+"</option>"
+    }
+    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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="Cache-Control" content="no-cache" />
+    <link rel="shortcut icon" href="/luci-static/material/favicon.ico">
+    <link rel="stylesheet" href="/meteo/meteo.css" type="text/css" />
+    <script type="text/javascript" src="/js/Chart.bundle.min.js"></script>
+    <script type="text/javascript" src="/meteo/graph.js"></script>
+</head>
+<body>
+
+<header>
+    <div class="fill">
+        <div class="container">
+            <a class="brand" href="/"><img width="48" src="/meteo/weather.svg"/></a>
+            <a class="brand" href="/meteo">Метеостанция</a>
+            <a class="brand" id="archive" href="/meteo">Архивы</a>
+        </div>
+    </div>
+</header>
+
+<div class="main">
+    <div id="maincontent">
+        <div class="container">
+            <div class="large-section" id="chartdiv">
+              <canvas id="chart">
+              </canvas>
+            </div>
+        </div>
+    </div>
+</div>
+
+<footer>Powered by OpenWRT</footer>
+
+</body>
+</html>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="Cache-Control" content="no-cache" />
+    <link rel="shortcut icon" href="/luci-static/material/favicon.ico">
+    <link rel="stylesheet" href="/meteo/meteo.css" type="text/css" />
+    <script type="text/javascript" src="/meteo/meteo.js"></script>
+</head>
+<body>
+
+<header>
+    <div class="fill">
+        <div class="container">
+            <a class="brand" href="/"><img width="48" src="weather.svg"/></a>
+            <a class="brand" href="archive">Архивы метеонаблюдений</a>
+	</div>
+</header>
+
+
+<div style="display: none;" id="template">
+<a href="graph/$SENSOR_ID/$SENSOR/$PARAM">
+<div class="section" title="$TIMESTAMP">
+    <div class="reference-header" style="color: $COLOR;">$NAME</div>
+    <div class="reference" id="value">$VALUE</div>
+    <div class="reference-unit">$UNITS</div>
+</div>
+</a>
+</div>
+
+<div class="main">
+
+    <div id="maincontent">
+			
+        <div class="container" id="meteo">
+
+        </div>
+    
+    </div>
+
+</div>
+
+<footer>Powered by OpenWRT</footer>
+
+</body>
+</html>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"
+	 viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#4F5D73;}
+	.st1{opacity:0.2;}
+	.st2{fill:#231F20;}
+	.st3{fill:#E0995E;}
+	.st4{fill:#FFFFFF;}
+</style>
+<g id="Layer_1">
+	<g>
+		<circle class="st0" cx="32" cy="32" r="32"/>
+	</g>
+	<g class="st1">
+		<circle class="st2" cx="22" cy="24" r="10"/>
+	</g>
+	<g>
+		<circle class="st3" cx="22" cy="22" r="10"/>
+	</g>
+	<g class="st1">
+		<path class="st2" d="M48.7,36c0-7.7-6.6-14-14.7-14c-6.9,0-12.6,4.5-14.2,10.6c-4.4,0.6-7.8,4.3-7.8,8.6c0,4.8,4.1,8.8,9.2,8.8
+			h27.5c4.1,0,7.3-3.1,7.3-7S52.7,36,48.7,36z"/>
+	</g>
+	<g>
+		<g class="st1">
+			<path class="st2" d="M32,22c0-1-0.2-2-0.4-2.9c-6.2,0.6-11.3,4.9-12.8,10.5c-0.8,0.1-1.6,0.4-2.4,0.7C18,31.4,19.9,32,22,32
+				C27.5,32,32,27.5,32,22z"/>
+		</g>
+	</g>
+	<g>
+		<path class="st4" d="M48.7,34c0-7.7-6.6-14-14.7-14c-6.9,0-12.6,4.5-14.2,10.6c-4.4,0.6-7.8,4.3-7.8,8.6c0,4.8,4.1,8.8,9.2,8.8
+			h27.5c4.1,0,7.3-3.1,7.3-7S52.7,34,48.7,34z"/>
+	</g>
+</g>
+<g id="Layer_2">
+</g>
+</svg>
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