# 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.isoformat(" ", "seconds"), endtime.isoformat(" ", "seconds"), 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 row['humidity'] is not None: 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 row['humidity'] is not None: 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 row['temperature'] is not None and row['humidity'] is not None: 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 row['pressure'] is not None: 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 row['batteryCharge'] is not None: 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 row['uv_intensity'] is not None and row['lux'] is not None: 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 row['light'] is not None: 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')