major cleanup, added comments
This commit is contained in:
parent
743aa2b383
commit
d30d6096d1
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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()
|
|
@ -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
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
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
0
webapp/static/jquery.min.js → webapp/static/js/jquery.min.js
vendored
Normal file → Executable file
0
webapp/static/justgage.min.js → webapp/static/js/justgage.min.js
vendored
Normal file → Executable file
0
webapp/static/justgage.min.js → webapp/static/js/justgage.min.js
vendored
Normal file → Executable 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);
|
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
0
webapp/static/raphael.min.js → webapp/static/js/raphael.min.js
vendored
Normal file → Executable 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue