From 0a7b0291411c7eedda7631b1c46f4cff79298b1f Mon Sep 17 00:00:00 2001 From: chlub Date: Tue, 9 Dec 2025 09:07:07 +0100 Subject: [PATCH] database access for rooms and mqtt client + generator --- .gitignore | 3 +- components.py | 21 ++++++ db_connect.py | 56 +++++++++++--- mqtt_client.py | 53 +++++++++++++ mqtt_data_generator.py | 63 ++++++++++++++++ requirements.txt | Bin 646 -> 2198 bytes static/scripts.js | 43 +++++++---- static/styles.css | 6 ++ templating.py | 50 +++++++++---- user_handling.py | 41 ++++++++++ web.py | 141 ++++++++++++++++++++++++----------- web/base.html.jinja | 26 +++++-- web/landing.html.jinja | 4 +- web/login.html.jinja | 25 +++++++ web/room.html.jinja | 33 ++++++++ web/value_display.html.jinja | 17 +++++ 16 files changed, 486 insertions(+), 96 deletions(-) create mode 100644 components.py create mode 100644 mqtt_client.py create mode 100644 mqtt_data_generator.py create mode 100644 user_handling.py create mode 100644 web/login.html.jinja create mode 100644 web/room.html.jinja create mode 100644 web/value_display.html.jinja diff --git a/.gitignore b/.gitignore index d995958..20eb121 100644 --- a/.gitignore +++ b/.gitignore @@ -175,4 +175,5 @@ cython_debug/ .pypirc ### CUSTOM GITIGNORES -db_creds.csv \ No newline at end of file +db_creds.csv +*key* \ No newline at end of file diff --git a/components.py b/components.py new file mode 100644 index 0000000..cdc9b6d --- /dev/null +++ b/components.py @@ -0,0 +1,21 @@ +from flask_login import UserMixin, AnonymousUserMixin + +from collections import namedtuple + +class User(UserMixin): + def __init__(self, id, username): + self.id = id + self.username = username + +class AnonymousUser(AnonymousUserMixin): + def __init__(self): + self.id = 1 + self.username = "nouser" + +UserInfo = namedtuple('UserInfo',['id', 'name']) +RoomInfo = namedtuple('RoomInfo',['name', 'shortcode', 'roomdata']) +# state is an int not in db +# 0 - value valid +# 1 - value late +# 2 - value missing +SensorInfo = namedtuple('SensorInfo',['state','type','timestamp','reading']) diff --git a/db_connect.py b/db_connect.py index 66b47af..c619be4 100644 --- a/db_connect.py +++ b/db_connect.py @@ -4,7 +4,7 @@ import logging from typing import Generator import datetime -from templating import SensorInfo +from components import SensorInfo # main database connector class # permission validation is not performed on this class @@ -14,7 +14,7 @@ class DatabaseConnect: with open("db_creds.csv","r") as f: credentials = f.read().split(",") try: - conn = mariadb.connect( + self.conn = mariadb.connect( user=credentials[0], password=credentials[1], host=credentials[2], @@ -24,23 +24,39 @@ class DatabaseConnect: except mariadb.Error as e: logging.fatal(f"Error connecting to database: {e}") return - self.cursor = conn.cursor() + self.cursor = self.conn.cursor() + self.credentials = f.read().split(",") super().__init__() - def create_user(self, username: str, pwd: int) -> None: - self.cursor.execute("INSERT INTO Users (username, pwd) VALUE (?,?)",(username, pwd)) - logging.info(f"Created user {username}") + def emit_log(self, severity:int, message: str) -> None: + self.cursor.execute("INSERT INTO `log` (`timestamp`, `type` ,`message`) VALUES (?,?,?);",(datetime.datetime.now(), severity, message)) + match severity: + case 1: + logging.error(f"Log emitted: {message}") + case 2: + logging.warning(f"Log emitted: {message}") + case 3: + logging.info(f"Log emitted: {message}") + + def create_user(self, username: str, salt: bytes, key: bytes) -> None: + self.cursor.execute("INSERT INTO `users` (`username`, `salt`, `key`) VALUES (?,?,?)",(username, salt, key)) + self.conn.commit() + self.emit_log(2, f"Created user {username}") def delete_user(self, id) -> None: self.cursor.execute("DELETE FROM Users WHERE ID = ?",(id)) - logging.info(f"Deleted user {id}") + self.emit_log(2, f"Deleted user ID {id}") def display_users(self) -> tuple[tuple[int, str, int]]: self.cursor.execute("SELECT * FROM Users") return self.cursor.fetchall() def username_from_id(self, user_id): - self.cursor.execute("SELECT users.`username` FROM users WHERE `ID` = 1;") + self.cursor.execute("SELECT users.`username` FROM users WHERE `ID` = ?;", (user_id,)) + return self.cursor.fetchone() + + def user_by_username(self, username) -> tuple[int, str, bytes, bytes] | None: + self.cursor.execute("SELECT * FROM users WHERE `username` = ?;", (username,)) return self.cursor.fetchone() def view_valid_rooms(self, user_id) -> list[tuple[str, str, int]]: @@ -61,9 +77,9 @@ class DatabaseConnect: # self.cursor.execute("SELECT rooms.`ID` from rooms WHERE rooms.shortname = ?;",(shortname,)) # return self.cursor.fetchone() - def get_sensors_in_room_shortname(self, shortname) -> list[int]: - self.cursor.execute("SELECT Sensors.`ID` from Sensors LEFT JOIN rooms ON Sensors.`roomID` = rooms.ID WHERE rooms.shortname = ?;",(shortname,)) - return [x[0] for x in self.cursor.fetchall()] + def get_sensors_in_room_shortname(self, shortname) -> list[tuple[int, int]]: + self.cursor.execute("SELECT Sensors.`ID`, Sensors.`Type` from Sensors LEFT JOIN rooms ON Sensors.`roomID` = rooms.ID WHERE rooms.shortname = ?;",(shortname,)) + return [x for x in self.cursor.fetchall()] def get_sensors_in_room_id(self, roomid) -> list[tuple[int, int]]: self.cursor.execute("SELECT Sensors.`ID`, Sensors.`Type` from Sensors LEFT JOIN rooms ON Sensors.`roomID` = rooms.ID WHERE rooms.`ID` = ?;",(roomid,)) @@ -86,6 +102,24 @@ class DatabaseConnect: def get_sensor_type(self, sensor_ID): self.cursor.execute("SELECT types.`type_desc` from types LEFT JOIN Sensors ON types.`ID` = Sensors.`type` WHERE Sensors.`ID` = ?;",(sensor_ID,)) return self.cursor.fetchone()[0] + + def get_sensor_from_topic(self, topic: str) -> int | None: + self.cursor.execute("SELECT Sensors.`ID` FROM Sensors WHERE Sensors.`mqttTopic` = ?;", (topic,)) + return self.cursor.fetchone()[0] + + + def get_unit_for_type(self, type_id: int) -> str | None: + self.cursor.execute("SELECT `unit` FROM `types` WHERE `ID` = ?;", (type_id,)) + return self.cursor.fetchone()[0] + + def get_all_topics(self) -> list[str]: + self.cursor.execute("SELECT `mqttTopic` FROM `sensors`;") + return [x[0] for x in self.cursor.fetchall()] + + def create_sensor_reading(self, sensor_id: int, timestamp: datetime.datetime, reading: float) -> None: + self.cursor.execute("INSERT INTO `readings` (`sensorID`, `Timestamp`, `reading`) VALUES (?,?,?);",(sensor_id, timestamp, reading)) + self.conn.commit() + self.emit_log(3, f"Inserted reading for sensor ID {sensor_id} at {timestamp}: {reading}") if __name__ == "__main__": a = DatabaseConnect() diff --git a/mqtt_client.py b/mqtt_client.py new file mode 100644 index 0000000..1f6c8a4 --- /dev/null +++ b/mqtt_client.py @@ -0,0 +1,53 @@ +from fastapi import FastAPI +from fastapi_mqtt import MQTTConfig, FastMQTT +from gmqtt import Client as MQTTClient +import fastapi_mqtt as mqtt +import uvicorn + +from contextlib import asynccontextmanager +import datetime + +import db_connect + +mqtt_config = mqtt.MQTTConfig( + host="localhost", + port=1883, + keepalive=60, +) + +fast_mqtt = mqtt.FastMQTT(config=mqtt_config) + +@asynccontextmanager +async def _lifespan(_app: FastAPI): + await fast_mqtt.mqtt_startup() + yield + await fast_mqtt.mqtt_shutdown() + +app = FastAPI(lifespan=_lifespan) + +topics = db_connect.DatabaseConnect().get_all_topics() + +@fast_mqtt.on_connect() +def on_connect(client: MQTTClient, flags, rc, properties): + for (topic) in topics: + client.subscribe(topic, qos=0) + print("Connected: ", client, flags, rc, properties) + db_connect.DatabaseConnect().emit_log(3, "MQTT client connected and subscribed to topics.") + +@fast_mqtt.on_message() +def on_message(client: MQTTClient, topic: str, payload: bytes, qos, properties): + # find sensor ID from topic + id = db_connect.DatabaseConnect().get_sensor_from_topic(topic) + if id is not None: + data = payload.decode().split(",") + db_connect.DatabaseConnect().create_sensor_reading(id, datetime.datetime.fromisoformat(data[0]), float(data[1])) + else: + db_connect.DatabaseConnect().emit_log(1, f"Received MQTT message for unknown topic {topic}") + +@fast_mqtt.on_disconnect() +def on_disconnect(client: MQTTClient, packet, exc=None): + print("Disconnected: ", client, packet, exc) + db_connect.DatabaseConnect().emit_log(2, "MQTT client disconnected.") + +if __name__ == "__main__": + uvicorn.run(app, port=8001) \ No newline at end of file diff --git a/mqtt_data_generator.py b/mqtt_data_generator.py new file mode 100644 index 0000000..327f8a5 --- /dev/null +++ b/mqtt_data_generator.py @@ -0,0 +1,63 @@ +# test script to generate sensor data for testing +# made to reprsent real world data + +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi_mqtt import MQTTConfig, FastMQTT +from gmqtt import Client as MQTTClient +import fastapi_mqtt as mqtt +import uvicorn + +import datetime +import logging +import random +import asyncio + +mqtt_config = mqtt.MQTTConfig( + host="localhost", + port=1883, + keepalive=60, +) + +fast_mqtt = mqtt.FastMQTT(config=mqtt_config) + +@asynccontextmanager +async def _lifespan(_app: FastAPI): + await fast_mqtt.mqtt_startup() + asyncio.create_task(main()) + yield + await fast_mqtt.mqtt_shutdown() + +app = FastAPI(lifespan=_lifespan) + + +# weights for hourly temps, based on LKTB sample on nov, 4th 2025 +TIME_WEIGHTS = [ + 4, 3, 3, 2, 2, 2, 2, 1, 3, 6, 8, 9, 11, 12, 12, 12, 11, 10, 9, 9, 8, 8, 5, 5 +] +# weights for specific rooms, here inside/outside +ROOM_WEIGHTS = { + "outside": 0, + "inside": 10 +} + +@fast_mqtt.on_connect() +def connect(client: MQTTClient, flags, rc, properties): + print("Connected: ", client, flags, rc, properties) + +async def publish_sensor_data(inside: bool = True): + now = datetime.datetime.now() + base_temp = TIME_WEIGHTS[now.hour] + temp = base_temp + ROOM_WEIGHTS["inside" if inside else "outside"] + random.gauss(0,0.5) + logging.info(f"{now.isoformat()},{temp}") + fast_mqtt.publish("210/temp" if inside else "000/temp", f"{now.isoformat()},{temp}", qos=0) + +async def main(): + while True: + await publish_sensor_data(inside=False) + await publish_sensor_data(inside=True) + await asyncio.sleep(300) + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + uvicorn.run(app) diff --git a/requirements.txt b/requirements.txt index 742fec1148a69224d89b0c5c98390bfa607b9b35..f5cba1b6b5827a1ffde79d9d636fe5e2e3f824a6 100644 GIT binary patch literal 2198 zcmezWFOeaSA&()Sp@bokp@booA%#JgA%!8IA(_FJ!Ir^*K@W^g7w&4SWXNR52g{i-=rI^F7{O(e7;+dg8S)si8B!UFz$y$G^cXDR^2uQF z5{68MG=@yDyb*%|gAs!%*c^~rkZB-$k{QyF)IeNp0CsUQ*p6g|Y_NU{20aF2B=hqb zav1U%iWm|Zav2iADnR}Pxzh})y9n&N5{7(+bg;Sth75*Euu2mKGq6b@6G3iBVaQ`B zW+-5&WGG?CV90041FHi07Gx{LHK|~GG8u9hbQ#JR62Wdu0f#%t6`=42sYkdljUkbt z7#uSN&=3H*59Di*8zE+*s?lYD_!wjsNG%8(GC+KTO=UhqDLBSabwPXv@)f$CT!uo1 z5{44E-$3p~cLyXcK{g|N?#7S<_6sB)Kt2S;pAk5e5puc=KH!iBr3#o2KyEPxt4n7< zwjE?XBBf<87%~`wT?G;c`52T6Kt9L-ySIQL864B8U^O7SLFz2Q=E78fVi}YoP*buY zikb?revn=x29WO{vY8B^*oUMbh~Hr1B@D$3pcu|$NC&58PvbrvyXG9)sjFeHKHK|X+BNX~?)$Oh-!a)vwxU4~4C5(ZsxdIW_e zB)!1G#FrtFp@<=yp_HM3A($bNA&mhNE+AV$euIQ}E<*}KDMJxM4%kMJILJmwoaKQ{ zC}+q3r(%SUK(PR`C6A$$As6lfkY7ROL)=@ykjMbam5B`LV4tUh?SSNMNQi(`1I>+~R08oMD9%B-Fqr{lLJ>nTINU*D0CIs515AG<*z7z8Q27EW z>p=2`;PS~79OocYAaMs#6OWRHK&nj`Kp_HA9l%h@07{{$3?MTRZZ-s$Xpm3?nFuQ# zK&33C#4QE80TRj}Qz0dw9zy{`J~;Qo!UIvVfb@b&Y>-`L;E)5k1Ed#{?ojQ?WhiCH z0owry5lHR@#U}`Z%2ZgGl`vF+O#_KR;sWAskl!L192tBWe8BRc&<5#+ge53tCNpG! zWegcWu>f)j#MLl4T?Sb0$_A%QP<(()Fl0dFVUXH%a9vacO^+ZIAbCjn1&T>f*n-Mr zU2wjFlqMkCOpweeX2@VjWyk@SuAqDeN~5_@*Xe<44@ep;X2@iKg4^iHM!VQ-CA+n&7EFGL1A+*lGAQ}{IAoYe|f56;P1y0ML8XP1Kas_hiLGm!Du1#Vv28RPE#6WQf zNiC%eWel0nv<<1XK&~cYV}GMk}_A(f$&0h04UIU8geBt-!LugN@a delta 53 zcmbOx*v88A|KG$KrHPMhCTB3YOpasDn*5DfZL$T+qRF>dxhB`JnoJg86Px^pO=|KH Kc7@3$97+H^#1pds diff --git a/static/scripts.js b/static/scripts.js index 4182906..b6d65b8 100644 --- a/static/scripts.js +++ b/static/scripts.js @@ -1,19 +1,30 @@ function tableSearch(){ - var table, input, tr, td, row_content; - table = document.getElementById("tableRooms"); - input = document.getElementById("tableRoomSearch").value.toUpperCase(); - tr = table.getElementsByTagName("tr"); + var table, input, tr, td, row_content; + table = document.getElementById("tableRooms"); + input = document.getElementById("tableRoomSearch").value.toUpperCase(); + tr = table.getElementsByTagName("tr"); - for (let i = 0; i < tr.length; i++) { - td = tr[i].getElementsByTagName("td")[0]; - if(td){ - row_content = td.textContent || td.innerText; - if (row_content.toUpperCase().indexOf(input) > -1) { - tr[i].style.display = ""; - } - else{ - tr[i].style.display ="none"; - } - } + for (let i = 0; i < tr.length; i++) { + td = tr[i].getElementsByTagName("td")[0]; + if(td){ + row_content = td.textContent || td.innerText; + if (row_content.toUpperCase().indexOf(input) > -1) { + tr[i].style.display = ""; } - } \ No newline at end of file + else{ + tr[i].style.display ="none"; + } + } + } +} + +function toggleRoomDropdown() { + document.getElementById("rooms-unrolled").style.display = "inline"; + document.getElementById("rooms-rolled").style.display = "none"; + +} + +function untoggleRoomDropdown() { + document.getElementById("rooms-rolled").style.display = "inline"; + document.getElementById("rooms-unrolled").style.display = "none"; +} diff --git a/static/styles.css b/static/styles.css index c7003c0..99eecda 100644 --- a/static/styles.css +++ b/static/styles.css @@ -90,3 +90,9 @@ tbody tr{ .rooms-table input{ width: 90%; } + +.current-readings .data-valid, +.current-readings .data-late, +.current-readings .data-missing{ + padding: 0.2em 1em; +} \ No newline at end of file diff --git a/templating.py b/templating.py index a99ef55..ce4404f 100644 --- a/templating.py +++ b/templating.py @@ -1,33 +1,27 @@ -from flask import url_for, Flask -from jinja2 import FileSystemLoader +from flask import url_for +from flask_login import current_user import tomllib -from os import sep -from collections import namedtuple def static_test_load(): with open("static_text.toml", "rb") as f: return tomllib.load(f) -UserInfo = namedtuple('UserInfo',['id', 'name']) -RoomInfo = namedtuple('RoomInfo',['name', 'shortcode', 'roomdata']) -# state is an int not in db -# 0 - value valid -# 1 - value late -# 2 - value missing -SensorInfo = namedtuple('SensorInfo',['state','type','timestamp','reading']) - # base class for inheriting other more specific pages class BasePage(): def __init__( self, jijna_env, + userinfo, + roominfo, target_path = "base.html.jinja" , statictext = None, ) -> None: self.env = jijna_env self.target = target_path self.rendervars = static_test_load() if statictext is None else statictext + self.rendervars["userinfo"] = userinfo + self.rendervars["roominfo"] = roominfo def render(self): url_for('static', filename="styles.css") @@ -44,6 +38,32 @@ class LandingPage(BasePage): target_path="landing.html.jinja", statictext=None, ) -> None: - super().__init__(jinja_env, target_path, statictext) - self.rendervars["userinfo"] = userinfo - self.rendervars["roominfo"] = roominfo + super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) + +class LoginPage(BasePage): + def __init__( + self, + jinja_env, + userinfo, + roominfo, + target_path="login.html.jinja", + statictext=None, + ) -> None: + super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) + +class RoomPage(BasePage): + def __init__( + self, + jinja_env, + userinfo, + roominfo, + current_room, + sensorinfolist, + fig, + target_path="room.html.jinja", + statictext=None, + ) -> None: + super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) + self.rendervars["current_room"] = current_room + self.rendervars["sensorinfolist"] = sensorinfolist + self.rendervars["fig"] = fig \ No newline at end of file diff --git a/user_handling.py b/user_handling.py new file mode 100644 index 0000000..652662c --- /dev/null +++ b/user_handling.py @@ -0,0 +1,41 @@ +from os import urandom +from typing import Tuple, Optional +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives.kdf.scrypt import Scrypt +import base64 + +with open("key.txt", "rb") as key_file: + key = key_file.read().strip() +f = Fernet(key) + +#TODO: properly set SQL blob sizes, they're constant +def __scrypt__(salt: bytes) -> Scrypt: + return Scrypt( + salt=salt, + length=32, + n=2**14, + r=8, + p=1, + ) + +def new_password(password: bytes) -> Tuple[bytes, bytes]: + salt = urandom(16) + key = base64.urlsafe_b64encode(__scrypt__(salt).derive(password)) + f = Fernet(key) + out = f.encrypt(password) + return salt, out + +def verify_password(stored_salt: bytes, stored_key: bytes, provided_password: bytes) -> bytes: + key = base64.urlsafe_b64encode(__scrypt__(stored_salt).derive(provided_password)) + f = Fernet(key) + return f.decrypt(stored_key) + +if __name__ == "__main__": + # helper script for inserting new users + import db_connect + username = input("Enter new username: ") + password = input("Enter new password: ").encode() + db = db_connect.DatabaseConnect() + salt, key = new_password(password) + db.create_user(username, salt, key) + print(f"Created user {username}") \ No newline at end of file diff --git a/web.py b/web.py index 195b0d8..c3a9aa7 100644 --- a/web.py +++ b/web.py @@ -1,19 +1,33 @@ -from db_connect import DatabaseConnect - -from flask import Flask, abort, session +from flask import Flask, abort, request, flash, redirect, get_flashed_messages +from flask_login import LoginManager, login_user, logout_user, login_required, current_user from jinja2 import FileSystemLoader, Template import plotly.express as px from plotly.subplots import make_subplots import plotly.graph_objects as go -import logging -from os import sep import datetime +from db_connect import DatabaseConnect import templating +import user_handling +import components app = Flask(__name__) -db = DatabaseConnect() +with open("session_key.txt","rb") as f: + app.secret_key = f.read().strip() + + +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.anonymous_user = components.AnonymousUser + +@login_manager.user_loader +def load_user(user_id): + user_data = DatabaseConnect().username_from_id(user_id) + if user_data: + return components.User(user_id, user_data[0]) + return None + Flask.jinja_options["loader"] = FileSystemLoader("web") env = app.create_jinja_environment() @@ -21,69 +35,106 @@ env = app.create_jinja_environment() DISPLAYEDTYPES = 2 TDLATE = datetime.timedelta(hours=8) -# before and after -TYPEUNITS = [ - "°C", - "%" -] + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + + user_data = DatabaseConnect().user_by_username(username) + if user_data: + user_id, _, salt, key = user_data + if user_handling.verify_password(salt, key, password.encode()): + user = components.User(user_id, username) + login_user(user) + DatabaseConnect().emit_log(3, f"User {username} logged in successfully.") + return redirect("/") + DatabaseConnect().emit_log(2, f"Failed login attempt for user {username}.") + flash("Invalid username or password.") + + return templating.LoginPage(env, current_user, roominfo=gather_room_info()).render() + +@app.route("/logout") +@login_required +def logout(): + logout_user() + return redirect("/") @app.route("/") def index(): - # no user handling yet so we get user of ID 1 (not logged in) - active_user = 1 - #populate userinfo - user_info = templating.UserInfo(id=active_user, name=db.username_from_id(active_user)[0]) + rooms_info = gather_room_info() + + out_html = templating.LandingPage(env, current_user,rooms_info) + return out_html.render() + +def gather_room_info(): rooms_info = [] - for room in db.view_valid_rooms(active_user): + for room in DatabaseConnect().view_valid_rooms(current_user.id): sensor_info = [] - sensors_avail = db.get_sensors_in_room_id(room[2]) + sensors_avail = DatabaseConnect().get_sensors_in_room_id(room[2]) for i in range(DISPLAYEDTYPES): sensors_with_type = [x for x in sensors_avail if x[1] == i] # TODO: handle more than one sensor in one room if sensors_with_type: - reading = db.get_sensorsinfo_by_sensorid(sensors_with_type[0][0]) + reading = DatabaseConnect().get_sensorsinfo_by_sensorid(sensors_with_type[0][0]) if reading is None: - sensor_info.append(templating.SensorInfo(2,None, None, "")) + sensor_info.append(components.SensorInfo(2,None, None, "")) continue - reading = templating.SensorInfo(1 if reading.timestamp + TDLATE < datetime.datetime.now() else 0, + reading = components.SensorInfo(1 if reading.timestamp + TDLATE < datetime.datetime.now() else 0, reading.type, reading.timestamp, - f"{reading.reading}{TYPEUNITS[i]}") + f"{reading.reading:.2f}{DatabaseConnect().get_unit_for_type(i) or ''}") sensor_info.append(reading) else: - sensor_info.append(templating.SensorInfo(2,None, None, "")) - rooms_info.append(templating.RoomInfo(room[0], room[1], sensor_info)) - - out_html = templating.LandingPage(env, user_info,rooms_info) - return out_html.render() + sensor_info.append(components.SensorInfo(2,None, None, "")) + rooms_info.append(components.RoomInfo(room[0], room[1], sensor_info)) + return rooms_info @app.route('/room/') def room_page(room_name=None): - if not db.user_has_room_perms(1,room_name): + if not DatabaseConnect().user_has_room_perms(current_user.id,room_name): abort(403) - sensor_list = db.get_sensors_in_room_shortname(room_name) - fig = make_subplots(rows=1, cols=len(sensor_list)) - for idx, sensorID in enumerate(sensor_list): - lst = [x for x in db.get_sensor_data(sensorID)] - fig.add_trace( - go.Scatter( - x = [x[0] for x in lst], - y = [x[1] for x in lst], - name=db.get_sensor_type(sensorID)), - row = 1, - col = idx + 1 - ) - fig.update_layout(title_text=f"Available Devices in room {room_name}:") + roominfos = gather_room_info() + current_room = next((x for x in roominfos if x.shortcode == room_name), None) + + #TODO: if statement to failsafe room with no sensors + sensor_list = DatabaseConnect().get_sensors_in_room_shortname(room_name) + fig = make_subplots(rows=1, cols=len(sensor_list)) + lastest_readings = [] + for idx, (sensorID, sensor_type) in enumerate(sensor_list): + lst = [x for x in DatabaseConnect().get_sensor_data(sensorID)] + if lst: + lastest_readings.append(components.SensorInfo( + 1 if lst[0][0] + TDLATE < datetime.datetime.now() else 0, + sensor_type, + lst[0][0], + f"{lst[0][1]:.2f}{DatabaseConnect().get_unit_for_type(sensor_type) or ''}" + )) + fig.add_trace( + go.Scatter( + x = [x[0] for x in lst], + y = [x[1] for x in lst], + name=DatabaseConnect().get_sensor_type(sensorID)), + row = 1, + col = idx + 1 + ) + + fig.update_layout(title_text=f"Available Devices in room {room_name}:") + + return templating.RoomPage( + env, + current_user, + roominfos, + current_room, + lastest_readings, + fig.to_html(full_html=False, include_plotlyjs='cdn') + ).render() - template_path = f'web{sep}room_template.html' - px_jinja_data = {"fig":fig.to_html(full_html=False)} - with open(template_path,'r') as template_file: - j2_template = Template(template_file.read()) - return j2_template.render(px_jinja_data) if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/web/base.html.jinja b/web/base.html.jinja index e90d648..e99efd5 100644 --- a/web/base.html.jinja +++ b/web/base.html.jinja @@ -13,8 +13,18 @@