add flask app files

This commit is contained in:
2EEEB 2020-04-08 14:44:10 +02:00
parent 2f4d309280
commit ecf6fa1457
Signed by: 2EEEB
GPG Key ID: 8F12BD6D9D435DB2
13 changed files with 830 additions and 0 deletions

22
webapp/README.md Normal file
View File

@ -0,0 +1,22 @@
#webapp
Flask web app for data visalization. Client side is built with [Chart.js](https://chartjs.org) and [justgage](https://github.com/toorshia/justgage) js libraries and jQuery.
The app exposes a simple api for querying the database and source sensor status available at /api/*function*.
The functions are:
-/api/getData
-Method: GET, POST
-Number of samples can be specified with argument samples=*desired number*, defaults to 120 i.e 2 hours.
-Returns json with the datasets and information about sensor status
-/api/heartbeat
-Method: GET
-Returns json with status info and last received message timestamp
-Method: POST
-Reserved for internal use in reporting sensor status. Requires a secret to be set.
The client side comprises of several parts:
-`templates/index.html` provides the basic html structure
-`static/styles/chartstyle.css` takes care of some basic styling and layout
-`static/main.js` script taking care of creating the plots and gauges, requesting data from server and updating the view every two minutes.

11
webapp/inenvmon_web.ini Normal file
View File

@ -0,0 +1,11 @@
[uwsgi]
module = wsgi
master = true
processes = 2
socket = inenvmon_web.sock
chmod-socket = 666
vacuum = true
die-on-term = true

193
webapp/inenvmon_web.py Normal file
View File

@ -0,0 +1,193 @@
from flask import Flask, render_template, make_response, jsonify, request
import mysql.connector as mariadb
import datetime
import time
import io
import json
import pandas as pd
from collections import OrderedDict
DB = ""
DB_USER = ""
DB_PASSWORD = ""
API_SECRET = ""
class Source():
def __init__(self):
self.status = "unknown"
self.last_msg = 0.0
source = Source()
app = Flask(__name__)
def query_db(sql):
dbconn = mariadb.connect(user=DB_USER, password=DB_PASSWORD, database=DB)
cursor = dbconn.cursor()
try:
cursor.execute(sql)
except mariadb.Error as e:
print(e)
data = cursor.fetchall()
dbconn.close()
return data
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/getData', methods = ['GET','POST'])
def process_data():
if request.method == 'GET':
if request.args.get('samples') is not None:
try:
samples = int(request.args.get('samples'))
if samples > 10800:
samples = 120
except Exception as excp:
print(excp)
samples = 120
else:
samples = 120
elif request.method == 'POST':
try:
samples = int(request.form.get('samples'))
except Exception as excp:
print(excp)
samples = 120
if samples > 10800:
samples = 120
sql = "SELECT * FROM envdata ORDER BY timestamp DESC LIMIT %s" % samples
dbdata = query_db(sql)
f = '%Y-%m-%d %H:%M:%S'
labels = []
temps = []
hums = []
press = []
concs = []
for i in range(len(dbdata)):
labels.append(dbdata[i][0])
temps.append(dbdata[i][1])
hums.append(dbdata[i][2])
press.append(dbdata[i][3])
concs.append(dbdata[i][4])
df = pd.DataFrame({'dt': labels, 'temp': temps, 'hum': hums, 'pres': press, 'conc': concs})
r = pd.date_range(start=df['dt'].min(), end=df['dt'].max(), freq='T')
# df = df.drop_duplicates('dt').set_index('dt').reindex(r, method='pad', tolerance='90s').reset_index()
df = df.set_index('dt').reindex(r, method='nearest', tolerance='90s').reset_index()
darry = df.to_numpy()
start = len(darry)
print("darry %s" % str(len(darry)))
print("samples %s" % str(samples))
print(start)
print(start-samples)
print(samples-start)
if len(darry) != samples:
if samples > len(darry):
labels=darry[samples-start:start:1,0]
temps=darry[samples-start:start:1,1]
hums=darry[samples-start:start:1,2]
press=darry[samples-start:start:1,3]
concs=darry[samples-start:start:1,4]
else:
labels=darry[start-samples:start:1,0]
temps=darry[start-samples:start:1,1]
hums=darry[start-samples:start:1,2]
press=darry[start-samples:start:1,3]
concs=darry[start-samples:start:1,4]
elif len(darry) == 1:
labels=darry[0,0]
temps=darry[0,1]
hums=darry[0,2]
press=darry[0,3]
concs=darry[0,4]
else:
labels=darry[0:start:1,0]
temps=darry[0:start:1,1]
hums=darry[0:start:1,2]
press=darry[0:start:1,3]
concs=darry[0:start:1,4]
tempdict = OrderedDict()
humdict = OrderedDict()
presdict = OrderedDict()
co2dict = OrderedDict()
if len(darry) == 1:
tempdict[labels.strftime(f)] = str(temps)
humdict[labels.strftime(f)] = str(hums)
presdict[labels.strftime(f)] = str(press)
co2dict[labels.strftime(f)] = str(concs)
else:
for i in range(len(labels)):
tempdict[labels[i].strftime(f)] = str(temps[i])
humdict[labels[i].strftime(f)] = str(hums[i])
presdict[labels[i].strftime(f)] = str(press[i])
co2dict[labels[i].strftime(f)] = str(concs[i])
if (time.time() - source.last_msg) > 180:
status = "dead"
else:
status = "alive"
respdict = OrderedDict()
datas = ["status", "temp", "hum", "pres", "co2"]
dicts = [status, tempdict, humdict, presdict, co2dict]
for i in range(len(datas)):
respdict[datas[i]] = dicts[i]
response = app.response_class(
response=json.dumps(respdict),
status=200,
mimetype='application/json'
)
return response
@app.route('/api/heartbeat', methods = ['GET','POST'])
def heartbeat():
global source
if request.method == 'POST':
key = request.form.get('key')
if key == API_SECRET:
if request.form.get('message') is not None:
source.last_msg = float(request.form.get('message'))
else:
source.last_msg = "nodata"
source.status = "unknown"
post_response = app.response_class(
status=200)
return post_response
else:
err_response = app.response_class(
status=401)
return err_response
if request.method == 'GET':
if (time.time() - source.last_msg) > 180:
source.status = "dead"
else:
source.status = "alive"
resp_data = {'status': source.status, 'last_msg': source.last_msg}
get_response=app.response_class(
response=json.dumps(resp_data),
status=200,
mimetype='application/json'
)
return get_response
if __name__ == "__main__":
app.run()

7
webapp/static/Chart.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
webapp/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

2
webapp/static/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
webapp/static/justgage.min.js vendored Normal file

File diff suppressed because one or more lines are too long

443
webapp/static/main.js Normal file
View File

@ -0,0 +1,443 @@
var temp_gauge = new JustGage({
id: 'temp_gauge',
value: 0,
min: 10,
max: 50,
decimals: 2,
width: 222,
height: 150,
symbol: ' °C',
showInnerShadow: true,
levelColors: ['#3300ff', '#fff200', '#ff0008'],
customSectors: {
percents: false,
ranges: [{
color : "#33ff00",
lo : 20,
hi : 25.5
}]
}
});
var hum_gauge = new JustGage({
id: "hum_gauge",
value: 0,
min: 0,
max: 100,
decimals: 2,
width: 222,
height: 150,
symbol: ' %',
showInnerShadow: true,
customSectors: {
percents: false,
ranges: [{
color : "#33ff00",
lo : 30,
hi : 60
}]
}
});
var pres_gauge = new JustGage({
id: "pres_gauge",
value: 0,
min: 900,
max: 1050,
decimals: 0,
width: 222,
height: 150,
symbol: ' hPa',
showInnerShadow: true,
noGradient: true,
customSectors: {
percents: true,
ranges: [{
color : "#2200ff",
lo : 0,
hi : 100
}]
}
});
var co2_gauge = new JustGage({
id: "co2_gauge",
value: 0,
min: 300,
max: 5000,
decimals: 0,
width: 222,
height: 150,
symbol: ' ppm',
showInnerShadow: true,
levelColors: ['#33ff00', '#ffff00', '#ff8800', '#ff0008']
});
function updateGauge(dbdata) {
for(var key in dbdata.temp){};
temp_gauge.refresh(dbdata.temp[key]);
hum_gauge.refresh(dbdata.hum[key]);
pres_gauge.refresh(dbdata.pres[key]);
co2_gauge.refresh(dbdata.co2[key]);
};
var reqform = $('#sample_select_form');
reqform.submit(function (f) {
f.preventDefault();
clearChartData();
getServerDataAndUpdate($('input[name=samples]').val());
});
setInterval(function fx() {
var statusData = $.ajax ({
type: 'GET',
url: '/api/heartbeat',
data: {},
dataType: 'json',
success: console.log("requested heartbeat data"),
}).done(function (retdata) {
if ( typeof fx.statusWas === 'undefined' ) {
fx.statusWas = "dead";
};
if (retdata.status == "alive" && fx.statusWas == "alive") {
getServerDataAndUpdate(1);
trimChartData();
fx.statusWas = "alive";
}
else if (retdata.status == "alive" && fx.statusWas == "dead") {
$('#footer_text').text("Live update enabled. (Data might be delayed up to a minute)");
clearChartData();
getServerDataAndUpdate($('input[name=samples]').val());
fx.statusWas = "alive";
}
else if (retdata.status == "dead" && fx.statusWas == "alive") {
$('#footer_text').text("Source not online, live update disabled. Showing latest available data.");
fx.statusWas = "dead";
}
else if (retdata.status == "dead" && fx.statusWas == "dead") {
fx.statusWas == "dead";
}
else {
location.reload(forceGet);
}
});
}, 90000);
$('#refresh').click(function() {
clearChartData();
getServerDataAndUpdate($('input[name=samples]').val());
});
$(document).ready(function() {
var statusData = $.ajax ({
type: 'GET',
url: '/api/heartbeat',
data: {},
dataType: 'json',
success: console.log("requested heartbeat data"),
}).done(function (retdata) {
if (retdata.status == "alive") {
$('#footer_text').text("Live update enabled. (Data might be delayed up to a minute)");
}
else if (retdata.status == "dead") {
$('#footer_text').text("Source not online, live update disabled. Showing latest available data.");
}
});
getServerDataAndUpdate($('input[name=samples]').val());
});
Chart.defaults.global.responsive = false;
var nan=NaN;
var tempChartData = {
labels : [],
datasets : [{
label: 'Temperature',
fill: true,
lineTension: 0.1,
backgroundColor: "rgba(75,192,192,0.4)",
borderColor: "rgba(75,192,192,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(75,192,192,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(75,192,192,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
showLine: true,
spanGaps: false,
data : []
}]
};
var tempChartSettings = {
type : 'line',
data : tempChartData,
options : {
legend: {
display: false
},
scales : {
xAxes : [{
distribution: 'series',
ticks: {
autoSkip: true,
maxTicksLimit: 20,
},
type: 'time',
source: 'auto'
}]
}
}
};
var humChartData = {
labels : [],
datasets : [{
label: 'Humidity',
fill: true,
lineTension: 0.1,
backgroundColor: "rgba(136,77,255,0.4)",
borderColor: "rgba(136,77,255,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(136,77,255,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(136,77,255,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
data : [],
spanGaps: false,
}]
};
var humChartSettings = {
type : 'line',
showLines: false,
data : humChartData,
options : {
legend: {
display: false
},
scales : {
xAxes : [{
distribution: 'series',
ticks: {
autoSkip: true,
maxTicksLimit: 20,
},
type: 'time',
source: 'auto'
}]
}
}
};
var co2ChartData = {
labels : [],
datasets : [{
label: 'CO2 concentration',
fill: true,
lineTension: 0.1,
backgroundColor: "rgba(0,128,43,0.4)",
borderColor: "rgba(0,128,43,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(0,128,43,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(0,128,43,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
data : [],
spanGaps: false,
}]
};
var co2ChartSettings = {
type : 'line',
showLines: false,
data : co2ChartData,
options : {
legend: {
display: false
},
scales : {
xAxes : [{
distribution: 'series',
ticks: {
autoSkip: true,
maxTicksLimit: 20,
},
type: 'time',
source: 'auto'
}],
yAxes: [{
ticks: {
min: 300,
}
}]
}
}
};
var multiChartData = {
labels : [],
datasets : [{
label: 'Temperature',
fill: true,
lineTension: 0.1,
backgroundColor: "rgba(75,192,192,0.4)",
borderColor: "rgba(75,192,192,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(75,192,192,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(75,192,192,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
data : [],
spanGaps: false,
}, {
label: 'Humidity',
fill: true,
lineTension: 0.1,
backgroundColor: "rgba(136,77,255,0.4)",
borderColor: "rgba(136,77,255,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(136,77,255,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(136,77,255,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
data : [],
spanGaps: false,
}]
};
var multiChartSettings = {
type : 'line',
showLines: false,
data : multiChartData,
options : {
scales : {
xAxes : [{
distribution: 'series',
ticks: {
autoSkip: true,
maxTicksLimit: 20,
},
type: 'time',
source: 'auto'
}],
yAxes : [{
ticks: {
min: 10
}
}]
}
}
};
var ctx = document.getElementById('tempchart').getContext("2d");
var tempChart = new Chart(ctx,tempChartSettings);
var ctx2 = document.getElementById('humchart').getContext("2d");
var humChart = new Chart(ctx2,humChartSettings);
var ctx3 = document.getElementById('co2chart').getContext("2d");
var co2Chart = new Chart(ctx3,co2ChartSettings);
var ctx4 = document.getElementById('multichart').getContext("2d");
var multiChart = new Chart(ctx4,multiChartSettings);
function getServerDataAndUpdate(samples) {
var jsonData = $.ajax({
type: 'POST',
url: '/api/getData',
data: {'samples' : samples },
dataType: 'json',
success: console.log("requested db data"),
}).done(function (dbdata) {
updateChart(dbdata);
updateGauge(dbdata);
});
};
function updateChart(dbdata) {
$.each(dbdata['temp'], function(key, value) {
tempChart.data.labels.push(key);
humChart.data.labels.push(key);
co2Chart.data.labels.push(key);
multiChart.data.labels.push(key);
tempChart.data.datasets[0].data.push(value);
multiChart.data.datasets[0].data.push(value);
});
$.each(dbdata['hum'], function(key, value){
humChart.data.datasets[0].data.push(value);
multiChart.data.datasets[1].data.push(value);
});
$.each(dbdata['co2'], function(key, value){
co2Chart.data.datasets[0].data.push(value);
});
tempChart.update();
humChart.update();
co2Chart.update();
multiChart.update();
};
function clearChartData() {
tempChart.data.datasets[0].data = [];
tempChart.data.labels = [];
humChart.data.datasets[0].data = [];
humChart.data.labels = [];
co2Chart.data.datasets[0].data = [];
co2Chart.data.labels = [];
multiChart.data.datasets[0].data = [];
multiChart.data.datasets[1].data = [];
multiChart.data.labels = [];
};
function trimChartData() {
tempChart.data.datasets[0].data.shift();
tempChart.data.labels.shift();
humChart.data.datasets[0].data.shift();
humChart.data.labels.shift();
co2Chart.data.datasets[0].data.shift();
co2Chart.data.labels.shift();
multiChart.data.datasets[0].data.shift();
multiChart.data.datasets[1].data.shift();
multiChart.data.labels.shift();
}

12
webapp/static/raphael.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
webapp/static/styles/Chart.min.css vendored Normal file
View File

@ -0,0 +1 @@
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}

View File

@ -0,0 +1,76 @@
#header_bar {
}
#footer_bar {
padding-bottom: 5px;
padding-left: 5px;
}
.gauge_container {
text-align: center;
display: block;
margin: auto;
margin-top: 15px;
}
.gauge {
display: inline-block;
width: 240px;
height: 160px;
margin-right: auto;
}
.gauge_desc {
margin: 0;
margin-top: -30px;
margin-bottom: 20px;
}
.graph_name {
margin-top: 5px;
}
#sample_select {
margin-top: 40px;
margin-left: 20px;
}
#uberschrift {
margin: 0;
padding: 2px;
margin-left: 20px;
margin-top: 10px;
}
#content_wrapper {
display: flex;
flex-flow: wrap;
margin-left: 5px;
margin-right: 5px;
}
#content_left {
flex: 0 0 20%;
justify-content: center;
}
#content_center {
flex: 0 0 33%;
justify-content: center;
}
#content_right {
flex: 0 0 33%;
justify-content: center;
}
body {
padding: 0;
margin: 0;
font-family: 'Noto Sans', Noto, sans-serif;
}
svg {
margin-left: auto;
margin-right: auto;
}

View File

@ -0,0 +1,57 @@
<!doctype html>
<html>
<head>
<title>IAQ dash</title>
<link rel='shortcut icon' type='image/ico' href='static/favicon.ico'>
<link rel='stylesheet' href='static/styles/chartstyle.css'>
<link rel='stylesheet' href='static/styles/Chart.min.css'>
<script type='text/javascript' src='static/Chart.bundle.min.js'></script>
<script type='text/javascript' src='static/jquery.min.js'></script>
<script type='text/javascript' src='static/raphael.min.js'></script>
<script type='text/javascript' src='static/justgage.min.js'></script>
</head>
<body>
<div id='header_bar'>
<h1 id='uberschrift'>Indoor enviro data</h1>
</div>
<div id='content_wrapper' class='column'>
<div id='content_left' class='column'>
<div class='gauge_container'>
<h3>Most recent values</h3>
<div id='temp_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>Temperature</h5>
<div id='hum_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>Relative humidity</h5>
<div id='pres_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>Pressure</h5>
<div id='co2_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>CO2 concentration</h5>
</div>
<div id='sample_select'>
<form id='sample_select_form' action='/'>
<p>Show last <input type='number' min=1 max=10800 name='samples' placeholder='120' size=8> minutes.
<input type='submit' value='Select'/></p>
</form>
<p><input type='button' id='refresh' value='Refresh all'/></p>
</div>
</div>
<div id='content_center' class='column'>
<h3 class='graph_name'>Temperature</h3>
<canvas id='tempchart' width='600' height='380' style='display: block;'></canvas>
<h3 class='graph_name'>Humidity</h3>
<canvas id='humchart' width='600' height='380' style='display: block;'></canvas>
</div>
<div id='content_right' class='column'>
<h3 class='graph_name'>Combined</h3>
<canvas id='multichart' width='600' height='380' style='display: block'></canvas>
<h3 class='graph_name'>CO2 concentration</h3>
<canvas id='co2chart' width='600' height='380' style='display: block;'></canvas>
</div>
</div>
<div id='footer_bar'>
<p id='footer_text'>Source not online, live update disabled. Showing latest available data.</p>
</div>
<script type='text/javascript' src='static/main.js'></script>
</body>
</html>

4
webapp/wsgi.py Normal file
View File

@ -0,0 +1,4 @@
from inenvmon_web import app as application
if __name__ == "__main__":
application.run()