backend rework- full test 1

This commit is contained in:
derlole
2025-05-07 11:08:43 +00:00
parent f7d9a640ee
commit 76bfa50995
19 changed files with 397 additions and 127 deletions

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"spellright.language": [
"de",
"en"
],
"spellright.documentTypes": [
"markdown",
"latex",
"plaintext"
]
}

20
db/README.md Normal file
View File

@@ -0,0 +1,20 @@
# COMMANDS DB description
## one command contains:
- command
- status
- command_id
- tstamp
### command may contain one out of following strings:
```["toggle_machine","","","","","",""]```
### status may contain one out of following strings:
```["pending","failed","served","rejected"]```
### command_id is a random generated 4 chars long integer to identify the exact command between Server frontend Database and ESP
### tstamp is the exact Date at the first ever creation of the db entry and does not say anything about completion or rejection.
## NOTE:
### A created command is marked as failed after 5 minutes if its status is still pending, to ensure that no processes continue after a communication failure in the chain.

Binary file not shown.

View File

@@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
# Dieses Skript erstellt eine SQLite-Datenbank mit einer Tabelle für Befehle.
import os
import sqlite3

29
init.py Normal file
View File

@@ -0,0 +1,29 @@
from modules.persistence import save_dict
from datetime import datetime, UTC
# Defaults
water = {
"lastFilled": str(datetime.now(UTC)),
"fill": 100,
"coffeesOnFill": 0
}
beans = {
"lastFilled": str(datetime.now(UTC)),
"fill": 100,
"coffeesOnFill": 0
}
machine = {
"state": "idle",
"connected": False,
"ready": False,
"peding_command": False,
"error": False,
"lastConnectionProof": str(datetime.now(UTC))
}
# In JSON-Dateien speichern
save_dict("water", water)
save_dict("beans", beans)
save_dict("machine", machine)

23
modules/persistence.py Normal file
View File

@@ -0,0 +1,23 @@
import os
import json
from datetime import datetime
BASE_PATH = os.path.join(os.path.dirname(__file__), "..", "persistence")
BASE_PATH = os.path.abspath(BASE_PATH)
def save_dict(name, data):
path = os.path.join(BASE_PATH, f"{name}.json")
os.makedirs(BASE_PATH, exist_ok=True)
with open(path, "w") as f:
json.dump(data, f, default=str, indent=2)
def load_dict(name):
path = os.path.join(BASE_PATH, f"{name}.json")
if os.path.exists(path):
with open(path, "r") as f:
return json.load(f)
return {} # fallback falls Datei fehlt
# no persistence but global variable important for tracking the esp-connection over runtime
esp_conn_infos = {"ip_global": None, "ip_local": None, "last_seen": None, "connection_valid": False}

16
modules/socketio.py Normal file
View File

@@ -0,0 +1,16 @@
# extensions.py
from flask_socketio import SocketIO
from modules.persistence import esp_conn_infos,load_dict, save_dict
socketio = SocketIO(cors_allowed_origins="*", async_mode='threading')
def resend_static_data():
water = load_dict("water")
beans = load_dict("beans")
machine = load_dict("machine")
socketio.emit('static_data', {
'water': water,
'beans': beans,
'machine': machine,
'esp_conn_infos': esp_conn_infos
})

6
persistence/beans.json Normal file
View File

@@ -0,0 +1,6 @@
{
"lastFilled": "2025-05-06 20:56:49.436340+00:00",
"fill": 100,
"coffeesOnFill": 0,
"refilled": 0
}

8
persistence/machine.json Normal file
View File

@@ -0,0 +1,8 @@
{
"state": "idle",
"connected": false,
"ready": false,
"peding_command": false,
"error": false,
"lastConnectionProof": "2025-05-06 20:56:49.436343+00:00"
}

6
persistence/water.json Normal file
View File

@@ -0,0 +1,6 @@
{
"lastFilled": "2025-05-06 20:56:49.436328+00:00",
"fill": 21,
"coffeesOnFill": 0,
"refilled": 0
}

View File

@@ -1,11 +1,13 @@
from flask import Blueprint, render_template, request, jsonify
import routes.shared as shared
from flask import Flask, jsonify, request
import paho.mqtt.client as mqtt
import json
import random
import sqlite3
import os
from modules.persistence import esp_conn_infos
import datetime
from modules.socketio import resend_static_data
esp = Blueprint('eps', __name__, url_prefix='/unsecure/esp')
@@ -15,11 +17,6 @@ MQTT_BROKER = "localhost" # oder IP/Domain
MQTT_PORT = 1883
MQTT_TOPIC = "coffee/command"
@esp.route('/')
def fetch_command():
pCd = shared.pending_command
shared.reset_command()
return jsonify(pCd)
@esp.route('/online', methods=['POST'])
def esp_online():
@@ -27,6 +24,12 @@ def esp_online():
sender_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
esp_ip = data.get("ip", "unknown")
esp_conn_infos["ip_local"] = esp_ip
esp_conn_infos["ip_global"] = sender_ip
esp_conn_infos["last_seen"] = datetime.now()
esp_conn_infos["connection_valid"] = True
resend_static_data()
print(f"ESP ONLINE von IP: {esp_ip}, roher IP: {sender_ip}")
return jsonify({"status": "ok"})

View File

@@ -1,10 +0,0 @@
pending_command = {'command': None, 'command-URL': None, 'command-expected': None, 'command-expected-URL': None}
def reset_command():
global pending_command
pending_command = {
'command': None,
'command-URL': None,
'command-expected': None,
'command-expected-topic': None
}

View File

@@ -1,25 +1,30 @@
from flask import Blueprint, render_template, request, jsonify
import routes.shared as shared
unsecure = Blueprint('unsecure', __name__, url_prefix='/unsecure')
from modules.persistence import load_dict, save_dict
from modules.persistence import esp_conn_infos
# from flask_socketio import SocketIO
from modules.socketio import resend_static_data
# def resend_static_data():
# water = load_dict("water")
# beans = load_dict("beans")
# machine = load_dict("machine")
# socketio.emit('static_data', {
# 'water': water,
# 'beans': beans,
# 'machine': machine,
# 'esp_conn_infos': esp_conn_infos
# })
@unsecure.route('/')
def index():
return render_template('index.html', title='gimmiCoffee')
water = load_dict("water")
beans = load_dict("beans")
machine = load_dict("machine")
print(f"Water: {water}, Beans: {beans}, Machine: {machine}")
return render_template('index.html', title='gimmiCoffee', water=water, beans=beans, machine=machine, esp_conn_infos=esp_conn_infos)
@unsecure.route('/send')
def send_command():
pCd = shared.pending_command
befehl = request.args.get('befehl')
if befehl:
pCd['command'] = befehl
pCd['command-URL'] = '/unsecure/esp/someURI'
pCd.update({'extra': 'test'})
print(pCd)
return f"Befehl '{pCd}' gespeichert."
return "Kein Befehl angegeben.", 400
# @unsecure.route('/live')
# def test():
# return render_template('live.html', users=[{'name': 'Max'}, {'name': 'Moritz'}, {'name': 'Hans'}])
@unsecure.route('/update')
def update():
resend_static_data()
return jsonify({"status": "ok"})

View File

@@ -1,19 +1,26 @@
from flask import Flask
from flask_socketio import SocketIO
from routes.unsecure_routes import unsecure
from routes.esp_routes import esp
import threading
import time
import os
import paho.mqtt.client as mqtt
from datetime import datetime, timedelta
import sqlite3
from modules.persistence import load_dict, save_dict, esp_conn_infos
from modules.socketio import socketio, resend_static_data
MQTT_BROKER = "localhost"
MQTT_PORT = 1883
MQTT_TOPIC_SUB = "coffee/status"
MQTT_TOPIC_SEND = "coffee/command"
app = Flask(__name__, static_url_path='/unsecure/static')
app.config['SECRET_KEY'] = 'super-secret-key'
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
socketio.init_app(app)
# Blueprints registrieren
app.register_blueprint(unsecure)
@@ -33,7 +40,7 @@ def on_message(client, userdata, msg):
'message': msg.payload.decode()
})
# MQTT-Thread starten
# MQTT-Thread
def mqtt_thread():
client = mqtt.Client()
client.on_connect = on_connect
@@ -41,24 +48,69 @@ def mqtt_thread():
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_forever()
# Dummy-Daten-Thread
# def send_data():
# counter = 0
# while True:
# data = {
# 'test': 'Live-Daten',
# 'status': 'OK',
# 'counter': counter
# }
# socketio.emit('update_data', data)
# counter += 1
# time.sleep(2)
# DB-Cleanup-Thread
def cleanup_old_commands():
# Beide Threads starten
#threading.Thread(target=send_data, daemon=True).start()
db_path = os.path.join(os.path.dirname(__file__), "db", "commands.db")
while True:
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
five_minutes_ago = (datetime.utcnow() - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
cursor.execute("""
UPDATE commands
SET status = 'failed'
WHERE status = 'pending' AND tstamp <= ?
""", (five_minutes_ago,))
updated_rows = cursor.rowcount
conn.commit()
conn.close()
if updated_rows > 0:
print(f"[Cleanup] {updated_rows} Einträge als 'failed' markiert.")
except Exception as e:
print(f"[Cleanup-Fehler] {e}")
time.sleep(60) # jede Minute prüfen ob es pending Einträge gibt, die älter als 5 Minuten sind
# Clear commands DB
def clear_commands_db():
import os
import sqlite3
db_path = os.path.join(os.path.dirname(__file__), "db", "commands.db")
if os.path.exists(db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("DELETE FROM commands")
conn.commit()
conn.close()
print("[DB] commands-Tabelle geleert.")
else:
print("[DB] Keine Datenbank gefunden nichts geleert.")
# Motitior ESP-Connection
def monitor_esp_connection():
while True:
if esp_conn_infos["last_seen"]:
time_diff = datetime.now() - esp_conn_infos["last_seen"]
if time_diff > timedelta(minutes=30):
esp_conn_infos["connection_valid"] = False
resend_static_data()
time.sleep(60) # einmal pro Minute die Verbindung zum ESP prüfen
### THREADS START ###
threading.Thread(target=cleanup_old_commands, daemon=True).start()
threading.Thread(target=monitor_esp_connection, daemon=True).start()
#threading.Thread(target=mqtt_thread, daemon=True).start()
if __name__ == '__main__':
#clear_commands_db()
socketio.run(app, host='0.0.0.0', port=3060, allow_unsafe_werkzeug=True)

View File

@@ -1 +1,55 @@
//im empty
function gebId(id) {
return document.getElementById(id);
}
const water = JSON.parse(document.getElementById("waterData").innerText)
const beans = JSON.parse(document.getElementById("beansData").innerText)
const machine = JSON.parse(document.getElementById("machineData").innerText)
const esp_conn_infos = JSON.parse(document.getElementById("espData").innerText)
// console.log(water)
// console.log(beans)
// console.log(machine)
// console.log(esp_conn_infos)
gebId("beans-fill").innerText = beans.fill + "%"
gebId("water-fill").innerText = water.fill + "%"
gebId("beans-filled").innerText = beans.refilled
gebId("water-filled").innerText = water.refilled
gebId("ip_global").innerText = esp_conn_infos.ip_global
gebId("ip_local").innerText = esp_conn_infos.ip_local
gebId("valid_connection").innerText = esp_conn_infos.connection_valid
gebId("last_seen").innerText = esp_conn_infos.last_seen
if (esp_conn_infos.connection_valid) {
gebId("validButt").classList.add("deniePress");
gebId("machine-status-butt").classList.remove("deniePress");
}else {
gebId("infoMain").classList.add("blink-orange");
}
if (water.fill < 20) {
gebId("water-fill").parentElement.classList.add("blink-orange");
}
if (beans.fill < 20) {
gebId("beans-fill").parentElement.classList.add("blink-orange");
}
function toggleMachine() {
if (gebId("machine-status-butt").classList.contains("deniePress")){
return;
}
// console.log("toggleMachine");
const result = confirm("Möchtest du den Vorgang wirklich ausführen?");
if (!result) {
return;
}
console.log("toggleMachine");
document.getElementById("machine-status").innerText = "PENDING";
document.getElementById("machine-status-butt").classList.add("blink-orange");
fetch('/unsecure/esp/toggle-machine', { method: 'POST' })
.then(res => res.json())
.then(data => {
console.log(data);
});
}

31
static/socketio.js Normal file
View File

@@ -0,0 +1,31 @@
function gebId(id) {
return document.getElementById(id);
}
const socket = io();
socket.on('static_data', (data) => {
gebId("beans-fill").innerText = data.beans.fill + "%"
gebId("water-fill").innerText = data.water.fill + "%"
gebId("beans-filled").innerText = data.beans.refilled
gebId("water-filled").innerText = data.water.refilled
if (data.water.fill < 20) {
gebId("water-fill").parentElement.classList.add("blink-orange");
}else {
gebId("water-fill").parentElement.classList.remove("blink-orange");
}
if (data.beans.fill < 20) {
gebId("beans-fill").parentElement.classList.add("blink-orange");
}else {
gebId("beans-fill").parentElement.classList.remove("blink-orange");
}
if (esp_conn_infos.connection_valid) {
gebId("validButt").classList.add("deniePress");
gebId("infoMain").classList.remove("blink-orange");
gebId("machine-status-butt").classList.remove("deniePress");
}else {
gebId("validButt").classList.remove("deniePress");
gebId("infoMain").classList.add("blink-orange");
gebId("machine-status-butt").classList.add("deniePress");
}
});

View File

@@ -115,16 +115,15 @@ header {
.button-grid {
display: flex;
flex-wrap: wrap;
gap: 30px;
max-height: 70vh;
overflow-y: auto;
gap: 20px;
max-height: 73vh;
}
.grid-button {
flex: 1 1 calc(50% - 15px);
background: #95a5a6;
color: white;
height: 23vh;
background: #818e8f;
/* color: white; */
height: 22vh;
display: flex;
align-items: center;
justify-content: center;
@@ -139,7 +138,7 @@ header {
border: 2px solid #000;
padding: 20px;
border-radius: 10px;
background: #f0f0f0;
background: #c4c4c4;
overflow-y: auto;
}
@@ -156,13 +155,13 @@ header {
position: relative;
width: 100%;
border-radius: 10px;
background: #2ecc71;
background: #5f5f5f;
font-family: Arial, sans-serif;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
color: rgb(0, 0, 0);
color: rgb(255, 255, 255);
flex: none;
height: 25vh;
transition: background 0.3s;
@@ -170,21 +169,24 @@ header {
.clickable:hover {
background: #27ae60;
background: #505050;
cursor: pointer;
}
.not:hover {
background: #5f5f5f;
}
.clickable .top-left-text {
position: absolute;
top: 10px;
left: 12px;
font-size: 24px;
color: #000000;
color: #ffffff;
}
.clickable .center-number {
font-size: 48px;
font-weight: bold;
color: #222;
color: #ffffff;
}
.grid-button .top-left-text {
position: absolute;
@@ -202,9 +204,11 @@ header {
color: #222;
text-align: center;
}
.deniePress:hover{
cursor: not-allowed;
.defaultGray:hover {
background: #6a7274;
}
.blink-orange {
background-color: orange;
animation: pulse-orange 1.0s infinite;
@@ -219,6 +223,34 @@ header {
background-color: red;
}
.initBackRed:Hover{
background-color: rgb(255, 37, 37);
background-color: rgb(252, 47, 47);
}
.deniePress:hover{
cursor: not-allowed;
}
.initBackRed:hover.deniePress {
background-color: red !important;
cursor: not-allowed;
}
.validationButtonOuter{
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.validButt{
background-color: green;
padding: 10px;
border-radius: 5px;
transition: background 0.3s;
}
.validButt:hover{
cursor: pointer;
background-color: rgb(0, 151, 0);
}
.validButt:hover.deniePress{
background-color: green !important;
}
.deniePress:hover{
cursor: not-allowed;
}

View File

@@ -7,6 +7,7 @@
<title>{{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="icon" href="{{ url_for('static', filename='gimmiCoffee_Logo.png') }}" type="image/png">
<script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
</head>
<body>
<header>
@@ -16,23 +17,43 @@
<a href="/logout" class="logout">Logout</a>
</div>
</header>
<div id="waterData" style="display: none;">{{ water | tojson }}</div>
<div id="beansData" style="display: none;">{{ beans | tojson }}</div>
<div id="machineData" style="display: none;">{{ machine | tojson }}</div>
<div id="espData" style="display: none;">{{ esp_conn_infos | tojson }}</div>
<main class="main-container">
<section class="left">
<div class="info">ESP-Conn Infos</div>
<div class="info" id="infoMain">
<div style="flex: 1; display: flex; align-items: center;">
<strong>ESP-Connection Infos</strong>
</div>
<div style="flex: 1;">
<p><strong>Global IP:</strong> <span id="ip_global"></span></p>
<p><strong>Local IP:</strong> <span id="ip_local"></span></p>
</div>
<div style="flex: 1;">
<p><strong>Valid Connection:</strong> <span id="valid_connection"></span></p>
<p><strong>Last Seen:</strong> <span id="last_seen"></span></p>
</div>
<div class="validationButtonOuter">
<div onclick="tryValidateConnection()" class="validButt" id="validButt">Validate Connection</div>
</div>
</div>
<div class="button-grid">
<div class="grid-button deniePress">
<div class="top-left-text">Kaffeeee</div>
<div class="center-number">Kaffee Machen</div>
</div>
<div class="grid-button initBackRed" id="machine-status-butt" onclick="toggleMachine()">
<div class="grid-button initBackRed deniePress" id="machine-status-butt" onclick="toggleMachine()">
<div class="top-left-text">Maschine</div>
<div class="center-number" id="machine-status">AUS</div>
</div>
<div class="grid-button deniePress">
<div class="top-left-text">Maschiene</div>
<div class="top-left-text">Maschine</div>
<div class="center-number">Nicht Bereit</div>
</div>
@@ -40,13 +61,13 @@
<div class="top-left-text">Fehler</div>
<div class="center-number">Wasser leer?</div>
</div>
<div class="grid-button deniePress">
<div class="grid-button defaultGray">
<div class="top-left-text">Wasser</div>
<div class="center-number">Aufgefüllt</div>
<div class="center-number">Nachgefüllt?</div>
</div>
<div class="grid-button deniePress">
<div class="grid-button defaultGray">
<div class="top-left-text">Bohnen</div>
<div class="center-number">Nachgefüllt</div>
<div class="center-number">Nachgefüllt?</div>
</div>
</div>
</section>
@@ -59,34 +80,26 @@
</div>
<div class="clickable">
<div class="top-left-text">Bohnen Füllstand</div>
<div class="center-number">XX%</div>
<div class="center-number" id="beans-fill">XX%</div>
</div>
<div class="clickable">
<div class="top-left-text">Wasser Füllstand</div>
<div class="center-number">XX%</div>
<div class="center-number" id="water-fill">XX%</div>
</div>
<div class="clickable">
<div class="clickable not">
<div class="top-left-text">Bohnen nachgefüllt</div>
<div class="center-number">XX</div>
<div class="center-number" id="beans-filled">XX</div>
</div>
<div class="clickable">
<div class="clickable not">
<div class="top-left-text">Wasser nachgefüllt</div>
<div class="center-number">XX</div>
<div class="center-number" id="water-filled">XX</div>
</div>
</section>
</main>
<script>
function toggleMachine() {
document.getElementById("machine-status").innerText = "PENDING";
document.getElementById("machine-status-butt").classList.add("blink-orange");
fetch('/unsecure/esp/toggle-machine', { method: 'POST' })
.then(res => res.json())
.then(data => {
console.log(data);
});
}
</script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
<script src="{{ url_for('static', filename='socketio.js') }}"></script>
</body>
</html>
@@ -94,4 +107,4 @@
<!--<script src="{{ url_for('static', filename='script.js') }}"></script>-->

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Live Update</title>
<script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
</head>
<body>
<h2>Daten vom Server</h2>
<p><strong>Test:</strong> <span id="test"></span></p>
<p><strong>Status:</strong> <span id="status"></span></p>
<p><strong>Counter:</strong> <span id="counter"></span></p>
<h1>Benutzer</h1>
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
<script>
const socket = io();
socket.on('update_data', (data) => {
document.getElementById('test').textContent = data.test;
document.getElementById('status').textContent = data.status;
document.getElementById('counter').textContent = data.counter;
});
</script>
</body>
</html>
</body>
</html>