major cleanup, added comments

This commit is contained in:
2EEEB 2020-06-22 17:38:07 +02:00
parent 743aa2b383
commit d30d6096d1
Signed by: 2EEEB
GPG Key ID: 8F12BD6D9D435DB2
17 changed files with 492 additions and 342 deletions

View File

@ -7,6 +7,7 @@ Some components are under a different license.
Files for a flask webapp to visualise data from database. Files for a flask webapp to visualise data from database.
### scripts ### inenvmon_collector.py
Helper scripts. A helper script to listen to mqtt topic and write relevant data into the database.
Also takes care of timestamping and reporting source status to web app.

80
inenvmon_collector.py Executable file
View File

@ -0,0 +1,80 @@
#!/usr/bin/pyhton -u
import sqlite3 as sql3db
import paho.mqtt.client as mqtt
import time
import datetime
import requests
MQTT_Broker = "127.0.0.1" # usually on the same machine, if not, adjust accordingly
MQTT_Port = 9883 # mqtt port for internal use, can be whatever, determined by the broker config
Keep_Alive_Interval = 60 # keep alive interval for broker connection
MQTT_Topic = "topic" # mqtt topic, set to the same value as the monitoring device
MQTT_Auth = {'username':"", 'password':""} # mqtt authentication, determined by the broker config
DB = "./inenvmon_data.db" # path to database file
DB_TABLE = "envdata0" # name of the database table (mqtt topic is fitting here)
API_SECRET = "secret" # set to same value as in the api
def write_db(sql): # function for writing to the database
dbconn = sql3db.connect(DB)
cursor = dbconn.cursor()
try:
cursor.execute(sql)
except sql3db.Error as e:
print(e)
return False
dbconn.commit()
dbconn.close()
return True
def send_heartbeat(): # function for heartbeat reporting, adjust according to server setup
return requests.post("http://localhost:80/api/heartbeat", data={'message':time.time(),'key': API_SECRET})
# on connection callback, used to subscirbe to the set up topic after connecting to the broker
def on_connect(client, userdata, flags, rc):
print("connected: " + str(rc))
mqttc.subscribe(MQTT_Topic, 0)
print("subscribed!")
# on message callback, executes every time a message is published to the topic
def on_message(mosq, obj, msg):
payload = msg.payload # save the message payload
try:
dec_msg = payload.decode("unicode_escape").split(",") # decode the payload and split at commas
# out of range value check, replaces erroneous values with no value
# included as a precaution, the sensors could malfunction and the communication between the devices is not always perfect
# the ranges are set to what can be reasonably expected in an interior environment, can be tweaked if desired
if float(dec_msg[0]) > 45 or float(dec_msg[0] < 0): # temperature
dec_msg[0] = "NULL"
if float(dec_msg[1]) < 0 or float(dec_msg[1] > 100): # relative humidity
dec_msg[1] = "NULL"
if int(dec_msg[2]) < 900 or int(dec_msg[2]) > 1100: # pressure
dec_msg[2] = "NULL"
if dec_msg[3] == "-1": # co2, value of -1 indicates a measurement error
dec_msg[3] = "NULL"
except Exception as ex:
print("Excp: %s" % ex) # print exceptions
f = '%Y-%m-%d %H:%M:%S' # timestamp format string
# create table
sql0 = "CREATE TABLE IF NOT EXISTS %s \
(\"timestamp\" datetime DEFAULT NULL, \
\"temp\" decimal(5 , 2) DEFAULT NULL, \
\"hum\" decimal(5 , 2) DEFAULT NULL, \
\"pres\" int(4) DEFAULT NULL, \
\"co2\" int(5) DEFAULT NULL)" % DB_TABLE
write_db(sql0)
# formulate sql string with values
sql = "INSERT INTO %s (timestamp, temp, hum, pres, co2) VALUES (\"%s\", %s, %s, %s, %s)" % (DB_TABLE, datetime.datetime.now().strftime(f), procdm[0], procdm[1], procdm[2], procdm[3])
write_db(sql) # execute the sql string
send_heartbeat() # send heartbeat
mqttc = mqtt.Client() # instantiate the client class
mqttc.on_message = on_message
mqttc.on_connect = on_connect # set up callbacks
mqttc.username_pw_set(MQTT_Auth['username'], MQTT_Auth['password']) # set mqtt credentials
mqttc.connect(MQTT_Broker, int(MQTT_Port), int(Keep_Alive_Interval)) # connect to the broker using provided settings
mqttc.loop_forever() # keep the connection running as long as the script is running

View File

@ -1,6 +0,0 @@
# scripts
### inenvmon_collector.py
A helper script to listen to mqtt topic and write relevant data into the database.
Also takes care of timestamping and reporting source status to web app.

View File

@ -1,70 +0,0 @@
#!/usr/bin/pyhton -u
import mysql.connector as mariadb
import paho.mqtt.client as mqtt
import time
import datetime
import requests
#import ssl
MQTT_Broker = "127.0.0.1"
MQTT_Port =
Keep_Alive_Interval = 60
MQTT_Topic = ""
MQTT_Auth = {'username':"", 'password':""}
DB = ""
DB_USER = ""
DB_PASSWORD = ""
API_SECRET = ""
def write_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)
return False
dbconn.commit()
dbconn.close()
return True
def send_heartbeat():
return requests.post("http://localhost:80/api/heartbeat", data={'message':time.time(),'key': API_SECRET})
def on_connect(client, userdata, flags, rc):
print("connected: "+str(rc))
mqttc.subscribe(MQTT_Topic, 0)
print("subscribed!")
def on_message(mosq, obj, msg):
try:
procdm = msg.payload.decode("unicode_escape").split(",")
if procdm[3] == -1:
procdm[3] = None;
except Exception as ex:
print("Fug: %s" % ex)
f = '%Y-%m-%d %H:%M:%S'
sql = "INSERT INTO envdata (timestamp, temp, hum, pres, co2) VALUES (%s, %s, %s, %s, %s)" % (str("\"%s\"") % datetime.datetime.now().strftime(f), str("\"%s\"") % procdm[0], str("\"%s\"") % procdm[1], str("\"%s\"") % procdm[2], str("\"%s\"") % procdm[3])
write_db(sql)
send_heartbeat()
def on_subscribed(mosq, obj, mid, granted_qos):
pass
mqttc = mqtt.Client()
mqttc.on_message = on_message
mqttc.on_connect = on_connect
mqttc.on_subscribed = on_subscribed
#mqttc.tls_set(ca_certs="/etc/ssl/cert.pem", tls_version=ssl.PROTOCOL_TLSv1_2)
mqttc.username_pw_set(MQTT_Auth['username'], MQTT_Auth['password'])
mqttc.connect(MQTT_Broker, int(MQTT_Port), int(Keep_Alive_Interval))
mqttc.loop_forever()

103
webapp/inenvmon_web.py Normal file → Executable file
View File

@ -1,5 +1,5 @@
from flask import Flask, render_template, make_response, jsonify, request from flask import Flask, render_template, make_response, jsonify, request
import mysql.connector as mariadb import sqlite3 as sql3db
import datetime import datetime
import time import time
import io import io
@ -7,27 +7,26 @@ import json
import pandas as pd import pandas as pd
from collections import OrderedDict from collections import OrderedDict
DB = "" DB = "../inenvmon_data.db" # path to the database file as set up in inenvmon_collector.py
DB_USER = "" DB_TABLE = "envdata0" # name of database table, as set up in inenvmon_collector.py
DB_PASSWORD = "" API_SECRET = "secret" # api secret for the heartbeat method, set the same in inenvmon_collector.py
API_SECRET = ""
class Source(): class Source():
def __init__(self): def __init__(self):
self.status = "unknown" self.status = "unknown" #used to persistently store status
self.last_msg = 0.0 self.last_msg = 0.0
source = Source() source = Source()
app = Flask(__name__) app = Flask(__name__)
def query_db(sql): def query_db(sql): #function for executing database queries
dbconn = mariadb.connect(user=DB_USER, password=DB_PASSWORD, database=DB) dbconn = sql3db.connect(DB)
cursor = dbconn.cursor() cursor = dbconn.cursor()
try: try:
cursor.execute(sql) cursor.execute(sql)
except mariadb.Error as e: except sql3db.Error as e:
print(e) print(e) # output errors
data = cursor.fetchall() data = cursor.fetchall()
dbconn.close() dbconn.close()
@ -36,63 +35,67 @@ def query_db(sql):
@app.route('/') @app.route('/')
def index(): def index():
return render_template('index.html') # no actual template rendering is going on, this is used for convenience since the api and web app are on the same domain
# the html is only a skeleton, all functionality is in the js
return render_template('index.html')
@app.route('/api/getData', methods = ['GET','POST']) @app.route('/api/getdata', methods = ['GET','POST'])
def process_data(): def process_data():
if request.method == 'GET': if request.method == 'GET':
if request.args.get('samples') is not None: if request.args.get('samples') is not None:
try: try:
samples = int(request.args.get('samples')) samples = int(request.args.get('samples'))
if samples > 10800: if samples > 10800: # limit the maximum amount of data points, working with values above this threshold was observed unstable
samples = 120 samples = 120
except Exception as excp: except Exception as excp:
print(excp) print(excp) # output errors
samples = 120 samples = 120
else: else:
samples = 120 samples = 120 # silently default to 120 in case of any problems
# same as above but for POST requests this time
elif request.method == 'POST': elif request.method == 'POST':
try: try:
samples = int(request.form.get('samples')) samples = int(request.form.get('samples'))
except Exception as excp: except Exception as excp:
print(excp) print(excp) # output errors
samples = 120 samples = 120
if samples > 10800: if samples > 10800:
samples = 120 samples = 120
sql = "SELECT * FROM envdata ORDER BY timestamp DESC LIMIT %s" % samples sql = "SELECT * FROM %s ORDER BY timestamp DESC LIMIT %s" % (DB_TABLE, samples) # sql selection string
dbdata = query_db(sql) dbdata = query_db(sql) # store data returned from the db
f = '%Y-%m-%d %H:%M:%S' f = '%Y-%m-%d %H:%M:%S' # string format
# initialize arrays
labels = [] labels = []
temps = [] temps = []
hums = [] hums = []
press = [] press = []
concs = [] concs = []
# split data returned from db into arrays
for i in range(len(dbdata)): for i in range(len(dbdata)):
labels.append(dbdata[i][0]) labels.append(datetime.datetime.strptime(dbdata[i][0], f))
temps.append(dbdata[i][1]) temps.append(dbdata[i][1])
hums.append(dbdata[i][2]) hums.append(dbdata[i][2])
press.append(dbdata[i][3]) press.append(dbdata[i][3])
concs.append(dbdata[i][4]) concs.append(dbdata[i][4])
# pandas dataset processing
# detects gaps in the dataset timestamps and fills them with no values
# gaps of one datapoint are bridged with an average value
df = pd.DataFrame({'dt': labels, 'temp': temps, 'hum': hums, 'pres': press, 'conc': concs}) 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') 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() df = df.set_index('dt').reindex(r, method='nearest', tolerance='90s').reset_index()
# pandas returns weird stuff, converted here to a numpy array
# numpy because that's what available without additional work
darry = df.to_numpy() darry = df.to_numpy()
start = len(darry) start = len(darry) # save the length for further slicing
print("darry %s" % str(len(darry))) # array slicing and ordering
print("samples %s" % str(samples)) # because js graphing function expects the dataset with inverted timebase
print(start) # different procedures for different scenarios
print(start-samples) # in case of missing values
print(samples-start)
if len(darry) != samples: if len(darry) != samples:
if samples > len(darry): if samples > len(darry):
labels=darry[samples-start:start:1,0] labels=darry[samples-start:start:1,0]
@ -106,12 +109,14 @@ def process_data():
hums=darry[start-samples:start:1,2] hums=darry[start-samples:start:1,2]
press=darry[start-samples:start:1,3] press=darry[start-samples:start:1,3]
concs=darry[start-samples:start:1,4] concs=darry[start-samples:start:1,4]
# in case of a single value (when updating the plot)
elif len(darry) == 1: elif len(darry) == 1:
labels=darry[0,0] labels=darry[0,0]
temps=darry[0,1] temps=darry[0,1]
hums=darry[0,2] hums=darry[0,2]
press=darry[0,3] press=darry[0,3]
concs=darry[0,4] concs=darry[0,4]
# in case of uninterrupted dataset
else: else:
labels=darry[0:start:1,0] labels=darry[0:start:1,0]
temps=darry[0:start:1,1] temps=darry[0:start:1,1]
@ -119,16 +124,18 @@ def process_data():
press=darry[0:start:1,3] press=darry[0:start:1,3]
concs=darry[0:start:1,4] concs=darry[0:start:1,4]
# order of the values is important therefore ordered dict
tempdict = OrderedDict() tempdict = OrderedDict()
humdict = OrderedDict() humdict = OrderedDict()
presdict = OrderedDict() presdict = OrderedDict()
co2dict = OrderedDict() co2dict = OrderedDict()
# fill the dict straight away with the single value
if len(darry) == 1: if len(darry) == 1:
tempdict[labels.strftime(f)] = str(temps) tempdict[labels.strftime(f)] = str(temps)
humdict[labels.strftime(f)] = str(hums) humdict[labels.strftime(f)] = str(hums)
presdict[labels.strftime(f)] = str(press) presdict[labels.strftime(f)] = str(press)
co2dict[labels.strftime(f)] = str(concs) co2dict[labels.strftime(f)] = str(concs)
# iterate over the arrays and fill the dicts
else: else:
for i in range(len(labels)): for i in range(len(labels)):
tempdict[labels[i].strftime(f)] = str(temps[i]) tempdict[labels[i].strftime(f)] = str(temps[i])
@ -136,18 +143,20 @@ def process_data():
presdict[labels[i].strftime(f)] = str(press[i]) presdict[labels[i].strftime(f)] = str(press[i])
co2dict[labels[i].strftime(f)] = str(concs[i]) co2dict[labels[i].strftime(f)] = str(concs[i])
# check status
if (time.time() - source.last_msg) > 180: if (time.time() - source.last_msg) > 180:
status = "dead" status = "dead"
else: else:
status = "alive" status = "alive"
# final response dict
respdict = OrderedDict() respdict = OrderedDict()
# ready data and corresponding key for json
datas = ["status", "temp", "hum", "pres", "co2"] datas = ["status", "temp", "hum", "pres", "co2"]
dicts = [status, tempdict, humdict, presdict, co2dict] dicts = [status, tempdict, humdict, presdict, co2dict]
# iterate over the data and fill the response dict
for i in range(len(datas)): for i in range(len(datas)):
respdict[datas[i]] = dicts[i] respdict[datas[i]] = dicts[i]
# formulate json response from the prepared dict
response = app.response_class( response = app.response_class(
response=json.dumps(respdict), response=json.dumps(respdict),
status=200, status=200,
@ -156,31 +165,33 @@ def process_data():
return response return response
@app.route('/api/heartbeat', methods = ['GET','POST']) @app.route('/api/heartbeat', methods = ['GET','POST'])
def heartbeat(): def heartbeat(): # device status reporting
global source global source
if request.method == 'POST': if request.method == 'POST':
key = request.form.get('key') key = request.form.get('key') # check the secret
if key == API_SECRET: if key == API_SECRET:
if request.form.get('message') is not None: if request.form.get('message') is not None:
source.last_msg = float(request.form.get('message')) source.last_msg = float(request.form.get('message')) # save the status if timestamp present
else: else:
source.last_msg = "nodata" source.last_msg = "nodata"
source.status = "unknown" source.status = "unknown" #unknown status if no timestamp
#respond with 200 ok
post_response = app.response_class( post_response = app.response_class(
status=200) status=200)
return post_response return post_response
else: else:
# respond with 401 unauthorized if secret is incorrect
err_response = app.response_class( err_response = app.response_class(
status=401) status=401)
return err_response return err_response
if request.method == 'GET': if request.method == 'GET':
# if get request calculate time delta
if (time.time() - source.last_msg) > 180: if (time.time() - source.last_msg) > 180:
source.status = "dead" source.status = "dead"
else: else:
source.status = "alive" source.status = "alive"
# formulate response json
resp_data = {'status': source.status, 'last_msg': source.last_msg} resp_data = {'status': source.status, 'last_msg': source.last_msg}
get_response=app.response_class( get_response=app.response_class(
response=json.dumps(resp_data), response=json.dumps(resp_data),

File diff suppressed because one or more lines are too long

0
webapp/static/favicon.ico Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

7
webapp/static/js/Chart.min.js vendored Executable file

File diff suppressed because one or more lines are too long

0
webapp/static/jquery.min.js → webapp/static/js/jquery.min.js vendored Normal file → Executable file
View File

0
webapp/static/justgage.min.js → webapp/static/js/justgage.min.js vendored Normal file → Executable file
View File

234
webapp/static/main.js → webapp/static/js/main.js Normal file → Executable file
View File

@ -1,3 +1,4 @@
//declare variables for gauges and charts, defining options as per the plugin documentation
var temp_gauge = new JustGage({ var temp_gauge = new JustGage({
id: 'temp_gauge', id: 'temp_gauge',
value: 0, value: 0,
@ -73,80 +74,6 @@ var co2_gauge = new JustGage({
levelColors: ['#33ff00', '#ffff00', '#ff8800', '#ff0008'] 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; Chart.defaults.global.responsive = false;
var nan=NaN; var nan=NaN;
@ -370,31 +297,40 @@ var multiChartSettings = {
} }
}; };
//instantiate chart objects with defined options
var ctx = document.getElementById('tempchart').getContext("2d"); var ctx = document.getElementById('tempchart').getContext("2d");
var tempChart = new Chart(ctx,tempChartSettings); var tempChart = new Chart(ctx,tempChartSettings);
var ctx2 = document.getElementById('humchart').getContext("2d"); var ctx2 = document.getElementById('humchart').getContext("2d");
var humChart = new Chart(ctx2,humChartSettings); var humChart = new Chart(ctx2,humChartSettings);
var ctx3 = document.getElementById('co2chart').getContext("2d"); var ctx3 = document.getElementById('co2chart').getContext("2d");
var co2Chart = new Chart(ctx3,co2ChartSettings); var co2Chart = new Chart(ctx3,co2ChartSettings);
var ctx4 = document.getElementById('multichart').getContext("2d"); var ctx4 = document.getElementById('multichart').getContext("2d");
var multiChart = new Chart(ctx4,multiChartSettings); var multiChart = new Chart(ctx4,multiChartSettings);
//function to clear chart datasets without redrawing
function getServerDataAndUpdate(samples) { function clearChartData() {
var jsonData = $.ajax({ tempChart.data.datasets[0].data = [];
type: 'POST', tempChart.data.labels = [];
url: '/api/getData', humChart.data.datasets[0].data = [];
data: {'samples' : samples }, humChart.data.labels = [];
dataType: 'json', co2Chart.data.datasets[0].data = [];
success: console.log("requested db data"), co2Chart.data.labels = [];
}).done(function (dbdata) { multiChart.data.datasets[0].data = [];
updateChart(dbdata); multiChart.data.datasets[1].data = [];
updateGauge(dbdata); multiChart.data.labels = [];
});
}; };
//function to remove last value of chart dataset, used to create a "scrolling" effect as new values come in
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();
}
//function to update the chart datasets with data from the database
function updateChart(dbdata) { function updateChart(dbdata) {
$.each(dbdata['temp'], function(key, value) { $.each(dbdata['temp'], function(key, value) {
tempChart.data.labels.push(key); tempChart.data.labels.push(key);
@ -411,33 +347,103 @@ function updateChart(dbdata) {
$.each(dbdata['co2'], function(key, value){ $.each(dbdata['co2'], function(key, value){
co2Chart.data.datasets[0].data.push(value); co2Chart.data.datasets[0].data.push(value);
}); });
tempChart.update(); tempChart.update();
humChart.update(); humChart.update();
co2Chart.update(); co2Chart.update();
multiChart.update(); multiChart.update();
}; };
//function to update the gauges
function clearChartData() { function updateGauge(dbdata) {
tempChart.data.datasets[0].data = []; for(var key in dbdata.temp){};
tempChart.data.labels = []; temp_gauge.refresh(dbdata.temp[key]);
humChart.data.datasets[0].data = []; hum_gauge.refresh(dbdata.hum[key]);
humChart.data.labels = []; pres_gauge.refresh(dbdata.pres[key]);
co2Chart.data.datasets[0].data = []; co2_gauge.refresh(dbdata.co2[key]);
co2Chart.data.labels = []; $('#timestamp').text(key);
multiChart.data.datasets[0].data = [];
multiChart.data.datasets[1].data = [];
multiChart.data.labels = [];
}; };
//ajax data fetching function
function trimChartData() { function getServerData(samples) {
tempChart.data.datasets[0].data.shift(); return $.ajax({
tempChart.data.labels.shift(); type: 'POST',
humChart.data.datasets[0].data.shift(); url: '/api/getdata',
humChart.data.labels.shift(); data: {'samples' : samples },
co2Chart.data.datasets[0].data.shift(); dataType: 'json',
co2Chart.data.labels.shift(); success: console.log("requested db data"),
multiChart.data.datasets[0].data.shift(); });
multiChart.data.datasets[1].data.shift(); };
multiChart.data.labels.shift(); //the main function, updates the values based on the device status, run on a timer
} function getStatusAndDecide() {
var statusData = $.ajax ({
type: 'GET',
url: '/api/heartbeat',
data: {},
dataType: 'json',
success: console.log("requested heartbeat data"),
}).done(function (retdata) {
if ( typeof getStatusAndDecide.statusWas === 'undefined' ) {
getStatusAndDecide.statusWas = "dead";
};
if (retdata.status == "alive" && getStatusAndDecide.statusWas == "alive") {
$.when(getServerData(1)).done(function(retdata) {
updateChart(retdata);
updateGauge(retdata);
});
trimChartData();
getStatusAndDecide.statusWas = "alive";
}
else if (retdata.status == "alive" && getStatusAndDecide.statusWas == "dead") {
$('#footer_text').text("Live update enabled. (Data might be delayed up to a minute)");
clearChartData();
$.when(getServerData($('input[name=samples]').val())).done(function(retdata) {
updateChart(retdata);
updateGauge(retdata);
});
getStatusAndDecide.statusWas = "alive";
}
else if (retdata.status == "dead" && getStatusAndDecide.statusWas == "alive") {
$('#footer_text').text("Source not online, live update disabled. Showing latest available data.");
getStatusAndDecide.statusWas = "dead";
}
else if (retdata.status == "dead" && getStatusAndDecide.statusWas == "dead") {
getStatusAndDecide.statusWas == "dead";
}
else {
location.reload(forceGet);
}
});
};
//functions run after clicking the refresh button
$('#refresh').click(function() {
console.log("refreshing all");
clearChartData();
$.when(getServerData($('input[name=samples]').val())).done(function(retdata) {
updateChart(retdata);
updateGauge(retdata);
});
});
//functions run after choosing sample amount from the gui
$('#sample_select_form').submit(function (f) {
f.preventDefault();
console.log("selected range from form");
clearChartData();
$.when(getServerData($('input[name=samples]').val())).done(function(retdata) {
updateChart(retdata);
updateGauge(retdata);
});
});
//function run on page load, fetches and dislpays the latest data
$(document).ready(function() {
$.when(getServerData($('input[name=samples]').val())).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.");
}
updateChart(retdata);
updateGauge(retdata);
});
});
//90 second timer to refresh contents
setInterval("getStatusAndDecide()", 90000);

2
webapp/static/js/moment.min.js vendored Executable file

File diff suppressed because one or more lines are too long

0
webapp/static/raphael.min.js → webapp/static/js/raphael.min.js vendored Normal file → Executable file
View File

0
webapp/static/styles/Chart.min.css vendored Normal file → Executable file
View File

View File

@ -1,76 +0,0 @@
#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;
}

188
webapp/static/styles/style.css Executable file
View File

@ -0,0 +1,188 @@
#header_bar {
}
#uberschrift {
margin: 0;
padding: 0 2px 2px;
margin-left: 20px;
margin-top: 10px;
}
#footer_bar {
padding-bottom: 5px;
padding-left: 5px;
}
#footer_text {
margin-bottom: 2px;
}
#recent_vals {
margin-left: 20px;
}
#timestamp {
margin-top: -10px;
margin-left:20px;
}
.gauge_container {
text-align: center;
display: block;
margin: auto;
margin-top: 15px;
}
.gauge {
display: inline-block;
margin-left: 20px;
}
.gauge_desc {
margin-left: -50px;
margin-top: -30px;
margin-bottom: 20px;
}
.graph_name {
margin-top: 5px;
}
.graph_canvas {
display: block;
width: 100%;
}
#tempchart {
margin-bottom: 25px;
}
#multichart {
margin-bottom: 25px;
}
#sample_select {
margin-top: 40px;
margin-left: 20px;
}
#content_wrapper {
display: flex;
flex-flow: wrap;
margin-left: 5px;
margin-right: 5px;
}
#content_left {
flex: 0 0 18%;
justify-content: center;
}
#content_center {
/*margin-left: 10px;*/
flex: 0 0 40%;
justify-content: center;
}
#content_right {
flex: 0 0 40%;
justify-content: center;
}
body {
padding: 0;
margin: 0;
font-family: 'Noto Sans', Noto, sans-serif;
}
svg {
margin: auto;
}
@media (max-width: 1280px) {
#content_left {
display: flex !important;
flex-direction: column;
justify-content: center;
flex: 1 1 100%;
}
#content_center {
flex: 0 0 95%;
justify-content: center;
margin: auto;
}
#content_right {
flex: 0 0 95%;
justify-content: center;
margin: auto;
}
.gauge_container_top {
display: flex;
flex-direction: row;
}
.gauge_container_bottom {
display: flex;
flex-direction: row;
}
.gauge_container {
text-align: center;
display: flex;
flex-direction: row;
margin: auto;
}
.gauge {
display: inline-block;
margin-left: auto;
margin-right: auto;
}
.gauge_desc {
justify-content: center;
margin-left: -30px;
margin-right: auto;
}
#sample_select {
margin-top: auto;
}
}
@media (max-width: 1000px) {
#recent_vals {
margin-left: 5px;
}
#uberschrift {
margin: 0;
padding: 2px;
margin-left: 5px;
margin-top: 5px;
}
#content_left {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1 1 100%;
}
#content_center {
flex: 0 0 95%;
justify-content: center;
margin: auto;
}
#content_right {
flex: 0 0 95%;
justify-content: center;
margin: auto;
}
.gauge_container_top {
display: flex;
flex-direction: row;
margin: auto;
margin-left: -15px;
}
.gauge_container_bottom {
display: flex;
flex-direction: row;
margin: auto;
margin-left: -15px;
}
.gauge_container {
text-align: center;
display: flex;
flex-direction: column;
margin: auto;
}
.gauge {
display: inline-block;
margin-left: auto;
margin-right: auto;
}
.gauge_desc {
justify-content: center;
margin-left: auto;
margin-right: auto;
}
#sample_select {
margin-top: auto;
margin-left: 5px;
}
}

56
webapp/templates/index.html Normal file → Executable file
View File

@ -1,31 +1,45 @@
<!doctype html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>IAQ dash</title> <title>IAQ dash</title>
<link rel='shortcut icon' type='image/ico' href='static/favicon.ico'> <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/style.css'>
<link rel='stylesheet' href='static/styles/Chart.min.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/js/moment.min.js'></script>
<script type='text/javascript' src='static/jquery.min.js'></script> <script type='text/javascript' src='static/js/Chart.min.js'></script>
<script type='text/javascript' src='static/raphael.min.js'></script> <script type='text/javascript' src='static/js/jquery.min.js'></script>
<script type='text/javascript' src='static/justgage.min.js'></script> <script type='text/javascript' src='static/js/raphael.min.js'></script>
<script type='text/javascript' src='static/js/justgage.min.js'></script>
</head> </head>
<body> <body>
<div id='header_bar'> <div id='header_bar'>
<h1 id='uberschrift'>Indoor enviro data</h1> <h2 id='uberschrift'>Indoor enviro data</h2>
</div> </div>
<div id='content_wrapper' class='column'> <div id='content_wrapper' class='column'>
<div id='content_left' class='column'> <div id='content_left' class='column'>
<h3 id='recent_vals'>Most recent values</h3>
<h4 id='timestamp'></h4>
<div class='gauge_container'> <div class='gauge_container'>
<h3>Most recent values</h3> <div class='gauge_container_top'>
<div id='temp_gauge' class='gauge' align='center'></div> <div class='gauge_block'>
<h5 align='center' class='gauge_desc'>Temperature</h5> <div id='temp_gauge' class='gauge' align='center'></div>
<div id='hum_gauge' class='gauge' align='center'></div> <h5 align='center' class='gauge_desc'>Temperature</h5>
<h5 align='center' class='gauge_desc'>Relative humidity</h5> </div>
<div id='pres_gauge' class='gauge' align='center'></div> <div class='gauge_block'>
<h5 align='center' class='gauge_desc'>Pressure</h5> <div id='hum_gauge' class='gauge' align='center'></div>
<div id='co2_gauge' class='gauge' align='center'></div> <h5 align='center' class='gauge_desc'>Relative humidity</h5>
<h5 align='center' class='gauge_desc'>CO2 concentration</h5> </div>
</div>
<div class='gauge_container_bottom'>
<div class='gauge_block'>
<div id='pres_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>Pressure</h5>
</div>
<div class='gauge_block'>
<div id='co2_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>CO2 concentration</h5>
</div>
</div>
</div> </div>
<div id='sample_select'> <div id='sample_select'>
<form id='sample_select_form' action='/'> <form id='sample_select_form' action='/'>
@ -37,21 +51,21 @@
</div> </div>
<div id='content_center' class='column'> <div id='content_center' class='column'>
<h3 class='graph_name'>Temperature</h3> <h3 class='graph_name'>Temperature</h3>
<canvas id='tempchart' width='600' height='380' style='display: block;'></canvas> <canvas class='graph_canvas' id='tempchart'></canvas>
<h3 class='graph_name'>Humidity</h3> <h3 class='graph_name'>Humidity</h3>
<canvas id='humchart' width='600' height='380' style='display: block;'></canvas> <canvas class='graph_canvas' id='humchart'></canvas>
</div> </div>
<div id='content_right' class='column'> <div id='content_right' class='column'>
<h3 class='graph_name'>Combined</h3> <h3 class='graph_name'>Combined</h3>
<canvas id='multichart' width='600' height='380' style='display: block'></canvas> <canvas class='graph_canvas' id='multichart'></canvas>
<h3 class='graph_name'>CO2 concentration</h3> <h3 class='graph_name'>CO2 concentration</h3>
<canvas id='co2chart' width='600' height='380' style='display: block;'></canvas> <canvas class='graph_canvas' id='co2chart'></canvas>
</div> </div>
</div> </div>
<div id='footer_bar'> <div id='footer_bar'>
<p id='footer_text'>Source not online, live update disabled. Showing latest available data.</p> <p id='footer_text'>Source not online, live update disabled. Showing latest available data.</p>
</div> </div>
<script type='text/javascript' src='static/main.js'></script> <script type='text/javascript' src='static/js/main.js'></script>
</body> </body>
</html> </html>