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.
### 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
import mysql.connector as mariadb
import sqlite3 as sql3db
import datetime
import time
import io
@ -7,27 +7,26 @@ import json
import pandas as pd
from collections import OrderedDict
DB = ""
DB_USER = ""
DB_PASSWORD = ""
API_SECRET = ""
DB = "../inenvmon_data.db" # path to the database file as set up in inenvmon_collector.py
DB_TABLE = "envdata0" # name of database table, as set up in inenvmon_collector.py
API_SECRET = "secret" # api secret for the heartbeat method, set the same in inenvmon_collector.py
class Source():
def __init__(self):
self.status = "unknown"
self.status = "unknown" #used to persistently store status
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)
def query_db(sql): #function for executing database queries
dbconn = sql3db.connect(DB)
cursor = dbconn.cursor()
try:
cursor.execute(sql)
except mariadb.Error as e:
print(e)
except sql3db.Error as e:
print(e) # output errors
data = cursor.fetchall()
dbconn.close()
@ -36,63 +35,67 @@ def query_db(sql):
@app.route('/')
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'])
def process_data():
@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:
if samples > 10800: # limit the maximum amount of data points, working with values above this threshold was observed unstable
samples = 120
except Exception as excp:
print(excp)
print(excp) # output errors
samples = 120
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':
try:
samples = int(request.form.get('samples'))
except Exception as excp:
print(excp)
print(excp) # output errors
samples = 120
if samples > 10800:
samples = 120
sql = "SELECT * FROM envdata ORDER BY timestamp DESC LIMIT %s" % samples
dbdata = query_db(sql)
sql = "SELECT * FROM %s ORDER BY timestamp DESC LIMIT %s" % (DB_TABLE, samples) # sql selection string
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 = []
temps = []
hums = []
press = []
concs = []
# split data returned from db into arrays
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])
hums.append(dbdata[i][2])
press.append(dbdata[i][3])
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})
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()
# pandas returns weird stuff, converted here to a numpy array
# numpy because that's what available without additional work
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)
start = len(darry) # save the length for further slicing
# array slicing and ordering
# because js graphing function expects the dataset with inverted timebase
# different procedures for different scenarios
# in case of missing values
if len(darry) != samples:
if samples > len(darry):
labels=darry[samples-start:start:1,0]
@ -106,12 +109,14 @@ def process_data():
hums=darry[start-samples:start:1,2]
press=darry[start-samples:start:1,3]
concs=darry[start-samples:start:1,4]
# in case of a single value (when updating the plot)
elif len(darry) == 1:
labels=darry[0,0]
temps=darry[0,1]
hums=darry[0,2]
press=darry[0,3]
concs=darry[0,4]
# in case of uninterrupted dataset
else:
labels=darry[0:start:1,0]
temps=darry[0:start:1,1]
@ -119,16 +124,18 @@ def process_data():
press=darry[0:start:1,3]
concs=darry[0:start:1,4]
# order of the values is important therefore ordered dict
tempdict = OrderedDict()
humdict = OrderedDict()
presdict = OrderedDict()
co2dict = OrderedDict()
co2dict = OrderedDict()
# fill the dict straight away with the single value
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)
# iterate over the arrays and fill the dicts
else:
for i in range(len(labels)):
tempdict[labels[i].strftime(f)] = str(temps[i])
@ -136,18 +143,20 @@ def process_data():
presdict[labels[i].strftime(f)] = str(press[i])
co2dict[labels[i].strftime(f)] = str(concs[i])
# check status
if (time.time() - source.last_msg) > 180:
status = "dead"
else:
status = "alive"
# final response dict
respdict = OrderedDict()
# ready data and corresponding key for json
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)):
respdict[datas[i]] = dicts[i]
respdict[datas[i]] = dicts[i]
# formulate json response from the prepared dict
response = app.response_class(
response=json.dumps(respdict),
status=200,
@ -156,31 +165,33 @@ def process_data():
return response
@app.route('/api/heartbeat', methods = ['GET','POST'])
def heartbeat():
def heartbeat(): # device status reporting
global source
if request.method == 'POST':
key = request.form.get('key')
if request.method == 'POST':
key = request.form.get('key') # check the secret
if key == API_SECRET:
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:
source.last_msg = "nodata"
source.status = "unknown"
source.status = "unknown" #unknown status if no timestamp
#respond with 200 ok
post_response = app.response_class(
status=200)
return post_response
else:
# respond with 401 unauthorized if secret is incorrect
err_response = app.response_class(
status=401)
return err_response
if request.method == 'GET':
# if get request calculate time delta
if (time.time() - source.last_msg) > 180:
source.status = "dead"
else:
source.status = "alive"
# formulate response json
resp_data = {'status': source.status, 'last_msg': source.last_msg}
get_response=app.response_class(
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({
id: 'temp_gauge',
value: 0,
@ -73,80 +74,6 @@ var co2_gauge = new JustGage({
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;
@ -370,31 +297,40 @@ var multiChartSettings = {
}
};
//instantiate chart objects with defined options
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 to clear chart datasets without redrawing
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 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) {
$.each(dbdata['temp'], function(key, value) {
tempChart.data.labels.push(key);
@ -411,33 +347,103 @@ function updateChart(dbdata) {
$.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 to update the gauges
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]);
$('#timestamp').text(key);
};
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();
}
//ajax data fetching function
function getServerData(samples) {
return $.ajax({
type: 'POST',
url: '/api/getdata',
data: {'samples' : samples },
dataType: 'json',
success: console.log("requested db data"),
});
};
//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>
<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/style.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>
<script type='text/javascript' src='static/js/moment.min.js'></script>
<script type='text/javascript' src='static/js/Chart.min.js'></script>
<script type='text/javascript' src='static/js/jquery.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>
<body>
<div id='header_bar'>
<h1 id='uberschrift'>Indoor enviro data</h1>
<h2 id='uberschrift'>Indoor enviro data</h2>
</div>
<div id='content_wrapper' class='column'>
<div id='content_left' class='column'>
<h3 id='recent_vals'>Most recent values</h3>
<h4 id='timestamp'></h4>
<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 class='gauge_container_top'>
<div class='gauge_block'>
<div id='temp_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>Temperature</h5>
</div>
<div class='gauge_block'>
<div id='hum_gauge' class='gauge' align='center'></div>
<h5 align='center' class='gauge_desc'>Relative humidity</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 id='sample_select'>
<form id='sample_select_form' action='/'>
@ -37,21 +51,21 @@
</div>
<div id='content_center' class='column'>
<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>
<canvas id='humchart' width='600' height='380' style='display: block;'></canvas>
<canvas class='graph_canvas' id='humchart'></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>
<canvas class='graph_canvas' id='multichart'></canvas>
<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 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>
<script type='text/javascript' src='static/js/main.js'></script>
</body>
</html>