diff options
author | Matt Kohls <mattkohls13@gmail.com> | 2023-09-04 15:36:33 -0400 |
---|---|---|
committer | Matt Kohls <mattkohls13@gmail.com> | 2023-09-04 15:36:33 -0400 |
commit | c8ef8843aaaf28bc38b544ae8ac72accf233aead (patch) | |
tree | 53e846d7480814040bbebe11dd6e290f5d366589 /snag | |
download | sensor-aggregator-c8ef8843aaaf28bc38b544ae8ac72accf233aead.tar.gz sensor-aggregator-c8ef8843aaaf28bc38b544ae8ac72accf233aead.tar.bz2 sensor-aggregator-c8ef8843aaaf28bc38b544ae8ac72accf233aead.zip |
Revamped project init
New repo for larger structural changes from Sensor-Server. Currently
doing most all the same stuff but hopefully better. Still have to clean
out some of the older templates for the new interface
Diffstat (limited to 'snag')
-rw-r--r-- | snag/__init__.py | 41 | ||||
-rw-r--r-- | snag/dashboard.py | 26 | ||||
-rw-r--r-- | snag/data.py | 116 | ||||
-rw-r--r-- | snag/db.py | 41 | ||||
-rw-r--r-- | snag/graphs.py | 315 | ||||
-rw-r--r-- | snag/schema.sql | 46 | ||||
-rw-r--r-- | snag/static/interactive.js | 64 | ||||
-rw-r--r-- | snag/static/pagedown.css | 155 | ||||
-rw-r--r-- | snag/static/style.css | 18 | ||||
-rw-r--r-- | snag/templates/dashboard/dashboard.html | 43 | ||||
-rw-r--r-- | snag/templates/interactive.html | 39 | ||||
-rw-r--r-- | snag/templates/layout.html | 29 | ||||
-rw-r--r-- | snag/templates/show_entries.html | 44 |
13 files changed, 977 insertions, 0 deletions
diff --git a/snag/__init__.py b/snag/__init__.py new file mode 100644 index 0000000..c0b936f --- /dev/null +++ b/snag/__init__.py @@ -0,0 +1,41 @@ +# snag +# Matt Kohls +# (c) 2023 + +import os + +from flask import Flask + +def create_app(test_config=None): + # create and configure the app + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY='development_key', + DATABASE=os.path.join(app.instance_path, 'snag.sqlite'), + ) + + # probably not going to mess with test stuff but just in case + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile('config.py', silent=True) + else: + # load the test config if passed in + app.config.from_mapping(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + from . import db + db.init_app(app) + + from . import graphs, data, dashboard + app.register_blueprint(graphs.bp) + app.register_blueprint(data.bp) + app.register_blueprint(dashboard.bp) + app.add_url_rule('/', endpoint='dashboard') + + return app + diff --git a/snag/dashboard.py b/snag/dashboard.py new file mode 100644 index 0000000..7e77124 --- /dev/null +++ b/snag/dashboard.py @@ -0,0 +1,26 @@ +# snag +# Matt Kohls +# (c) 2023 + +from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for +) +from werkzeug.exceptions import abort +from datetime import datetime +from snag.db import get_db + +bp = Blueprint('dashboard', __name__) + +@bp.route('/') +def dashboard(): + db = get_db() + + rows = db.execute( + 'SELECT de.deviceId, de.environment, d.deviceName' + ' FROM device_env de JOIN devices d ON de.deviceId = d.deviceId' + ' ORDER BY entry DESC' + ).fetchall() + + deviceList = [dict(deviceId=row['deviceId'], location=row['environment'], name=row['deviceName']) for row in rows] + return render_template('dashboard/dashboard.html', deviceList=deviceList, generatedAt=datetime.now()) + diff --git a/snag/data.py b/snag/data.py new file mode 100644 index 0000000..3bf8df3 --- /dev/null +++ b/snag/data.py @@ -0,0 +1,116 @@ +# snag +# Matt Kohls +# (c) 2023 + +from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for, json +) +from werkzeug.exceptions import abort +from datetime import datetime +from snag.db import get_db + +bp = Blueprint('data', __name__, url_prefix='/data') + +# Process json data +# +# Expects something like this: +# { +# "scheme": "1", +# "deviceId": 1, +# "readings": [ +# { +# "env": "outdoor", +# "temp": 123 +# }, +# { +# "env": "enclosed", +# "temp": 456 +# } +# ], +# "status": [ +# { +# "heater": "off", +# "battery": 10 +# } +# ] +# } +def unpack_json_scheme_1(req): + try: + deviceId = req['deviceId'] + db = get_db() + + # Status only has one object in it, and is optional + if 'status' in req: + if 'timeStamp' in req['status'][0]: + now = req['status'][0]['timeStamp'] + else: + now = datetime.now() + battery = req['status'][0]['battery'] + heater = req['status'][0]['heater'] + db.execute('INSERT INTO device_status (date, deviceId, batteryCharge, heaterRunning) VALUES (?, ?, ?, ?)', [now, deviceId, battery, heater]) + + for environment in req['readings']: + env = environment['environment'] + temp = environment['temperature'] + humidity = environment['humidity'] + pressure = environment['pressure'] + now = environment['timeStamp'] + if env == 'outdoor': + lux = environment['lux'] + uv = environment['uv'] + db.execute('INSERT INTO env_outdoor (date, deviceId, temperature, humidity, pressure, lux, uv_intensity) VALUES (?, ?, ?, ?, ?, ?, ?)', [now, deviceId, temp, humidity, pressure, lux, uv]) + elif env == 'indoor': + db.execute('INSERT INTO env_indoor (date, deviceId, temperature, humidity, pressure) VALUES (?, ?, ?, ?, ?)', [now, deviceId, temp, humidity, pressure]) + elif env == 'enclosed': + light = environment['light'] + db.execute('INSERT INTO env_enclosed (date, deviceId, temperature, humidity, pressure, light) VALUES (?, ?, ?, ?, ?, ?)', [now, deviceId, temp, humidity, pressure, light]) + else: + raise 'Bad reading environment' + + db.commit() + + except Exception as err: + print(err) + return "Bad JSON", 400 + + return "Data received", 200 + +@bp.route('/json', methods=['POST']) +def add_json_data(): + if request.is_json: + req = request.get_json() + if 'scheme' in req: + if req['scheme'] == '1': + return unpack_json_scheme_1(req) + else: + abort(400, "Unknown scheme") + else: + abort(400, "Unknown JSON") + + else: + abort(400, "Unknown payload type") + +@bp.route('/devices', methods=['GET']) +def get_devices(): + db = get_db() + + out = [] + devices = db.execute('SELECT * FROM devices ORDER BY deviceId').fetchall() + for device in devices: + envList = [] + envs = db.execute('SELECT * FROM device_env WHERE deviceId = ?', [device['deviceId']]).fetchall() + for env in envs: + envDict = { + "environment" : env["environment"], + # "environmentDesc" : env["environmentDescription"] + } + envList.append(envDict) + deviceLine = { + "deviceId" : device["deviceId"], + "deviceName" : device["deviceName"], + "deviceDescription" : device["deviceLocation"] + } + deviceLine['environments'] = envList + out.append(deviceLine) + return out + diff --git a/snag/db.py b/snag/db.py new file mode 100644 index 0000000..56e3306 --- /dev/null +++ b/snag/db.py @@ -0,0 +1,41 @@ +# snag +# Matt Kohls +# (c) 2023 + +import sqlite3 + +import click +from flask import current_app, g + +def get_db(): + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + +def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +def init_db(): + db = get_db() + + with current_app.open_resource('schema.sql') as f: + db.executescript(f.read().decode('utf8')) + +@click.command('init-db') +def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + +def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) + diff --git a/snag/graphs.py b/snag/graphs.py new file mode 100644 index 0000000..2905d20 --- /dev/null +++ b/snag/graphs.py @@ -0,0 +1,315 @@ +# snag +# Matt Kohls +# (c) 2023 + +import functools +from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for, Response +) +from werkzeug.exceptions import abort +from datetime import datetime, timedelta +from dateutil import parser +from io import BytesIO +from matplotlib.figure import Figure +import matplotlib.dates as mdates +from snag.db import get_db + +bp = Blueprint('graph', __name__, url_prefix='/graph') + +def get_table(env): + if env == 'outdoor': + return 'env_outdoor' + elif env == 'indoor': + return 'env_indoor' + elif env == 'enclosed': + return 'env_enclosed' + else: + return None + +def get_current(requestArgs, columns, table): + # start and end should be formatted YYYYMMDDHHMMSS + start = requestArgs.get('start') + end = requestArgs.get('end') + env = requestArgs.get('type') + deviceId = request.args.get('deviceId') + + # Sets default dates of information to be fetched, starttime will default to one day behind endtime, which defaults to now + if end is None: + endtime = datetime.now() + else: + endtime = datetime.strptime(end, "%Y%m%d%H%M%S") + if start is None: + starttime = endtime - timedelta(days=1) + else: + starttime = datetime.strptime(start, "%Y%m%d%H%M%S") + + if endtime < starttime: + temp = starttime + starttime = endtime + endtime = temp + + if deviceId is None: + deviceId = 1 + + db = get_db() + + if table is not None: + sql = (f"SELECT {columns} " + f"FROM {table} " + "WHERE (date BETWEEN ? AND ?) AND deviceId = ?") + subset = db.execute(sql, [starttime, endtime, deviceId]) + else: + subset = None + + return subset + +def single_line_graph(xData, yData, xLabel, yLabel, title, lineColor): + fig = Figure() + ax = fig.subplots() + locator = mdates.AutoDateLocator(minticks=3, maxticks=7) + formatter = mdates.ConciseDateFormatter(locator) + + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + ax.plot(xData, yData, color=lineColor) + ax.set_xlabel(xLabel) + ax.set_ylabel(yLabel) + ax.set_title(title) + + buf = BytesIO() + fig.savefig(buf, format="png") + + return buf + +def double_line_graph(xData, yData1, yData2, xLabel, yLabel1, yLabel2, title, line1Color, line2Color, line1Label, line2Label): + fig = Figure() + ax = fig.subplots() + bx = ax.twinx() + locator = mdates.AutoDateLocator(minticks=3, maxticks=7) + formatter = mdates.ConciseDateFormatter(locator) + + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + bx.xaxis.set_major_locator(locator) + bx.xaxis.set_major_formatter(formatter) + aline, = ax.plot(xData, yData1, color=line1Color, label=line1Label) + ax.set_xlabel(xLabel) + ax.set_ylabel(yLabel1) + ax.set_title(title) + bline, = bx.plot(xData, yData2, color=line2Color, label=line2Label) + bx.set_ylabel(yLabel2) + + lines = [aline, bline] + ax.legend(lines, [l.get_label() for l in lines]) + + buf = BytesIO() + fig.savefig(buf, format="png") + + return buf + +def double_y_graph(xData, yData1, yData2, xLabel, yLabel1, yLabel2, title, line1Color): + fig = Figure() + ax = fig.subplots() + bx = ax.twinx() + locator = mdates.AutoDateLocator(minticks=3, maxticks=7) + formatter = mdates.ConciseDateFormatter(locator) + + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + bx.xaxis.set_major_locator(locator) + bx.xaxis.set_major_formatter(formatter) + ax.plot(xData, yData1, color=line1Color) + ax.set_xlabel(xLabel) + ax.set_ylabel(yLabel1) + ax.set_title(title) + bx.plot(xData, yData2, visible=False) + bx.set_ylabel(yLabel2) + + buf = BytesIO() + fig.savefig(buf, format="png") + + return buf + +@bp.route('/temperature.png', methods=['GET']) +def draw_t_graph(): + cur = get_current(request.args, "date, temperature", get_table(request.args['type'])) + + if cur is None: + abort(404, "Data not found") + + data = cur.fetchall() + dates = [] + tempsC = [] + tempsF = [] + + if data is None: + abort(404, "Data not found") + + for row in data: + if isinstance(row['date'], str): + dates.append(parser.parse(row['date'])) + else: + dates.append(row['date']) + tempsC.append(row['temperature']) + tempsF.append(row['temperature'] * 1.8 + 32) + + buf = double_y_graph(dates, tempsC, tempsF, "Timestamps", "Celsius", "Fahrenheit", "Temperature", "C1") + + return Response(buf.getvalue(), mimetype='image/png') + +@bp.route('/humidity.png', methods=['GET']) +def draw_h_graph(): + cur = get_current(request.args, "date, humidity", get_table(request.args['type'])) + + if cur is None: + abort(404, "Data not found") + + data = cur.fetchall() + dates = [] + humidities = [] + + if data is None: + abort(404, "Data not found") + + for row in data: + if isinstance(row['date'], str): + dates.append(parser.parse(row['date'])) + else: + dates.append(row['date']) + humidities.append(row['humidity']) + + buf = single_line_graph(dates, humidities, "Timestamps", "Percent", "Relative Humidity", "C0") + + return Response(buf.getvalue(), mimetype='image/png') + +@bp.route('/humidity-temperature.png', methods=['GET']) +def draw_ht_graph(): + cur = get_current(request.args, "date, temperature, humidity", get_table(request.args['type'])) + + if cur is None: + abort(404, "Data not found") + + data = cur.fetchall() + dates = [] + humidities = [] + #tempsC = [] + tempsF = [] + + if data is None: + abort(404, "Data not found") + + for row in data: + if isinstance(row['date'], str): + dates.append(parser.parse(row['date'])) + else: + dates.append(row['date']) + # tempsC.append(row['temperature']) + tempsF.append(row['temperature'] * 1.8 + 32) + humidities.append(row['humidity']) + + buf = double_line_graph(dates, humidities, tempsF, "Timestamps", "Percent", "Fahrenheit", "Humidity and Temperature", "C0", "C1", "Relative Humidity", "Temperature") + + return Response(buf.getvalue(), mimetype='image/png') + +@bp.route('/pressure.png', methods=['GET']) +def draw_p_graph(): + cur = get_current(request.args, "date, pressure", get_table(request.args['type'])) + + if cur is None: + abort(404, "Data not found") + + data = cur.fetchall() + dates = [] + pressures = [] + + if data is None: + abort(404, "Data not found") + + for row in data: + if isinstance(row['date'], str): + dates.append(parser.parse(row['date'])) + else: + dates.append(row['date']) + pressures.append(row['pressure']) + + buf = single_line_graph(dates, pressures, "Timestamps", "hPa", "Barometric Pressure", "C2") + + return Response(buf.getvalue(), mimetype='image/png') + +@bp.route('/battery.png', methods=['GET']) +def draw_b_graph(): + cur = get_current(request.args, "date, batteryCharge", "device_status") + + if cur is None: + abort(404, "Data not found") + + data = cur.fetchall() + dates = [] + batteries = [] + + if data is None: + abort(404, "Data not found") + + for row in data: + if isinstance(row['date'], str): + dates.append(parser.parse(row['date'])) + else: + dates.append(row['date']) + batteries.append(row['batteryCharge']) + + buf = single_line_graph(dates, batteries, "Timestamps", "Volts", "Battery Voltage", "C3") + + return Response(buf.getvalue(), mimetype='image/png') + +@bp.route('/light.png', methods=['GET']) +def draw_l_graph(): + env = request.args['type'] + + if (env is not None) and (env == 'outdoor'): + cur = get_current(request.args, "date, uv_intensity, lux", get_table(env)) + + if cur is None: + abort(404, "Data not found") + + data = cur.fetchall() + + if data is None: + abort(404, "Data not found") + + dates = [] + uv = [] + lux = [] + for row in data: + if isinstance(row['date'], str): + dates.append(parser.parse(row['date'])) + else: + dates.append(row['date']) + uv.append(row['uv_intensity']) + lux.append(row['lux']) + + buf = double_line_graph(dates, uv, lux, "Timestamps", "UV Index", "Lux", "UV and Light Intensity", "C4", "C5", "UV Light", "Light Intensity") + + else: + cur = get_current(request.args, "date, light", get_table(env)) + + if cur is None: + abort(404, "Data not found") + + data = cur.fetchall() + + if data is None: + abort(404, "Data not found") + + dates = [] + light = [] + for row in data: + if isinstance(row['date'], str): + dates.append(parser.parse(row['date'])) + else: + dates.append(row['date']) + light.append(row['light']) + + buf = single_line_graph(dates, light, "Timestamps", "Raw Light Reading", "Light Intensity", "C5") + + return Response(buf.getvalue(), mimetype='image/png') + diff --git a/snag/schema.sql b/snag/schema.sql new file mode 100644 index 0000000..9c4dfda --- /dev/null +++ b/snag/schema.sql @@ -0,0 +1,46 @@ +create table devices ( + deviceId integer primary key autoincrement, + deviceName text, + deviceLocation text +); +create table device_env ( + entry integer primary key autoincrement, + deviceId integer, + environment text, + environmentDescription text +); +create table env_outdoor ( + entry integer primary key autoincrement, + date timestamp, + deviceId integer, + temperature real, + humidity real, + pressure real, + lux real, + uv_intensity real +); +create table env_indoor ( + entry integer primary key autoincrement, + date timestamp, + deviceId integer, + temperature real, + humidity real, + pressure real +); +create table env_enclosed ( + entry integer primary key autoincrement, + date timestamp, + deviceId integer, + temperature real, + humidity real, + pressure real, + light real +); +create table device_status ( + entry integer primary key autoincrement, + date timestamp, + deviceId integer, + batteryCharge real, + heaterRunning integer +); + diff --git a/snag/static/interactive.js b/snag/static/interactive.js new file mode 100644 index 0000000..de44670 --- /dev/null +++ b/snag/static/interactive.js @@ -0,0 +1,64 @@ +/** + * Script to change the shown graph + * + * Matt Kohls + * 2021 + */ + +const form = document.getElementById('query'); +const submitButton = document.getElementById('submit'); +const figure = document.getElementById('figure'); + +function sendRequest(data) { + let url = "/", + urlEncoded = "", + urlEncodedPairs = [], + name, + startDate, + startTime, + endDate, + endTime; + + for (name of data) { + switch (name[0]) { + case 'graph': + url = url.concat(name[1]); + break; + case 'sd': + startDate = name[1].replace(/-/g, ''); + break; + case 'st': + startTime = name[1].replace(/:/g, '').concat('00'); + case 'ed': + endDate = name[1].replace(/-/g, ''); + break; + case 'et': + endTime = name[1].replace(/:/g, '').concat('00'); + break; + case 'deviceId': + default: + urlEncodedPairs.push(name[0] + '=' + name[1]); + } + } + if (startDate !== "") { + if (startTime === "00") { + startTime = "000000"; + } + urlEncodedPairs.push('start=' + startDate + startTime); + } + if (endDate !== "") { + if (endTime === "00") { + endTime = "000000"; + } + urlEncodedPairs.push('end=' + endDate + endTime); + } + + urlEncoded = urlEncodedPairs.join('&'); + figure.setAttribute('src', url + '?' + urlEncoded); +} + +submitButton.addEventListener('click', function() { + const formData = new FormData(form); + sendRequest(formData); +}) + diff --git a/snag/static/pagedown.css b/snag/static/pagedown.css new file mode 100644 index 0000000..7f9c3e0 --- /dev/null +++ b/snag/static/pagedown.css @@ -0,0 +1,155 @@ +@font-face { + font-family: "Courier 10 Pitch"; + src: url(/fonts/courier10bt-regular.woff); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Courier 10 Pitch"; + src: url(/fonts/courier10bt-bold.woff); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: "Courier 10 Pitch"; + src: url(/fonts/courier10bt-italic.woff); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: "Courier 10 Pitch"; + src: url(/fonts/courier10bt-bolditalic.woff); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: Literata; + src: url(/fonts/Literata-Regular.woff) + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: Literata; + src: url(/fonts/Literata-Bold.woff) + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: Literata; + src: url(/fonts/Literata-Italic.woff) + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: Literata; + src: url(/fonts/Literata-BoldItalic.woff) + font-weight: bold; + font-style: italic; +} + +html, body { + min-height: 100%; + height: 100%; + margin: 1em +} + +body { + color: #212529; + background-color: #fff; + font-family: "Nimbus Sans L", Helvetica, sans-serif; +} + +img, video { + display: block; + max-width: 80%; + box-shadow: 1px 1px 5px 0 rgba(0,0,0,.4); + margin: 0 auto +} + +figcaption { + text-align: center; + margin: 0 auto +} + +pre { + font-family: "Courier 10 Pitch", Courier, monospace; + background: #eee; + padding: .5rem; + margin: 0 -.5rem; + overflow-x: auto +} + +nav { + margin: 0 auto; + clear: both +} + +nav a:not(:first-child) { + margin-left: 1rem +} + +nav .brand { + font-size: 1.25rem; + position: relative; + top: 1px +} + +label { + display: inline-block; + margin-bottom: .25rem +} + +aside { + border-left-style: dotted; + padding: 1em +} + +h1 { + font-family: Literata, serif; +} + +h2 { + font-family: Literata, serif; +} + +h3 { + font-family: Literata, serif; +} + +h4 { + font-family: Literata, serif; +} + +header { + width: 50%; + margin-top: .5rem; +} + +table { + border-collapse: collapse; + border: 0px +} + +th { + border-bottom: 1px solid #212121 +} + +th, td { + padding: 5px; +} + +tr:nth-child(even) { + background-color: #f6f6f6 +} + +footer { + width: 50%; + margin: 0 0 1rem +} diff --git a/snag/static/style.css b/snag/static/style.css new file mode 100644 index 0000000..da1cb34 --- /dev/null +++ b/snag/static/style.css @@ -0,0 +1,18 @@ +body { font-family: sans-serif; background: #eee; } +a, h1, h2 { color: #377ba8; } +h1, h2 { font-family: 'Georgia', serif; margin: 0; } +h1 { border-bottom: 2px solid #eee; } +h2 { font-size: 1.2em; } + +.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } +.weather { list-style: none; margin: 0; padding: 0; } +.weather li { margin: 0.8em 1.2em; } +.weather li h2 { margin-left: -1em; } +.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } +.add-entry dl { font-weight: bold; } +.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } +.flash { background: #cee5F5; padding: 0.5em; + border: 1px solid #aacbe2; } +.error { background: #f0d6d6; padding: 0.5em; } diff --git a/snag/templates/dashboard/dashboard.html b/snag/templates/dashboard/dashboard.html new file mode 100644 index 0000000..c061b66 --- /dev/null +++ b/snag/templates/dashboard/dashboard.html @@ -0,0 +1,43 @@ +{% extends 'layout.html' %} + +{% block headline %}{% block title %}Dashboard{% endblock %}{% endblock %} + +{% block content %} +<main> + {% if deviceList|length == 0 %} +Hmmm nothing seems to be logged yet + {% endif %} + {% for entry in deviceList %} + + <h2> {{ entry.name }} | {{ entry.location }} </h2> + <table> + <tr> + {% if entry.location != "status" %} + <td> + <figure> + <embed type="image/png" src="/graph/humidity-temperature.png?deviceId={{ entry.deviceId }}&type={{ entry.location}}" /> + </figure> + </td> + + <td> + <figure> + <embed type="image/png" src="/graph/pressure.png?deviceId={{ entry.deviceId }}&type={{ entry.location}}" /> + </figure> + </td> + {% else %} + <td> + <figure> + <embed type="image/png" src="/graph/battery.png?deviceId={{ entry.deviceId }}" /> + </figure> + </td> + {% endif %} + </tr> + + </table> + + {% endfor %} + +</main> +{% endblock %} + +{% block footer %} Best by: {{ generatedAt }} {% endblock %} diff --git a/snag/templates/interactive.html b/snag/templates/interactive.html new file mode 100644 index 0000000..1756865 --- /dev/null +++ b/snag/templates/interactive.html @@ -0,0 +1,39 @@ +{% extends "layout.html" %} +{% block body %} + <form id="query"> + <p> + <label>Location</label> + <select id="deviceId" name="deviceId"> + <option value="1">Bedroom</option> + <option value="2">Mainfloor</option> + </select> + </p> + <p> + <label>Graph Type</label> + <select id="graph" name="graph"> + <option value="htgraph.png">Humidity & Temperature</option> + <option value="tgraph.png">Temperature</option> + <option value="hgraph.png">Humidity</option> + <option value="pgraph.png">Pressure</option> + <option value="bgraph.png">Battery</option> + </select> + </p> + <p> + <label>Start Date</label> + <input type="date" name="sd" id="sd"> + <input type="time" name="st" id="st"> + </p> + <p> + <label>End Date</label> + <input type="date" name="ed" id="ed"> + <input type="time" name="et" id="et"> + </p> + </form> + <button id='submit'>Submit</button> + + <figure> + <embed id="figure" type="image/png" src="/htgraph.png?deviceId=2" /> + </figure> + + <script src="static/interactive.js"></script> +{% endblock %} diff --git a/snag/templates/layout.html b/snag/templates/layout.html new file mode 100644 index 0000000..50ad57b --- /dev/null +++ b/snag/templates/layout.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en-US"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>snag | {% block title %}{% endblock %}</title> + <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='pagedown.css') }}"> + </head> + + <body> + <header> + <nav> + <a href="/">Dashboard</a> <a href="interactive">Interactive</a> + </nav> + <hr> + </header> + <h1> {% block headline %}{% endblock %} </h1> + <hr> + + {% block content %}{% endblock %} + + <footer> + <hr> + <p> + {% block footer %}{% endblock %} + </p> + </footer> + </body> +</html> diff --git a/snag/templates/show_entries.html b/snag/templates/show_entries.html new file mode 100644 index 0000000..86451df --- /dev/null +++ b/snag/templates/show_entries.html @@ -0,0 +1,44 @@ +{% extends "layout.html" %} +{% block body %} + <aside> + <b>{% if devId == 1 %}Bedroom + {% else %}Mainfloor + {% endif %}Currently:</b> + <ul class=current> + <li>{{ current.temperature }}°F</li> + <li>{{ current.humidity }}% humidity</li> + </ul> + </aside> + <figure> + <embed type="image/png" src="/htgraph.png?deviceId={{ devId }}" /> + </figure> + <figure> + <embed type="image/png" src="/pgraph.png?deviceId={{ devId }}" /> + </figure> + {% if devId == 2 %} + <figure> + <embed type="image/png" src="/bgraph.png?deviceId={{ devId }}" /> + </figure> + {% endif %} + + <hr> + <h2>Readings from the past 24 hours</h2> + <p> + <table class=log> + <tr> + <th>Timestamp</th> + <th>Temperature (C)</th> + <th>Humidity (%)</th> + <th>Pressure (hPa)</th> + </tr> + {% for entry in log %} + <tr> + <td>{{ entry.date }}</td> + <td>{{ entry.temperature }}</td> + <td>{{ entry.humidity }}</td> + <td>{{ entry.pressure }}</td> + </tr> + {% endfor %} + </table> + </p> +{% endblock %} |