This commit is contained in:
Chiara 2025-06-15 14:56:30 +02:00
commit d094982b2c
20 changed files with 2317 additions and 154 deletions

2
.gitignore vendored
View file

@ -1 +1 @@
/webserver/plots/ /webserver/static/plots/

View file

@ -81,17 +81,33 @@ Data should be sent and received via MQTT to an ESP32. The Pi acts as a sensor g
- `main.py` - `main.py`
The main file for the project The main file for the project
- `The Graphs`
https://ron.sh/creating-real-time-charts-with-fastapi/
### Git Help ### Git Help
![A Git Cheatsheet](gitHelp.png "Git Cheatsheet") ![A Git Cheatsheet](images/gitHelp.png "Git Cheatsheet")
### MarkDown Help ### MarkDown Help
![A MarkDown Cheatsheet](mdHelp.png "MarkDown Cheatsheet") ![A MarkDown Cheatsheet](images/mdHelp.png "MarkDown Cheatsheet")
### Html Help ### Html Help
![A Html Cheatsheet](htmlHelp.png "Html Cheatsheet") ![A Html Cheatsheet](images/htmlHelp.png "Html Cheatsheet")
### Css Help
![A Css Cheatsheet](images/cssHelp.png "Css Cheatsheet")
### JavaScript Help
![A JavaScript Cheatsheet](images/javascriptHelp.png "JavaScript Cheatsheet")
### Jinja Help
![A Jinja Cheatsheet](images/jinjaHelp.png "Jinja Cheatsheet")
## Arbeitsaufträge / Aufgabeneinteilung ## Arbeitsaufträge / Aufgabeneinteilung

BIN
images/cssHelp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

View file

Before

Width:  |  Height:  |  Size: 521 KiB

After

Width:  |  Height:  |  Size: 521 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 701 KiB

After

Width:  |  Height:  |  Size: 701 KiB

Before After
Before After

BIN
images/javascriptHelp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

BIN
images/jinjaHelp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

View file

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Before After
Before After

12
main.py
View file

@ -0,0 +1,12 @@
# Copyright 2025 Kieler, Chiara
#
# Licensed under the AGPLv3.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.gnu.org/licenses/
# What this file should do? It should:
# 1. Start all the Sensors
# 2. Start the Webserver
# 3. Somehow transfer data from the Sensors to the Webserver

View file

View file

@ -1,12 +0,0 @@
{ pkgs ? import <nixpkgs> {} }:
(pkgs.buildFHSUserEnv {
name = "venv";
targetPkgs = pkgs: (with pkgs; [
python312
python312Packages.pip
python312Packages.virtualenv
python312Packages.matplotlib
]);
runScript = "bash --init-file /etc/profile";
# runScript = "zsh";
}).env

100
webserver/data_big.json Normal file
View file

@ -0,0 +1,100 @@
{
"location": "Building B - Research Wing",
"sensors": [
{
"id": "temp-A12",
"type": "temperature",
"unit": "°C",
"readings": [
{ "ts": 1747814400, "value": 21.0 },
{ "ts": 1747818000, "value": 21.4 },
{ "ts": 1747821600, "value": 22.1 },
{ "ts": 1747825200, "value": 22.6 },
{ "ts": 1747828800, "value": 22.8 },
{ "ts": 1747832400, "value": 23.0 }
]
},
{
"id": "hum_B2",
"type": "humidity",
"unit": "%",
"readings": [
{ "ts": 1747814400, "value": 50.1 },
{ "ts": 1747818000, "value": 51.7 },
{ "ts": 1747821600, "value": 49.3 },
{ "ts": 1747825200, "value": 48.8 },
{ "ts": 1747828800, "value": 47.6 },
{ "ts": 1747832400, "value": 46.9 },
{ "ts": 1747836000, "value": 45.2 },
{ "ts": 1747839600, "value": 44.7 },
{ "ts": 1747843200, "value": 43.5 }
]
},
{
"id": "PRES-009",
"type": "pressure",
"unit": "hPa",
"readings": [
{ "ts": 1747814400, "value": 1011.0 },
{ "ts": 1747818000, "value": 1010.8 }
]
},
{
"id": "sensorX04",
"type": "CO2",
"unit": "ppm",
"readings": [
{ "ts": 1747814400, "value": 415 },
{ "ts": 1747818000, "value": 420 },
{ "ts": 1747821600, "value": 432 },
{ "ts": 1747825200, "value": 440 },
{ "ts": 1747828800, "value": 437 }
]
},
{
"id": "luxSensor-1",
"type": "light",
"unit": "lux",
"readings": [
{ "ts": 1747814400, "value": 300 },
{ "ts": 1747818000, "value": 450 },
{ "ts": 1747821600, "value": 600 },
{ "ts": 1747825200, "value": 550 },
{ "ts": 1747828800, "value": 500 },
{ "ts": 1747832400, "value": 480 },
{ "ts": 1747836000, "value": 470 },
{ "ts": 1747839600, "value": 460 },
{ "ts": 1747843200, "value": 455 },
{ "ts": 1747846800, "value": 440 },
{ "ts": 1747850400, "value": 430 },
{ "ts": 1747854000, "value": 420 }
]
},
{
"id": "vib_07",
"type": "vibration",
"unit": "mm/s",
"readings": [
{ "ts": 1747814400, "value": 0.05 },
{ "ts": 1747818000, "value": 0.06 },
{ "ts": 1747821600, "value": 0.08 },
{ "ts": 1747825200, "value": 0.07 },
{ "ts": 1747828800, "value": 0.09 },
{ "ts": 1747832400, "value": 0.1 },
{ "ts": 1747836000, "value": 0.12 }
]
},
{
"id": "motion-Z3",
"type": "motion",
"unit": "count",
"readings": [
{ "ts": 1747814400, "value": 3 },
{ "ts": 1747818000, "value": 1 },
{ "ts": 1747821600, "value": 0 },
{ "ts": 1747825200, "value": 2 }
]
}
]
}

View file

@ -1,3 +1,4 @@
{ {
"location": "Building A - Lab 3", "location": "Building A - Lab 3",
"sensors": [ "sensors": [

View file

@ -1,28 +1,40 @@
# Copyright 2025 Kieler, Chiara
#
# Licensed under the AGPLv3.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.gnu.org/licenses/
from datetime import datetime from datetime import datetime
import re from flask import Flask, render_template
from flask import Flask, send_from_directory, render_template
import json import json
import matplotlib
import hashlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
# Global Variables: # Global Variables:
jsonpath = "data.json" jsonpath = "data_big.json"
htmldata = "" sensors: dict = {}
# class Entries: def readJson():
# def __init__(self, id: str, ts: list, value: list): f = open(jsonpath)
# self.id = id jsondata = json.load(f) # returns JSON object as a dictionary
# templist: list = [] for i in jsondata["sensors"]: # Iterating through the json list
# for i in range(len(ts)): timestamps: list = []
# templist.append([ts[i], value[i]]) values: list = []
# self.entries: list = templist # print(len(i["readings"]))
# for j in range(len(i["readings"])):
# def printEntries(self): timestamps.append(i["readings"][j]["ts"])
# print(self.entries) values.append(i["readings"][j]["value"])
# # print(i)
# # print(i["readings"][1])
# tese = Entries("sensor0", [1, 2, 3, 4], [10, 20, 30, 40]) # print("\n")
# tese.printEntries() sensors[i["id"]] = Sensor(i["id"], i["unit"], i["type"], timestamps, values)
f.close()
class Sensor: class Sensor:
@ -34,6 +46,7 @@ class Sensor:
self.values: list = value self.values: list = value
self.timeonly: list = [] self.timeonly: list = []
self.timedate: list = [] self.timedate: list = []
self.hash = hashlib.sha256(repr(ts).encode()).hexdigest()
for i in ts: for i in ts:
# print(i) # print(i)
self.timeonly.append(datetime.fromtimestamp(int(i)).strftime("%H:%M.%S")) self.timeonly.append(datetime.fromtimestamp(int(i)).strftime("%H:%M.%S"))
@ -73,6 +86,29 @@ class Sensor:
def getReadingsTime(self): def getReadingsTime(self):
return self.getReadings(timetype="time") return self.getReadings(timetype="time")
def getChartJSData(self, limit=5, reversed=False, timetype="ts"):
datalist: list = [[],[]]
match timetype:
case "ts":
for i in range(limit if limit < len(self.ts) else len(self.ts)):
datalist[0].append(self.ts[i])
datalist[1].append(self.values[i])
case "time":
for i in range(limit if limit < len(self.ts) else len(self.ts)):
datalist[0].append(self.timeonly[i])
datalist[1].append(self.values[i])
case "timedate":
for i in range(limit if limit < len(self.ts) else len(self.ts)):
datalist[0].append(self.timedate[i])
datalist[1].append(self.values[i])
case _:
return "ERROR: timetype must be one of 'ts', 'time', 'timedate'"
if reversed:
datalist[0].reverse()
datalist[1].reverse()
return datalist
def getValueByTimestamp(self, ts: int): def getValueByTimestamp(self, ts: int):
c = 0 c = 0
for i in self.ts: for i in self.ts:
@ -97,112 +133,15 @@ class Sensor:
plt.close() plt.close()
return path return path
# def formatLine(self, ts: int): def getHash(self):
# for i in self.ts: return self.hash
# if i == ts:
# return f"{self.timedate}> {self.values[]}"
# Json readJson()
f = open(jsonpath)
jsondata = json.load(f) # returns JSON object as a dictionary
# sensordict = dict()
sensors: dict = {}
for i in jsondata["sensors"]: # Iterating through the json list
timestamps: list = []
values: list = []
# print(len(i["readings"]))
for j in range(len(i["readings"])):
timestamps.append(i["readings"][j]["ts"])
values.append(i["readings"][j]["value"])
# print(i)
# print(i["readings"][1])
# print("\n")
# sensordict[i["id"]] = [i["type"], i["unit"], i["readings"]]
# print(sensordict[i["id"]])
sensors[i["id"]] = Sensor(i["id"], i["unit"], i["type"], timestamps, values)
# print("\n")
# print(sensors)
f.close()
print("\n")
# print(sensordict)
print(sensors)
# print(sensors["sensor_001"].getReadings(limit=2, reversed=True, timetype="time"))
# for i in sensordict:
# type = sensordict[i][0]
# unit = sensordict[i][1]
# readings = ""
# datapoints: list = []
# sensorrange = len(sensordict[i][2]) if len(sensordict[i][2]) < 5 else 5
# for j in range(sensorrange):
# datapoints.append([])
# ts = sensordict[i][2][j]["ts"]
# value = sensordict[i][2][j]["value"]
# datapoints[j].append(ts)
# datapoints[j].append(value)
# datapoints[j].append(unit)
# datapoints.sort(reverse=True)
#
# graphdata: list = [[], []]
# for j in datapoints:
# ts = j[0]
# time = datetime.fromtimestamp(ts).strftime("%d.%m.%Y %H:%M.%S")
# timeNoDate = datetime.fromtimestamp(ts).strftime("%H:%M.%S")
# value = j[1]
# unit = j[2]
# # print(f"{time} {j}")
# readings += f"{time}> {value}{unit}<br>"
# graphdata[0].append(timeNoDate)
# graphdata[1].append(value)
#
# # create_plot(i, unit, graphdata[0], graphdata[1])
#
# htmldata += f"""
# <p>Id: {i}</p>
# <p>Type: {type}</p>
# <p>Readings:</p>
# <div style="margin-left: 40px;">
# {readings}
# <img src="/plots/{i}.png" alt="Graph">
# </div>
# <hr>"""
#
# html = f"""
# <!DOCTYPE html>
# <html lang="en">
# <head>
# <meta charset="UTF-8">
# <title>Sensor Data</title>
# <style>
# * {{
# font-family: 'Caskaydia Cove NF';
# font-size: 18px;
# color: white;
# background: #1C1C1C;
# }}
# </style>
# </head>
# <body>
# {htmldata}
# </body>
# </html>
# """
# class MyHandler(BaseHTTPRequestHandler):
# def do_GET(self):
# self.send_response(200)
# # self.send_header("Content-type", "text/html")
# self.end_headers()
# self.wfile.write(html.encode("utf-8"))
#
#
# if __name__ == "__main__":
# server_address = ("", 8000)
# httpd = HTTPServer(server_address, MyHandler)
# print("Serving on http://localhost:8000")
# httpd.serve_forever()
app = Flask(__name__) app = Flask(__name__)
@ -211,13 +150,10 @@ app = Flask(__name__)
def index(): def index():
return render_template("index.html", data=sensors) return render_template("index.html", data=sensors)
@app.before_request
# @app.before_request def reload_data():
# def before_request(): readJson()
# for sensor in sensors.values(): print("hello")
# print(sensor)
# sensor.renderPlot()
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True) app.run(debug=True)

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,3 +1,11 @@
<!-- Copyright 2025 Kieler, Chiara -->
<!---->
<!-- Licensed under the AGPLv3.0 (the "License"); -->
<!-- You may not use this file except in compliance with the License. -->
<!-- You may obtain a copy of the License at -->
<!---->
<!-- https://www.gnu.org/licenses/ -->
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@ -7,28 +15,96 @@
href="{{ url_for('static', filename='favicon.ico') }}" href="{{ url_for('static', filename='favicon.ico') }}"
/> />
<title>Sensor Data</title> <title>Sensor Data</title>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js">
</script>
<style> <style>
@font-face {
font-family: 'CaskaydiaCove Nerd Font';
src: url('/static/CaskaydiaCove-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
* { * {
font-family: "Caskaydia Cove NF"; font-family: "CaskaydiaCove Nerd Font";
font-size: 18px; font-size: 18px;
color: white; color: white;
background: #1c1c1c; background: #1c1c1c;
<!-- margin: 0; -->
<!-- padding: 0; -->
<!-- box-sizing: border-box; -->
} }
.container {
display: flex; /* Enable flexbox */
justify-content: space-between; /* Optional: space between items */
}
.imgbox {
<!-- background-color: #4CAF50; /* Background color for the boxes */ -->
display: flex; /* Enable flexbox for inner content */
justify-content: right; /* Center text horizontally */
width: 100%;
max-width: 1000px;
}
.txtbox {
<!-- background-color: #C4FA05; /* Background color for the boxes */ -->
justify-content: left; /* Center text horizontally */
}
.txtbox h1 {
margin: 0;
padding: 0.5em 0; /* optional: add some vertical padding */
}
.txtbox h2 {
margin: 0;
padding: 0em 0; /* optional: add some vertical padding */
}
</style> </style>
</head> </head>
<body> <body>
<div class = "container">
<div class = "txtbox" style="text-align: center; margin: auto;">
<h1>Sensor Readings</h1> <h1>Sensor Readings</h1>
</div>
</div>
<h2>{% for sensor in data.items() %}</h2> <h2>{% for sensor in data.items() %}</h2>
<hr>
<h2>{{ sensor[1].getId() }}[{{ sensor[1].getType() }}]</h2> <h2>{{ sensor[1].getId() }}[{{ sensor[1].getType() }}]</h2>
<div class = "container">
<div class = "txtbox">
<ul> <ul>
{% for key,value in sensor[1].getReadingsTimeDate().items() %} {% for key,value in sensor[1].getReadingsTimeDate().items() %}
<li>{{ key }}» {{ value }}{{ sensor[1].getUnit() }}</li> <li>{{ key }}» {{ value }}{{ sensor[1].getUnit() }}</li>
{% endfor %} {% endfor %}
<img
src="{{ url_for('static', filename='plots/' + sensor[1].getId() + '.png') }}"
alt="Graph"
/>
</ul> </ul>
</div>
<canvas id={{ sensor[1].getId() }} class = "imgbox"></canvas>
</div>
<script>
var sensordata = {{ sensor[1].getChartJSData(timetype="timedate")|tojson }};
console.log(sensordata);
new Chart('{{ sensor[1].getId() }}', {
type: 'line',
data: {
labels: sensordata[0],
datasets: [{
label: '{{ sensor[1].getId() }}',
data: sensordata[1],
borderColor: 'rgba(245, 40, 145, 1)',
backgroundColor: 'rgba(245, 40, 145, 0.2)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>
{% endfor %} {% endfor %}
</body> </body>
</html> </html>