--- /dev/null
+<?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>
--- /dev/null
+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);
--- /dev/null
+<?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;
+
+?>
--- /dev/null
+<?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));
+
+}
+
+?>
<?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
+?>
--- /dev/null
+<?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';
+
+?>
--- /dev/null
+<?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>
--- /dev/null
+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)
--- /dev/null
+<?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>
--- /dev/null
+* {
+ 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;
+}
--- /dev/null
+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);
<?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();
--- /dev/null
+<?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