209 lines
7.6 KiB
Python
Executable File
209 lines
7.6 KiB
Python
Executable File
from flask import Flask, current_app, render_template, make_response, jsonify, request
|
|
from waitress import serve
|
|
import sqlite3 as sql3db
|
|
import datetime
|
|
import time
|
|
import io
|
|
import json
|
|
import pandas as pd
|
|
from collections import OrderedDict
|
|
|
|
PORT = 8080 # port to serve the api on
|
|
DB = "/inenvmon/data/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 = "itsasecret" # api secret for the heartbeat method, set the same in inenvmon_collector.py
|
|
|
|
class Source():
|
|
def __init__(self):
|
|
self.status = "unknown" #used to persistently store status
|
|
self.last_msg = 0.0
|
|
|
|
app = Flask(__name__)
|
|
|
|
with app.app_context():
|
|
current_app.source = Source()
|
|
|
|
def query_db(sql): #function for executing database queries
|
|
dbconn = sql3db.connect(DB)
|
|
cursor = dbconn.cursor()
|
|
|
|
try:
|
|
cursor.execute(sql)
|
|
except sql3db.Error as e:
|
|
print(e) # output errors
|
|
|
|
data = cursor.fetchall()
|
|
dbconn.close()
|
|
|
|
return data
|
|
|
|
@app.route('/')
|
|
def index():
|
|
# 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():
|
|
if request.method == 'GET':
|
|
if request.args.get('samples') is not None:
|
|
try:
|
|
samples = int(request.args.get('samples'))
|
|
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) # output errors
|
|
samples = 120
|
|
else:
|
|
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) # output errors
|
|
samples = 120
|
|
|
|
if samples > 10800:
|
|
samples = 120
|
|
|
|
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' # string format
|
|
|
|
# initialize arrays
|
|
labels = []
|
|
temps = []
|
|
hums = []
|
|
press = []
|
|
concs = []
|
|
# split data returned from db into arrays
|
|
for i in range(len(dbdata)):
|
|
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.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) # 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]
|
|
temps=darry[samples-start:start:1,1]
|
|
hums=darry[samples-start:start:1,2]
|
|
press=darry[samples-start:start:1,3]
|
|
concs=darry[samples-start:start:1,4]
|
|
else:
|
|
labels=darry[start-samples:start:1,0]
|
|
temps=darry[start-samples:start:1,1]
|
|
hums=darry[start-samples:start:1,2]
|
|
press=darry[start-samples:start:1,3]
|
|
concs=darry[start-samples:start:1,4]
|
|
# 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]
|
|
hums=darry[0:start:1,2]
|
|
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()
|
|
# 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])
|
|
humdict[labels[i].strftime(f)] = str(hums[i])
|
|
presdict[labels[i].strftime(f)] = str(press[i])
|
|
co2dict[labels[i].strftime(f)] = str(concs[i])
|
|
|
|
# check status
|
|
if (time.time() - current_app.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]
|
|
# iterate over the data and fill the response dict
|
|
for i in range(len(datas)):
|
|
respdict[datas[i]] = dicts[i]
|
|
# formulate json response from the prepared dict
|
|
response = app.response_class(
|
|
response=json.dumps(respdict),
|
|
status=200,
|
|
mimetype='application/json'
|
|
)
|
|
return response
|
|
|
|
@app.route('/api/heartbeat', methods = ['GET','POST'])
|
|
def heartbeat(): # device status reporting
|
|
if request.method == 'POST':
|
|
key = request.form.get('key') # check the secret
|
|
if key == API_SECRET:
|
|
if request.form.get('message') is not None:
|
|
current_app.source.last_msg = float(request.form.get('message')) # save the status if timestamp present
|
|
else:
|
|
current_app.source.last_msg = "nodata"
|
|
current_app.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() - current_app.source.last_msg) > 180:
|
|
current_app.source.status = "dead"
|
|
else:
|
|
current_app.source.status = "alive"
|
|
# formulate response json
|
|
resp_data = {'status': current_app.source.status, 'last_msg': current_app.source.last_msg}
|
|
get_response=app.response_class(
|
|
response=json.dumps(resp_data),
|
|
status=200,
|
|
mimetype='application/json'
|
|
)
|
|
return get_response
|
|
|
|
if __name__ == "__main__":
|
|
print("INFO: Starting inenvmon REST API on port {}".format(PORT))
|
|
serve(app, host="0.0.0.0", port=PORT)
|