diff options
author | Matt Kohls <mattkohls13@gmail.com> | 2021-04-23 23:42:42 -0400 |
---|---|---|
committer | Matt Kohls <mattkohls13@gmail.com> | 2021-04-23 23:42:42 -0400 |
commit | 437c1179b545927197637c26bb2d87d20401e5f2 (patch) | |
tree | 590e57b5dd6f0d707d8e95a7a5242bd1e892053f | |
parent | 352c56375b4ec2af8f0b593b450069271bcd65e2 (diff) | |
download | Sensor-Server-437c1179b545927197637c26bb2d87d20401e5f2.tar.gz Sensor-Server-437c1179b545927197637c26bb2d87d20401e5f2.tar.bz2 Sensor-Server-437c1179b545927197637c26bb2d87d20401e5f2.zip |
Many changes, mostly to make landing page more readable
Oh and a file to read a sensor attached via i2c on a Raspberry Pi and
add that to the db
-rw-r--r-- | local.py | 84 | ||||
-rwxr-xr-x[-rw-r--r--] | sensor.py | 330 | ||||
-rw-r--r-- | static/interactive.js | 64 | ||||
-rw-r--r-- | static/pagedown.css | 155 | ||||
-rw-r--r-- | templates/interactive.html | 39 | ||||
-rw-r--r-- | templates/layout.html | 16 | ||||
-rw-r--r-- | templates/show_entries.html | 43 |
7 files changed, 681 insertions, 50 deletions
diff --git a/local.py b/local.py new file mode 100644 index 0000000..fb7803a --- /dev/null +++ b/local.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +## Local Sensor Reading +# +# Reads data from sensor attached to Pi and saves in database +# +# Matt Kohls +# 2021 +## + +import sqlite3 +from sqlite3 import Error +from datetime import datetime +import time +import board +import busio +import adafruit_bme280 + + +# Local i2c BME sensor config +i2c = busio.I2C(board.SCL, board.SDA) +bme = adafruit_bme280.Adafruit_BME280_I2C(i2c) + +# Device ID for logging +deviceId = 1 + +# Start with connecting +print("Connecting to db...") +try: + db = sqlite3.connect('sensors.db') + +except Error: + print("DB connect fail: %s" % Error) + exit(-1) + + +# Check if table exists, create if needed +cursor = db.cursor() +try: + cursor.execute('SELECT name from sqlite_master WHERE type = "table" AND name = "house"') + if cursor.fetchall() is []: + print("Empty db. Creating table...") + with open('house_schema.sql', 'r') as schemaFile: + schema = schemaFile.read() + + cursor.executescript(schema) + db.commit() + +except Error: + print("DB cursor issues: %s" % Error) + exit(-1) + +# Main loop +print("Logging...") +while True: + now = datetime.now() + temp = bme.temperature + pressure = bme.pressure + humidity = bme.humidity + try: + cursor.execute('INSERT INTO house (date, temperature, humidity, pressure, deviceId) VALUES (?, ?, ?, ?, ?)', + [now, temp, humidity, pressure, deviceId]) + db.commit() + + except Error: + retryFail = False + for i in range(0, 5): + try: + cursor.execute('INSERT INTO house (date, temperature, humidity, pressure, deviceId) VALUES (?, ?, ?, ?, ?)', + [now, temp, humidity, pressure, deviceId]) + db.commit() + retryFail = False + break + + except Error: + retryFail = True + + time.sleep(5) + + if retryFail: + print("Unable to update db. Assuming the worst") + exit(-1) + time.sleep(300) + diff --git a/sensor.py b/sensor.py index b21d51f..56c9594 100644..100755 --- a/sensor.py +++ b/sensor.py @@ -1,12 +1,19 @@ +#!/usr/bin/env python3 + # imports import sqlite3 -from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash +from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, json, Response from contextlib import closing -from datetime import datetime -import pygal +from datetime import datetime, timedelta +from dateutil import parser + + +from io import BytesIO +from matplotlib.figure import Figure +import matplotlib.dates as mdates # configuration -DATABASE = '/tmp/sensors.db' +DATABASE = 'sensors.db' DEBUG = True SECRET_KEY = 'development_key' @@ -19,14 +26,46 @@ app.config.from_object(__name__) # initialize database def init_db(): with closing(connect_db()) as db: - with app.open_resource('schema.sql', mode='r') as f: - db.cursor().executescript(f.read()) + #with app.open_resource('schema.sql', mode='r') as f: + # db.cursor().executescript(f.read()) db.commit() # connect to database def connect_db(): return sqlite3.connect(app.config['DATABASE']) +# pull data between two dates +# startdate is for some date farther in the past than enddate +def dates_between(startdate, enddate, table, deviceId): + if table is 'weather': + subset = g.db.execute('SELECT date, temperature, humidity, pressure FROM weather WHERE date BETWEEN ? AND ?', [startdate, enddate]) + else: + subset = g.db.execute('SELECT date, temperature, humidity, pressure, battery FROM house WHERE (date BETWEEN ? AND ?) AND deviceId = ?', [startdate, enddate, deviceId]) + return subset + +def getCurrent(requestArgs): + # the args should be formatted YYYYMMDDHHMMSS + start = requestArgs.get('start') + end = requestArgs.get('end') + table = 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 table is None: + table = 'house' + if deviceId is None: + deviceId = 1 + + return dates_between(starttime, endtime, table, deviceId) + # handle requests @app.before_request def before_request(): @@ -41,47 +80,238 @@ def teardown_request(exception): # viewing database @app.route('/') def show_entries(): - cur = g.db.execute('SELECT date, temperature, humidity, pressure FROM weather ORDER BY id desc') + enddate = datetime.now() + startdate = enddate - timedelta(days=1) + deviceId = 1 + cur = g.db.execute('SELECT date, temperature, humidity, pressure FROM house WHERE (date BETWEEN ? AND ?) AND deviceId = ? ORDER BY id desc ', [startdate, enddate, deviceId]) entries = [dict(date=row[0], temperature=row[1], humidity=row[2], pressure=row[3]) for row in cur.fetchall()] + temp = "{:.1f}".format(entries[0].get("temperature") * 1.8 + 32) + humid = "{:.1f}".format(entries[0].get("humidity")) + current = dict(temperature=temp, humidity=humid) + + return render_template('show_entries.html', log=entries, current=current, devId=1) - return render_template('show_entries.html', weather=entries) +@app.route('/bedroom') +def bedroom_entries(): + enddate = datetime.now() + startdate = enddate - timedelta(days=1) + deviceId = 1 + cur = g.db.execute('SELECT date, temperature, humidity, pressure FROM house WHERE (date BETWEEN ? AND ?) AND deviceId = ? ORDER BY id desc ', [startdate, enddate, deviceId]) + entries = [dict(date=row[0], temperature=row[1], humidity=row[2], pressure=row[3]) for row in cur.fetchall()] + temp = "{:.1f}".format(entries[0].get("temperature") * 1.8 + 32) + humid = "{:.1f}".format(entries[0].get("humidity")) + current = dict(temperature=temp, humidity=humid) + + return render_template('show_entries.html', log=entries, current=current, devId=1) + +@app.route('/mainfloor') +def mainfloor_entries(): + enddate = datetime.now() + startdate = enddate - timedelta(days=1) + deviceId = 2 + cur = g.db.execute('SELECT date, temperature, humidity, pressure FROM house WHERE (date BETWEEN ? AND ?) AND deviceId = ? ORDER BY id desc ', [startdate, enddate, deviceId]) + entries = [dict(date=row[0], temperature=row[1], humidity=row[2], pressure=row[3]) for row in cur.fetchall()] + temp = "{:.1f}".format(entries[0].get("temperature") * 1.8 + 32) + humid = "{:.1f}".format(entries[0].get("humidity")) + current = dict(temperature=temp, humidity=humid) + + return render_template('show_entries.html', log=entries, current=current, devId=2) + +@app.route('/interactive') +def interactive(): + return render_template('interactive.html') # drawing graphs -@app.route('/tgraph.svg') +@app.route('/tgraph.png', methods=['GET']) def draw_t_graph(): - cur = g.db.execute('SELECT date, temperature, humidity, pressure FROM weather ORDER BY id desc') + cur = getCurrent(request.args) + data = cur.fetchall() + dates = [] + tempsC = [] + tempsF = [] - datetimeline = pygal.DateTimeLine( - x_label_rotation=30, truncate_label=-1, - x_value_formatter=lambda dt: dt.strftime('%d, %b %Y %I:%M %p') - ) - datetimeline.add("Temp", [(datetime.strptime(row[0], '%Y-%m-%d %H:%M:%S.%f'), float(row[1])) for row in cur.fetchall()]) + for row in data: + dates.append(parser.parse(row[0])) + tempsC.append(row[1]) + tempsF.append(row[1] * 1.8 + 32) - return datetimeline.render_response() + # Generate the figure **without using pyplot**. + 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(dates, tempsC, color="C1") + ax.set_xlabel("Timestamps") + ax.set_ylabel("Celsius") + ax.set_title("Temperature") + bx.plot(dates, tempsF, visible=False) + bx.set_ylabel("Fahrenheit") + # Save it to a temporary buffer. + buf = BytesIO() + fig.savefig(buf, format="png") -@app.route('/hgraph.svg') + return Response(buf.getvalue(), mimetype='image/png') + +@app.route('/hgraph.png', methods=['GET']) def draw_h_graph(): - cur = g.db.execute('SELECT date, temperature, humidity, pressure FROM weather ORDER BY id desc') + data = getCurrent(request.args) + dates = [] + humidities = [] + + for row in data: + dates.append(parser.parse(row[0])) + humidities.append(row[2]) + + # Generate the figure **without using pyplot**. + 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(dates, humidities, color="C0") + ax.set_xlabel("Timestamps") + ax.set_ylabel("Percent") + ax.set_title("Relative Humidity") + # Save it to a temporary buffer. + buf = BytesIO() + fig.savefig(buf, format="png") + + return Response(buf.getvalue(), mimetype='image/png') + +@app.route('/htgraph.png', methods=['GET']) +def draw_ht_graph(): + cur = getCurrent(request.args) + data = cur.fetchall() + dates = [] + humidities = [] + #tempsC = [] + tempsF = [] + + for row in data: + dates.append(parser.parse(row[0])) + # tempsC.append(row[1]) + tempsF.append(row[1] * 1.8 + 32) + humidities.append(row[2]) + + fig = Figure() + #fig.subplots_adjust(right=0.75) + ax = fig.subplots() + bx = ax.twinx() + #cx = ax.twinx() - datetimeline = pygal.DateTimeLine( - x_label_rotation=30, truncate_label=-1, - x_value_formatter=lambda dt: dt.strftime('%d, %b %Y %I:%M %p') - ) - datetimeline.add("Humidity", [(datetime.strptime(row[0], '%Y-%m-%d %H:%M:%S.%f'), float(row[2])) for row in cur.fetchall()]) + #cx.spines["right"].set_position(("axes", 1.2)) + #cx.set_frame_on(True) + #cx.patch.set_visible(False) + #for sp in cx.spines.values(): + # sp.set_visible(False) + #cx.spines["right"].set_visible(True) - return datetimeline.render_response() + 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) + #cx.xaxis.set_major_locator(locator) + #cx.xaxis.set_major_formatter(formatter) -@app.route('/pgraph.svg') + hline, = ax.plot(dates, humidities, color="C0", label="Relative Humidity") + ax.set_xlabel("Timestamps") + ax.set_ylabel("Percent") + ax.set_title("Humidity and Temperature") + tline, = bx.plot(dates, tempsF, color="C1", label="Temperature") + bx.set_ylabel("Fahrenheit") + #cx.plot(dates, tempsC, visible=False) + #cx.set_ylabel("Celsius") + + lines = [hline, tline] + ax.legend(lines, [l.get_label() for l in lines]) + + buf = BytesIO() + fig.savefig(buf, format="png") + + return Response(buf.getvalue(), mimetype='image/png') + +@app.route('/pgraph.png', methods=['GET']) def draw_p_graph(): - cur = g.db.execute('SELECT date, temperature, humidity, pressure FROM weather ORDER BY id desc') + data = getCurrent(request.args) + dates = [] + pressures = [] + + for row in data: + dates.append(parser.parse(row[0])) + pressures.append(row[3]) + + # Generate the figure **without using pyplot**. + 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(dates, pressures, color="C2") + ax.set_xlabel("Timestamps") + ax.set_ylabel("hPa") + ax.set_title("Pressure") + # Save it to a temporary buffer. + buf = BytesIO() + fig.savefig(buf, format="png") + + return Response(buf.getvalue(), mimetype='image/png') + +@app.route('/bgraph.png', methods=['GET']) +def draw_b_graph(): + data = getCurrent(request.args) + dates = [] + batteries = [] + + for row in data: + dates.append(parser.parse(row[0])) + batteries.append(row[4]) + + # Generate the figure **without using pyplot**. + 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(dates, batteries, color="C3") + ax.set_xlabel("Timestamps") + ax.set_ylabel("Volts") + ax.set_title("Battery Voltage") + # Save it to a temporary buffer. + buf = BytesIO() + fig.savefig(buf, format="png") - datetimeline = pygal.DateTimeLine( - x_label_rotation=30, truncate_label=-1, - x_value_formatter=lambda dt: dt.strftime('%d, %b %Y %I:%M %p') - ) - datetimeline.add("Pressure", [(datetime.strptime(row[0], '%Y-%m-%d %H:%M:%S.%f'), float(row[3])) for row in cur.fetchall()]) + return Response(buf.getvalue(), mimetype='image/png') - return datetimeline.render_response() +## Database Retrieval Route +# +# Sends the information in the database in JSON format +@app.route('/get_database') +def get_database(): + entries = getCurrent(request.args) + empList = [] + for emp in entries: + empDict = { + 'date': emp[0], + 'temperature': emp[1], + 'humidity': emp[2], + 'pressure': emp[3] } + empList.append(empDict) + response = app.response_class( + response=json.dumps(empList), + status=200, + mimetype='application/json' + ) + return response # adding entries to database @@ -100,5 +330,39 @@ def add_data(): return render_template('show_entries.html'), 200 +@app.route('/json_data', methods=['POST']) +def add_json_data(): + mkey = request.args.get('key') + if mkey != SECRET_KEY: + abort(401) + + # Validate the request body contains JSON + if request.is_json: + # Parse the JSON into a Python dictionary + req = request.get_json() + + # Print the dictionary + print(req) + + try: + temp = req['temperature'] + humidity = req['humidity'] + pressure = req['pressure'] + battery = req['battery'] + deviceId = req['deviceId'] + now = datetime.now() + g.db.execute('INSERT INTO house (date, temperature, humidity, pressure, deviceId, battery) VALUES (?, ?, ?, ?, ?, ?)', [now, temp, humidity, pressure, deviceId, battery]) + g.db.commit() + except: + print("bad json") + + # Return a string along with an HTTP status code + return "JSON received!", 200 + + else: + # The request body wasn't JSON so return a 400 HTTP status code + return "Request was not JSON", 400 + + if __name__ == '__main__': - app.run() + app.run(host='0.0.0.0') diff --git a/static/interactive.js b/static/interactive.js new file mode 100644 index 0000000..de44670 --- /dev/null +++ b/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/static/pagedown.css b/static/pagedown.css new file mode 100644 index 0000000..7f9c3e0 --- /dev/null +++ b/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/templates/interactive.html b/templates/interactive.html new file mode 100644 index 0000000..1756865 --- /dev/null +++ b/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/templates/layout.html b/templates/layout.html index 2a06690..a49f142 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,12 +1,12 @@ <!doctype html> -<title>Weather</title> -<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}"> +<title>Sensor</title> +<link rel=stylesheet type=text/css href="{{ url_for('static', filename='pagedown.css') }}"> <div class=page> - <h1>Weather</h1> - <div class=metanav> - </div> - {% for message in get_flashed_messages() %} - <div class=flash>{{ message }}</div> - {% endfor %} + <h1>Sensor Readings</h1> + <hr> + <a href="bedroom">Bedroom</a> <a href="mainfloor">Mainfloor</a> <a href="interactive">Interactive</a> + <hr> + <main> {% block body %}{% endblock %} + </main> </div> diff --git a/templates/show_entries.html b/templates/show_entries.html index 572f360..86451df 100644 --- a/templates/show_entries.html +++ b/templates/show_entries.html @@ -1,19 +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/svg+xml" src="/tgraph.svg" /> + <embed type="image/png" src="/htgraph.png?deviceId={{ devId }}" /> </figure> <figure> - <embed type="image/svg+xml" src="/hgraph.svg" /> + <embed type="image/png" src="/pgraph.png?deviceId={{ devId }}" /> </figure> + {% if devId == 2 %} <figure> - <embed type="image/svg+xml" src="/pgraph.svg" /> + <embed type="image/png" src="/bgraph.png?deviceId={{ devId }}" /> </figure> - <ul class=weather> - {% for entry in weather %} - <li><h2>{{ entry.date }}</h2>Temp: {{ entry.temperature }}, Humidity: {{ entry.humidity }}, Pressure: {{ entry.pressure }} - {% else %} - <li><em>No entries here so far</em> + {% 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 %} - </ul> + </table> + </p> {% endblock %} |