Полностью новая версия веб-интерфейса на базе chart.js
authorRoman Bazalevsky <rvb@rvb.name>
Wed, 28 Nov 2018 14:17:23 +0000 (17:17 +0300)
committerRoman Bazalevsky <rvb@rvb.name>
Wed, 28 Nov 2018 14:17:23 +0000 (17:17 +0300)
13 files changed:
web/archive.html [new file with mode: 0644]
web/archive.js [new file with mode: 0644]
web/auth.php [new file with mode: 0644]
web/cgi.php [new file with mode: 0644]
web/config_local.php
web/config_local.php~ [new file with mode: 0644]
web/graph.html [new file with mode: 0644]
web/graph.js [new file with mode: 0644]
web/index.html [new file with mode: 0644]
web/meteo.css [new file with mode: 0644]
web/meteo.js [new file with mode: 0644]
web/send.php
web/weather.svg [new file with mode: 0644]

diff --git a/web/archive.html b/web/archive.html
new file mode 100644 (file)
index 0000000..e402043
--- /dev/null
@@ -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="/meteo/weather.svg">
+    <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/graph/">Последние сутки</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 Ubuntu 18.04</footer>
+
+</body>
+</html>
diff --git a/web/archive.js b/web/archive.js
new file mode 100644 (file)
index 0000000..9420250
--- /dev/null
@@ -0,0 +1,328 @@
+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 sensor_path = devid+"."+sensor+"."+param;
+
+  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["places"][sensor_path] + ' - ' + properties["names"][sensor_path],
+            backgroundColor: color(properties["colors"][sensor_path]).alpha(0.5).rgbString(),
+            borderColor: properties["colors"][sensor_path],
+            data: processDataset(graphData,devid,sensor,param),
+            type: 'line',
+            pointRadius: 0,
+            fill: true,
+            borderWidth: 2
+          }
+        ]      
+      },
+      options: {
+        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);
+  };
+  
+  req.open("GET", urlbase+"get-archive/"+year+"/"+month+"/"+day+"/"+devid+"/"+sensor+"/"+param, true);
+  req.withCredentials = 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.withCredentials = 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.withCredentials = 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.withCredentials = 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.withCredentials = true;
+  req.send();
+  
+}
+
+function SensorName(id) {
+  return properties["places"][id]+" - "+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.withCredentials = true;
+  req.send();
+  
+}
+
+setTimeout(GetProperties,100);
diff --git a/web/auth.php b/web/auth.php
new file mode 100644 (file)
index 0000000..4807f49
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+if (!function_exists('com_create_guid')) {
+  function com_create_guid() {
+    return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+        mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
+        mt_rand( 0, 0xffff ),
+        mt_rand( 0, 0x0fff ) | 0x4000,
+        mt_rand( 0, 0x3fff ) | 0x8000,
+        mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
+    );
+  }
+}
+
+include('config_local.php');
+
+if (! ($db = new PDO("mysql:host=$mysql_host;port=$mysql_port;dbname=$mysql_schema",$mysql_user,$mysql_pwd,array( PDO::ATTR_PERSISTENT => false)))) {
+  die($err);
+}  
+
+$db -> exec('SET CHARACTER SET utf8');
+
+$auth_token = $_COOKIE["auth-token"];
+
+$timestamp = 0;
+
+if ($auth_token) {
+
+  $sql = "
+    SELECT UNIX_TIMESTAMP(MAX(expires)) timestamp FROM tokens WHERE str=:s and expires>now()
+  ";
+  
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':s',$auth_token,PDO::PARAM_INT);
+  $q -> execute();
+
+  $res = [];
+
+  $row = $q -> fetch(PDO::FETCH_ASSOC);
+  $timestamp = $row['timestamp'];
+
+} 
+
+if ($timestamp) {
+  setcookie("auth-token",$auth_token,$timestamp);
+} else {
+  $auth_token = com_create_guid();
+  $timestamp = time()+86400*365;
+  setcookie("auth-token",$auth_token,$timestamp);
+  $sql = "
+    INSERT INTO tokens(str,description,expires) VALUES (:token,:descr,FROM_UNIXTIME(:expires)) 
+  ";
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':token',$auth_token,PDO::PARAM_STR);
+  $descr = $_SERVER["PHP_AUTH_USER"]." from ".$_SERVER["REMOTE_ADDR"]." at ".date('m/d/Y h:i:s a', time());
+  $q -> bindParam(':descr',$descr,PDO::PARAM_STR);
+  $q -> bindParam(':expires',$timestamp,PDO::PARAM_INT);
+  $db -> beginTransaction();
+  $q -> execute();
+  $db -> commit();
+  
+}
+
+print $auth_token;
+
+?>
diff --git a/web/cgi.php b/web/cgi.php
new file mode 100644 (file)
index 0000000..facdc70
--- /dev/null
@@ -0,0 +1,467 @@
+<?php
+
+$query=$_REQUEST['query'];
+
+$client_ip = $_SERVER["REMOTE_ADDR"];
+
+include('config_local.php');
+
+function startsWith($haystack, $needle)
+{
+     $length = strlen($needle);
+     return (substr($haystack, 0, $length) === $needle);
+}
+
+if (! ($db = new PDO("mysql:host=$mysql_host;port=$mysql_port;dbname=$mysql_schema",$mysql_user,$mysql_pwd,array( PDO::ATTR_PERSISTENT => false)))) {
+  die($err);
+}  
+
+$db -> exec('SET CHARACTER SET utf8');
+
+$auth_token = $_COOKIE["auth-token"];
+$auth = 0;
+
+if ($auth_token) {
+
+  $sql = "
+    SELECT COUNT(*) AS auth FROM tokens WHERE str=:s and expires>now()
+  ";
+  
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':s',$auth_token,PDO::PARAM_INT);
+  $q -> execute();
+
+  $res = [];
+
+  $row = $q -> fetch(PDO::FETCH_ASSOC);
+  $auth = $row['auth'];
+
+} 
+
+if ($auth || (strpos($client_ip, "192.168.") === 0) || (strpos($client_ip, "10.8.") === 0)) {
+  $filter = '';
+} else {
+  $filter = ' and s.is_public=1';
+}
+
+$hash = md5($_SERVER['QUERY_STRING'].":".$filter);
+$redis = new Redis();
+$redis->pconnect('127.0.0.1', 6379);
+$results = $redis->get('meteo-'.$hash);
+if ($results) {
+
+  $results = unserialize($results);
+  print(json_encode($results));
+  return;
+
+}
+
+function getYears($db) {
+
+  $sql = "
+    SELECT DISTINCT DATE_FORMAT(day,'%Y') y FROM sensors_ranges ORDER BY y DESC
+  ";
+  
+  $q = $db -> prepare( $sql );
+  $q -> execute();
+
+  $res = [];
+
+  while ($row = $q -> fetch(PDO::FETCH_ASSOC)) {
+    array_push($res, $row['y']);
+  }
+
+  return $res;  
+
+}
+
+function getMonths($db,$year) {
+
+  $y = intval($year);
+
+  $sql = "
+    SELECT DISTINCT DATE_FORMAT(day,'%m') m FROM sensors_ranges WHERE day>=STR_TO_DATE('".strval($y)."-01-01','%Y-%m-%d') and day<STR_TO_DATE('".strval($y+1)."-01-01','%Y-%m-%d') ORDER BY m DESC
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> execute();
+
+  $res = [];
+
+  while ($row = $q -> fetch(PDO::FETCH_ASSOC)) {
+    array_push($res, $row['m']);
+  }
+
+  return $res;  
+
+}
+
+function getDays($db,$year,$month) {
+
+  $y = intval($year);
+  $m = intval($month);
+
+  $sql = "
+    SELECT DISTINCT DATE_FORMAT(day,'%d') d FROM sensors_ranges WHERE day>=STR_TO_DATE('".strval($y)."-".strval($m)."-01','%Y-%m-%d') and DATE_ADD(STR_TO_DATE('".strval($y)."-".strval($m)."-01','%Y-%m-%d'), INTERVAL 1 MONTH) ORDER BY d DESC
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> execute();
+
+  $res = [];
+
+  while ($row = $q -> fetch(PDO::FETCH_ASSOC)) {
+    array_push($res, $row['d']);
+  }
+
+  return $res;  
+
+}
+
+function getSensors($db,$year,$month,$day) {
+
+  global $filter;
+
+  $y = intval($year);
+  $m = intval($month);
+  $d = intval($day);
+
+  $sql = "
+    SELECT DISTINCT 
+      CONCAT(s_id,'.',t.st_abbr,'.',p.st_name) id
+    FROM
+      sensors_ranges r, sensors s, sensor_types t, st_parameters p
+    WHERE 
+      r.sensor=s.id and r.parameter=p.id and s.st_id=t.id and p.st_id=t.id ".$filter."
+      and r.day=STR_TO_DATE('".strval($y)."-".strval($m)."-".strval($d)."','%Y-%m-%d')
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> execute();
+
+  $res = [];
+
+  while ($row = $q -> fetch(PDO::FETCH_ASSOC)) {
+    array_push($res, $row['id']);
+  }
+
+  return $res;  
+
+}
+
+function getCurrent($db,$id,$type,$param) {
+
+  global $filter;
+
+  $sql = "
+    SELECT s.id s_id,p.id p_id 
+    FROM sensors s,sensor_types t, st_parameters p
+    WHERE s.st_id=t.id and p.st_id=t.id and s_id=:id and t.st_abbr=:type and p.st_name=:param ".$filter."
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':id',$id,PDO::PARAM_STR);
+  $q -> bindParam(':type',$type,PDO::PARAM_STR);
+  $q -> bindParam(':param',$param,PDO::PARAM_STR);
+  $q -> execute();
+  $sensor = $q -> fetch(PDO::FETCH_ASSOC);
+
+  $sql = "
+    SELECT 
+      u.id stored_unit,du.id display_unit 
+    FROM 
+      sensors s,sensor_types t,st_parameters p,units u,units du
+    WHERE s.st_id=t.id and p.st_id=t.id and p.st_unit=u.id and u.unit_group=du.unit_group and du.is_default=1 and s.id=:id and p.id=:param
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':id',$sensor['s_id'],PDO::PARAM_INT);
+  $q -> bindParam(':param',$sensor['p_id'],PDO::PARAM_INT);
+  $q -> execute();
+  $units = $q -> fetch(PDO::FETCH_ASSOC);
+
+  $sql = "
+    SELECT 
+      CONCAT(subset.t,'5:00') t,UnitConv(avg(subset.y),:stored,:display) y
+    FROM (
+      SELECT 
+        substr(date_format(timestamp,'%Y-%m-%dT%H:%i'),1,15) t,value y
+      FROM 
+        sensor_values
+      WHERE 
+        sensor_id = :id and parameter_id=:param 
+        and timestamp>adddate(now(),-1)
+      ) subset 
+    GROUP BY subset.t
+    ORDER BY subset.t
+  ";
+  
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':id',$sensor['s_id'],PDO::PARAM_INT);
+  $q -> bindParam(':param',$sensor['p_id'],PDO::PARAM_INT);
+  $q -> bindParam(':stored',$units['stored_unit'],PDO::PARAM_INT);
+  $q -> bindParam(':display',$units['display_unit'],PDO::PARAM_INT);
+  $q -> execute();
+
+  return $q -> fetchAll(PDO::FETCH_ASSOC);
+
+}
+
+function getArchive($db,$year,$month,$day,$id,$type,$param) {
+
+  global $filter;
+
+  $y = intval($year);
+  $m = intval($month);
+  $d = intval($day);
+  
+  $date = strval($y).'-'.strval($m).'-'.strval($d);
+
+  $sql = "
+    SELECT s.id s_id,p.id p_id 
+    FROM sensors s,sensor_types t, st_parameters p
+    WHERE s.st_id=t.id and p.st_id=t.id and s_id=:id and t.st_abbr=:type and p.st_name=:param ".$filter."
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':id',$id,PDO::PARAM_STR);
+  $q -> bindParam(':type',$type,PDO::PARAM_STR);
+  $q -> bindParam(':param',$param,PDO::PARAM_STR);
+  $q -> execute();
+  $sensor = $q -> fetch(PDO::FETCH_ASSOC);
+
+  $sql = "
+    SELECT 
+      u.id stored_unit,du.id display_unit 
+    FROM 
+      sensors s,sensor_types t,st_parameters p,units u,units du
+    WHERE s.st_id=t.id and p.st_id=t.id and p.st_unit=u.id and u.unit_group=du.unit_group and du.is_default=1 and s.id=:id and p.id=:param
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':id',$sensor['s_id'],PDO::PARAM_INT);
+  $q -> bindParam(':param',$sensor['p_id'],PDO::PARAM_INT);
+  $q -> execute();
+  $units = $q -> fetch(PDO::FETCH_ASSOC);
+
+  $sql = "
+    SELECT 
+      CONCAT(subset.t,'5:00') t,UnitConv(avg(subset.y),:stored,:display) y
+    FROM (
+      SELECT 
+        substr(date_format(timestamp,'%Y-%m-%dT%H:%i'),1,15) t,value y
+      FROM 
+        sensor_values
+      WHERE 
+        sensor_id = :id and parameter_id=:param 
+        and timestamp>=STR_TO_DATE(:d,'%Y-%m-%d')
+        and timestamp<DATE_ADD(STR_TO_DATE(:d,'%Y-%m-%d'), interval 1 day)
+      ) subset 
+    GROUP BY subset.t
+    ORDER BY subset.t
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> bindParam(':id',$sensor['s_id'],PDO::PARAM_INT);
+  $q -> bindParam(':param',$sensor['p_id'],PDO::PARAM_INT);
+  $q -> bindParam(':d',$date,PDO::PARAM_STR);
+  $q -> bindParam(':stored',$units['stored_unit'],PDO::PARAM_INT);
+  $q -> bindParam(':display',$units['display_unit'],PDO::PARAM_INT);
+  $q -> execute();
+
+  return $q -> fetchAll(PDO::FETCH_ASSOC);
+
+}
+
+function getProps($db, $localNet) {
+
+  global $filter;
+  
+  $sql = "
+    SELECT 
+      CONCAT(s_id,'.',t.st_abbr,'.',p.st_name) sensor_id,
+      p.st_description name,
+      du.name_short unit,
+      du.prec prec,
+      pl.place_name,
+      p.st_line_color color
+    FROM 
+      sensors s, sensor_types t, st_parameters p,units u,units du,places pl
+    WHERE 
+      s.st_id=t.id and p.st_id=t.id and p.st_unit=u.id and u.unit_group=du.unit_group and du.is_default=1 and pl.idplaces=s.place_id ".$filter."
+  ";
+  
+  $q = $db -> prepare( $sql );
+  $q -> execute();
+  $reply = [ 
+    "names" => [], 
+    "colors" => [], 
+    "units" => [], 
+    "scale" => [], 
+    "places" => [],
+    "fonts" => [ 
+      "axes" => [ "color" => "black", "size" => 16, "style" => "normal" ], 
+      "legend" => [ "color" => "black", "size" => 16, "style" => "normal" 
+  ] ] ];
+
+  while ($row = $q -> fetch(PDO::FETCH_ASSOC)) {
+    $reply["names"][$row["sensor_id"]] = $row["name"];
+    $reply["colors"][$row["sensor_id"]] = $row["color"];
+    $reply["units"][$row["sensor_id"]] = $row["unit"];
+    $reply["places"][$row["sensor_id"]] = $row["place_name"];
+    $reply["scale"][$row["sensor_id"]] = [ 0 => 1.0, 1 => $row["prec"] ];
+  }
+  
+  return $reply;
+  
+}
+
+function getState($db, $localNet) {
+
+  global $filter;
+  
+  $sql = "
+    SELECT 
+      DISTINCT 
+        s.s_id as sensor_id,
+        st.st_abbr,
+        v.sensor as sensor_int_id,
+        pl.place_name s_description,
+        p.id as param_id,
+        p.st_name as param_name,
+        p.st_description,
+        s.place_id,
+        u.id stored_unit_id,
+        du.id unit_id
+    FROM 
+      sensors_ranges v,st_parameters p,sensors s,places pl,sensor_types st,units u,units du
+    WHERE 
+      v.timestamp>addtime(now(), -43200) 
+      and s.st_id=st.id
+      and v.sensor=s.id
+      and s.st_id=st.id
+      and v.parameter=p.id  
+      and s.st_id=p.st_id
+      and p.id>=0
+      and s.place_id=pl.idplaces
+      and p.st_unit=u.id 
+      and u.unit_group=du.unit_group
+      and du.is_default=1
+      ".$filter."
+    ORDER BY
+      s_description,sensor_id,param_id
+  ";
+
+  $q = $db -> prepare( $sql );
+  $q -> execute();
+
+  $reply = [];
+
+  while ($row = $q -> fetch(PDO::FETCH_ASSOC)) {
+
+    $sensor_id = $row['sensor_id'];
+    $st_id = $row['st_abbr'];
+    $sensor_int_id = $row['sensor_int_id'];
+    $param_id = $row['param_name'];
+    $param_int_id = $row['param_id'];
+    $unit_id = $row['unit_id'];
+    $stored_unit_id = $row['stored_unit_id'];
+    $place_description = $row['s_description'];
+
+    $sql_last_val = "
+      SELECT UnitConv(value,".$stored_unit_id.",".$unit_id.") as val,timestamp
+      FROM
+        sensor_values
+      WHERE 
+        sensor_id = ".$sensor_int_id." and parameter_id=".$param_int_id."
+      ORDER BY
+        timestamp DESC
+      LIMIT 1
+    ";
+
+    $qv = $db -> prepare( $sql_last_val );
+    $qv -> execute();
+
+    $v_row = $qv -> fetch(PDO::FETCH_ASSOC);
+    
+    $value = $v_row["val"];
+    $timestamp = $v_row["timestamp"];
+
+    if (! array_key_exists($place_description,$reply)) {
+      $reply[$place_description] = [];
+    }
+
+    if (! array_key_exists($sensor_id,$reply[$place_description])) {
+      $reply[$place_description][$sensor_id] = [];
+    }
+    
+    if (! array_key_exists($st_id,$reply[$place_description][$sensor_id])) {
+      $reply[$place_description][$sensor_id][$st_id] = [];
+    }
+
+    $reply[$place_description][$sensor_id][$st_id][$param_id] = $value;
+    $reply[$place_description][$sensor_id]['timestamp'] = $timestamp;
+
+  }
+
+  return $reply;
+  
+}
+
+$expire = 60;
+
+if ($query == 'props') {
+
+  $reply = getProps($db, $local_net);
+  $expire = 600;
+
+} elseif ($query == 'state') {
+
+  $reply = getState($db, $local_net);
+
+} elseif (startsWith($query,'get/')) {
+
+  $sensor = explode('/',substr($query,strlen('get/')));
+  $reply = getCurrent($db,$sensor[0],$sensor[1],$sensor[2]);
+
+} elseif ($query == 'years') {
+
+  $reply = getYears($db);
+  $expire = 3600;
+
+} elseif (startsWith($query,'months/')) {
+
+  $date = explode('/',substr($query,strlen('months/')));
+  $reply = getMonths($db,$date[0]);
+  $expire = 3600;
+
+} elseif (startsWith($query,'days/')) {
+
+  $date = explode('/',substr($query,strlen('days/')));
+  $reply = getDays($db,$date[0],$date[1]);
+  $expire = 3600;
+
+} elseif (startsWith($query,'sensors/')) {
+
+  $date = explode('/',substr($query,strlen('sensors/')));
+  $reply = getSensors($db,$date[0],$date[1],$date[2]);
+  $expire = 3600;
+
+} elseif (startsWith($query,'get-archive/')) {
+
+  $path = explode('/',substr($query,strlen('get-archive/')));
+  $reply = getArchive($db,$path[0],$path[1],$path[2],$path[3],$path[4],$path[5]);
+  $expire = 14400;
+
+}
+
+if ($reply) {
+
+  $redis->set('meteo-'.$hash, serialize($reply));
+  $redis->expire('meteo-'.$hash, $expire);
+  
+  print(json_encode($reply));
+
+}
+
+?>
index d6cce97947bfa30399bdc49702bf09383ba64cbf..a4614fdcbf16158f0ec2610676b457bbb72c19de 100644 (file)
@@ -1,14 +1,13 @@
 <?php
 
-  $mysql_host = 'host';
+  $mysql_host = 'dbhost';
   $mysql_schema = 'meteo';
   $mysql_user = 'meteo';
-  $mysql_pwd = 'somestrongpasswd';
+  $mysql_pwd = 'somestronpasword';
   $mysql_port = 3306;
 
   setlocale(LC_ALL,'ru_RU.UTF8');
 
-  $valid_ip_start = ip2long('192.168.1.0');
-  $valid_ip_end   = ip2long('192.168.1.255');
+  $site_header = 'METEO';
                 
-?>
\ No newline at end of file
+?>
diff --git a/web/config_local.php~ b/web/config_local.php~
new file mode 100644 (file)
index 0000000..1ca0c18
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+
+  $mysql_host = 'estia.rvb-home.lan';
+  $mysql_schema = 'meteo';
+  $mysql_user = 'meteo';
+  $mysql_pwd = 'snovadozhdi';
+  $mysql_port = 3306;
+
+  setlocale(LC_ALL,'ru_RU.UTF8');
+
+  $site_header = 'RVB.NAME';
+                
+?>
diff --git a/web/graph.html b/web/graph.html
new file mode 100644 (file)
index 0000000..f580ca4
--- /dev/null
@@ -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="/meteo/weather.svg">
+    <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/archive">Архивы</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 Ubuntu 18.04</footer>
+
+</body>
+</html>
diff --git a/web/graph.js b/web/graph.js
new file mode 100644 (file)
index 0000000..9ad8aa4
--- /dev/null
@@ -0,0 +1,143 @@
+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;
+
+  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["places"][sensor_path] + ' - ' + 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,60000);
+      return;
+    }
+    var graphData = JSON.parse(this.responseText);
+    drawGraph(graphData);
+    setTimeout(RefreshGraph,60000)
+  };
+  
+  req.open("GET", urlbase+"get/"+sensor_id+"/"+sensor+"/"+param, true);
+  req.withCredentials = 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.withCredentials = true;
+  req.send();
+
+}
+
+setTimeout(GetProperties,100)
diff --git a/web/index.html b/web/index.html
new file mode 100644 (file)
index 0000000..0fec812
--- /dev/null
@@ -0,0 +1,53 @@
+<?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="/meteo/weather.svg">
+    <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="/meteo/archive">Архивы метеонаблюдений</a>
+            <div id="auth" class="brand" style="margin-left: auto" onclick="GetAuth();">Авторизация</div>
+       </div>
+    </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 style="display: none;" id="divider-template">
+<div class="divider">$PLACE</div>
+</div>
+
+<div class="main">
+
+    <div id="maincontent">
+                       
+        <div class="container" id="meteo">
+
+        </div>
+    
+    </div>
+
+</div>
+
+<footer>Powered by Ubuntu 18.04</footer>
+
+</body>
+</html>
diff --git a/web/meteo.css b/web/meteo.css
new file mode 100644 (file)
index 0000000..2fcb18b
--- /dev/null
@@ -0,0 +1,220 @@
+* {
+    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: darkred;
+    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: 0.5rem;
+    padding: 0.5rem;
+}
+
+header > .fill > .container > .brand {
+    font-size: 1.2rem;
+    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 1rem 1rem 1rem;
+    padding-top: 1px;
+    height: 100%;
+}
+.section {
+    margin: 0.5rem;
+    padding: 1rem;
+    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: 0.5rem;
+    padding: 1rem;
+    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;
+}
+
+.divider {
+    margin: 0.5rem;
+    padding: 1rem;
+    border: 0;
+    font-weight: normal;
+    font-style: bold;
+    line-height: 1;
+    font-family: inherit;
+    font-size: 1.5rem;
+    float: left;
+    min-width: calc(100% - 2rem);
+    border-radius: 0;
+    color: #404040;
+    -webkit-overflow-scrolling: touch;
+}
+
+.large-section {
+    margin: 0.5rem;
+    padding: 1rem;
+    border: 0;
+    font-weight: normal;
+    font-style: normal;
+    line-height: 1;
+    font-family: inherit;
+    min-width: calc(100% - 2rem);
+    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: 0.5rem;
+    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;
+    position:   fixed;
+    left:   0px;
+    bottom: 0px;
+    width:  100%;
+}
+
+footer > a {
+    color: #aaa;
+    text-decoration: none;
+}
+    
+.reference {
+    padding: 1rem 1rem;
+    text-decoration: bold;
+    float: left;
+    font-size: 4rem;
+}
+
+.reference-unit {
+    padding: 0.5rem 1rem;
+    text-decoration: bold;
+    float: left;
+    font-size: 3rem;
+}
+    
+.section:hover {
+    color: white;
+    background: darkred;
+}
+    
+.reference-header {
+    padding: 0.5rem 0.2rem;
+    text-decoration: none;
+    font-size: 1.5rem;
+    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/web/meteo.js b/web/meteo.js
new file mode 100644 (file)
index 0000000..ee8947e
--- /dev/null
@@ -0,0 +1,130 @@
+urlbase="/meteo/cgi/"
+
+currentState=""
+
+function getCookie(name) {
+  var matches = document.cookie.match(new RegExp(
+    "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
+  ));
+  return matches ? decodeURIComponent(matches[1]) : undefined;
+}
+
+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 divider_template = document.getElementById("divider-template").innerHTML;
+      var value_color = document.getElementById("value").style.color;
+
+      var html = "";
+
+      for (var place in returnedData) {  
+        var divider = divider_template.replace(/\$PLACE/g,place);
+        html = html + divider;
+        for (var sensor_id in returnedData[place]) {
+          var timestamp = returnedData[place][sensor_id]["timestamp"];
+          for (var sensor in returnedData[place][sensor_id]) {
+            if (sensor != "timestamp") {
+              for (var param in returnedData[place][sensor_id][sensor]) {
+                sensor_path = sensor_id+"."+sensor+"."+param;
+                name = properties["names"][sensor_path];
+                if (! name.startsWith("-")) {
+                  value = returnedData[place][sensor_id][sensor][param];
+                  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,60000)
+    } else {
+      setTimeout(RefreshPageState,60000)
+    }    
+  };
+
+  req.open("GET", urlbase+"state", true);
+  req.withCredentials = 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.withCredentials = true;
+  req.send();
+
+}
+
+function GetAuth() {
+  var req = new XMLHttpRequest();
+
+  req.onreadystatechange = function () {
+    if (this.readyState != 4) return; 
+    if (this.status != 200) {
+      setTimeout(GetAuth,30000);
+      return;
+    }
+    location.reload(); 
+  };
+
+  req.open("GET", "auth", true);
+  req.withCredentials = true;
+  req.send();
+
+}
+
+function CheckCookie() {
+
+  if (getCookie("auth-token")) {
+  
+    authDiv = document.getElementById("auth");
+    authDiv.innerHTML = "";
+
+  }
+
+}
+
+setTimeout(GetProperties,100);
+setTimeout(CheckCookie,200);
index 60d21dbbc704bfedb1c8826a2b3c74ad0a779f7b..86286ef77d2a4c1f29a9a1f75f218976b09c8a0c 100644 (file)
@@ -1,28 +1,57 @@
 <?php
     require_once 'config_local.php';
 
-    $client_ip = ip2long($_SERVER["REMOTE_ADDR"]);
 
-    if (!client_ip || $client_ip > $valid_ip_end || $valid_ip_start > $client_ip) {
+    $client_ip = $_SERVER["REMOTE_ADDR"];
+
+    if ((strpos($client_ip, "192.168.") === 0) || (strpos($client_ip, "10.8.") === 0) || (strpos($client_ip, "2001:470:6f:9d5:") === 0)) {
+
+   
+      $local_net = True;
+            
+    } else {
+  
+      $local_net = False;
+  
+    }
+
+
+
+    if (! $local_net) { 
       header('HTTP/1.1 403 Forbidden');
       echo "IP not in allowed range";
       exit;
     }
 
+
     $stype = $_REQUEST['stype'];
     $sid   = $_REQUEST['sid'];
     $param = $_REQUEST['param'];
     $value = $_REQUEST['value'];
 
+    if (isset($_REQUEST['time'])) {
+    
+      $timestamp = "'".$_REQUEST['time']."'";
+    
+    } else {
+    
+      $timestamp = 'NULL';
+    
+    }
+
+    
+
     $connection = new mysqli($mysql_host, $mysql_user, $mysql_pwd, $mysql_schema, $mysql_port);
     if ($connection->connect_errno) {
         header('HTTP/1.1 500 Internal Server Error');
         exit;    
     }
         
-    $str = "CALL meteo.submit_value('".$stype."','".$sid."','".$param."',".$value.",NULL)";
+    $str = "CALL meteo.submit_value('".$stype."','".$sid."','".$param."',".$value.",".$timestamp.")";
     if (!$connection->query($str)) {
         header('HTTP/1.1 500 Internal Server Error');
+        echo "$str\n";
+        echo "Call Failed\n";
         exit;    
     } else {
         $connection->commit();
diff --git a/web/weather.svg b/web/weather.svg
new file mode 100644 (file)
index 0000000..dc49122
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"\r
+        viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">\r
+<style type="text/css">\r
+       .st0{fill:#4F5D73;}\r
+       .st1{opacity:0.2;}\r
+       .st2{fill:#231F20;}\r
+       .st3{fill:#E0995E;}\r
+       .st4{fill:#FFFFFF;}\r
+</style>\r
+<g id="Layer_1">\r
+       <g>\r
+               <circle class="st0" cx="32" cy="32" r="32"/>\r
+       </g>\r
+       <g class="st1">\r
+               <circle class="st2" cx="22" cy="24" r="10"/>\r
+       </g>\r
+       <g>\r
+               <circle class="st3" cx="22" cy="22" r="10"/>\r
+       </g>\r
+       <g class="st1">\r
+               <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\r
+                       h27.5c4.1,0,7.3-3.1,7.3-7S52.7,36,48.7,36z"/>\r
+       </g>\r
+       <g>\r
+               <g class="st1">\r
+                       <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\r
+                               C27.5,32,32,27.5,32,22z"/>\r
+               </g>\r
+       </g>\r
+       <g>\r
+               <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\r
+                       h27.5c4.1,0,7.3-3.1,7.3-7S52.7,34,48.7,34z"/>\r
+       </g>\r
+</g>\r
+<g id="Layer_2">\r
+</g>\r
+</svg>\r