from flask import Flask, request, abort, render_template, redirect, url_for, session, flash
import logging
import sqlite3
from argon2 import PasswordHasher
import requests
import json
import base64
import struct
app = Flask(__name__)
app.secret_key = 'clesecrete'
# Supprimer les logs de Flask
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
DATABASE = 'rhumservice.db'
ph = PasswordHasher()
TTN_URL = "xxxxxx"
TTN_API_KEY = "xxxxxx"
admin_list = ('admin', 'toor')
def init_db():
"""Cre les tables si elles n'existent pas."""
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS user (
uid TEXT PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
is_driver BOOLEAN DEFAULT 0,
balance INT DEFAULT 10
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS lora_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
poids_capteur1 INT,
poids_capteur2 INT,
poids_capteur3 INT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS cocktail_order (
order_id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL,
boissonA INT,
boissonB INT,
boissonC INT,
peanut BOOLEAN,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
def hash_password(password):
"""Hache un mot de passe avec Argon2id."""
return ph.hash(password)
def verify_password(input_password, stored_hash):
"""Vrifie si le mot de passe saisi correspond au hachage stock."""
try:
return ph.verify(stored_hash, input_password)
except Exception as e:
print(f"Erreur lors de la vrification du mot de passe : {e}")
return False
def user_exists(username):
"""Vrifie si un nom d'utilisateur existe dj dans la BDD."""
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT 1 FROM user WHERE username = ?
''', (username,))
return cursor.fetchone() is not None
def get_user_hashed_pass(username):
"""Rcupre le hash du mot de passe d'un utilisateur."""
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT password FROM user WHERE username = ?
''', (username,))
row = cursor.fetchone()
return row[0]
def insert_user(uid, username, password, first_name, last_name):
"""Insre un nouvel utilisateur dans la table user."""
password = hash_password(password)
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user (uid, username, password, first_name, last_name) VALUES (?, ?, ?, ?, ?)
''', (uid, username, password, first_name, last_name))
conn.commit()
def insert_lora_data(poids_capteur1, poids_capteur2, poids_capteur3):
"""Insre les donnes LoRa dans la table lora_data."""
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO lora_data (poids_capteur1, poids_capteur2, poids_capteur3) VALUES (?, ?, ?)
''', (poids_capteur1, poids_capteur2, poids_capteur3))
conn.commit()
@app.route('/post-lora', methods=['POST'])
def receive_lora_data():
"""Rception des donnes LoRa et insertion en BDD."""
auth_header = request.headers.get('Authorization')
if auth_header != f'Bearer {TTN_API_KEY}':
abort(403, description='Forbidden: Invalid API Key')
data = request.json
# Vrification des donnes reues
if not data or 'uplink_message' not in data:
return 'Donnes invalides', 400
decoded_payload = data['uplink_message'].get('decoded_payload', {})
rfid_uid = decoded_payload.get('uid')
poids_capteur1 = decoded_payload.get('weight1')
poids_capteur2 = decoded_payload.get('weight2')
poids_capteur3 = decoded_payload.get('weight3')
# Log format des donnes reues
print(f"RFID UID: {rfid_uid}\nPoids capteur 1: {poids_capteur1}\nPoids capteur 2: {poids_capteur2}\nPoids capteur 3: {poids_capteur3}\n")
# Consultation de la table cocktail_order pour vrifier les commandes en attente
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT order_id, boissonA, boissonB, boissonC, peanut
FROM cocktail_order
WHERE uid = ?
ORDER BY timestamp ASC
LIMIT 1
''', (rfid_uid,))
pending_order = cursor.fetchone()
if pending_order is not None:
order_id, boissonA, boissonB, boissonC, want_peanut = pending_order
send_cocktail_payload(boissonA, boissonB, boissonC, want_peanut)
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM cocktail_order WHERE order_id = ?
''', (order_id,))
conn.commit()
else:
print(f"Aucune commande en attente pour l'UID '{rfid_uid}'.")
# Insertion des donnes en BDD
try:
insert_lora_data(poids_capteur1, poids_capteur2, poids_capteur3)
return 'Donnes enregistres', 204
except Exception as e:
print(f"Erreur lors de l'insertion en BDD : {e}")
return 'Erreur serveur', 500
def send_cocktail_payload(boissonA, boissonB, boissonC, want_peanut):
"""
Compose et envoie un payload vers TTN contenant la composition du cocktail.
Le payload est constitu de 3 octets correspondant aux pourcentages des boissons.
"""
# Pack des 3 valeurs + cacahutes dans 4 octets
payload_bytes = struct.pack("BBBB", boissonA, boissonB, boissonC, want_peanut)
# Encodage en Base64 pour TTN
payload_base64 = base64.b64encode(payload_bytes).decode('utf-8')
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {TTN_API_KEY}'
}
data = {
"downlinks": [
{
"frm_payload": payload_base64,
"f_port": 1,
"priority": "NORMAL"
}
]
}
response = requests.post(TTN_URL, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print("Downlink envoy avec succs")
else:
print(f"Erreur sur l'envoie du downlink : {response.text}")
# --- Endpoints Web ---
@app.route('/')
def home():
"""Page d'accueil avec liens vers l'inscription et la connexion."""
return render_template('home.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
"""Page d'inscription utilisateur."""
if request.method == 'POST':
uid = request.form['uid']
username = request.form['username']
password = request.form['password']
first_name = request.form['first_name']
last_name = request.form['last_name']
if user_exists(username):
flash("Ce nom d'utilisateur existe dj.")
return redirect(url_for('register'))
try:
insert_user(uid, username, password, first_name, last_name)
flash("Inscription russie. Veuillez vous connecter.")
return redirect(url_for('login'))
except Exception as e:
print(f"Erreur lors de l'inscription : {e}")
flash("Erreur lors de l'inscription.")
return redirect(url_for('register'))
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Page de connexion utilisateur."""
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if verify_password(password, get_user_hashed_pass(username)):
session['username'] = username
return redirect(url_for('dashboard'))
else:
flash("Identifiants invalides.")
return redirect(url_for('login'))
return render_template('login.html')
@app.route('/dashboard')
def dashboard():
"""Page dashboard affichant les informations de l'utilisateur."""
if 'username' not in session:
return redirect(url_for('login'))
username = session['username']
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT uid, username, first_name, last_name, is_driver, balance FROM user WHERE username = ?
''', (username,))
user = cursor.fetchone()
cursor.execute('''
SELECT boissonA, boissonB, boissonC, peanut, timestamp
FROM cocktail_order
WHERE uid = ?
ORDER BY timestamp ASC
''', (user[0],))
cocktail_orders = cursor.fetchall()
return render_template('dashboard.html', user=user, cocktail_orders=cocktail_orders)
@app.route('/toggle_driver', methods=['POST'])
def toggle_driver():
"""Permet l'utilisateur de changer son statut 'Conducteur'."""
if 'username' not in session:
return redirect(url_for('login'))
username = session['username']
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE user SET is_driver = CASE WHEN is_driver = 1 THEN 0 ELSE 1 END WHERE username = ?
''', (username,))
conn.commit()
flash("Statut conducteur modifi.")
return redirect(url_for('dashboard'))
@app.route('/add_balance', methods=['POST'])
def add_balance():
"""Ajoute 10 au solde de l'utilisateur."""
if 'username' not in session:
return redirect(url_for('login'))
username = session['username']
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE user SET balance = balance + 10 WHERE username = ?
''', (username,))
conn.commit()
flash("Solde augment de 10.")
return redirect(url_for('dashboard'))
# Fonction de mapping : 0% -> 0 et 100% -> 400 (40cl)
def estimated_weight(percentage, margin=1.05):
return (400 * percentage / 100) * margin
@app.route('/cocktail')
def cocktail_page():
"""Page de commande de cocktails."""
if 'username' not in session:
return redirect(url_for('login'))
return render_template('cocktail.html')
@app.route('/order_cocktail', methods=['POST'])
def order_cocktail():
"""Ajoute une commande dans la file d'attente pour l'utilisateur."""
if 'username' not in session:
return redirect(url_for('login'))
# Rcupration et conversion des pourcentages envoys par le formulaire
try:
boissonA = int(request.form.get('boissonA', 0))
boissonB = int(request.form.get('boissonB', 0))
boissonC = int(request.form.get('boissonC', 0))
except ValueError:
flash("Valeurs invalides pour les pourcentages.")
return redirect(url_for('cocktail_page'))
# Rcupration et conversion du statut "Cacahutes ?"
try:
want_peanut = 1 if request.form.get('Cacahuetes') else 0
except ValueError:
flash("Valeur pour 'Cacahutes' invalide.")
return redirect(url_for('cocktail_page'))
# Vrification que les valeurs sont comprises entre 0 et 100
if not (0 <= boissonA <= 100 and 0 <= boissonB <= 100 and 0 <= boissonC <= 100):
flash("Les pourcentages doivent tre entre 0 et 100.")
return redirect(url_for('cocktail_page'))
# Vrification de la somme des pourcentages
total = boissonA + boissonB + boissonC
if total > 100:
flash("La somme des pourcentages ne peut dpasser 100%.")
return redirect(url_for('cocktail_page'))
# Rcuprer la dernire donne de lora_data pour vrifier la disponibilit
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT poids_capteur1, poids_capteur2, poids_capteur3 FROM lora_data ORDER BY timestamp DESC LIMIT 1
''')
lora_row = cursor.fetchone()
if not lora_row:
flash("Donnes des rservoirs indisponibles.")
return redirect(url_for('cocktail_page'))
# Rcuprer les commandes en attente pour calculer le poids estim dduire
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT boissonA, boissonB, boissonC FROM cocktail_order
''')
pending_orders = cursor.fetchall()
pending_weight_A = sum(estimated_weight(int(order[0])) for order in pending_orders)
pending_weight_B = sum(estimated_weight(int(order[1])) for order in pending_orders)
pending_weight_C = sum(estimated_weight(int(order[2])) for order in pending_orders)
available_A = lora_row[0] - pending_weight_A
available_B = lora_row[1] - pending_weight_B
available_C = lora_row[2] - pending_weight_C
# Debug
# print(f"Quantit virtuelle rservoir A : {available_A}\n"
# f"Quantit virtuelle rservoir B : {available_B}\n"
# f"Quantit virtuelle rservoir C : {available_C}\n")
# Vrifier la disponibilit pour chaque boisson
if boissonA > 0 and available_A < estimated_weight(boissonA):
flash("Quantit insuffisante pour la Boisson A.")
return redirect(url_for('cocktail_page'))
if boissonB > 0 and available_B < estimated_weight(boissonB):
flash("Quantit insuffisante pour la Boisson B.")
return redirect(url_for('cocktail_page'))
if boissonC > 0 and available_C < estimated_weight(boissonC):
flash("Quantit insuffisante pour la Boisson C.")
return redirect(url_for('cocktail_page'))
# Rcuprer l'UID et le solde de l'utilisateur
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT uid, balance, is_driver FROM user WHERE username = ?
''', (session['username'],))
result = cursor.fetchone()
if result:
uid, balance, is_driver = result
else:
flash("Erreur utilisateur.")
return redirect(url_for('cocktail_page'))
# Test : L'utilisateur doit avoir au moins 10 de solde
if balance < 10:
flash("Solde insuffisant pour commander un cocktail.")
return redirect(url_for('cocktail_page'))
# Test : Limitation des boissons si l'utilisateur est conducteur
if is_driver and boissonA > 0:
flash("En tant que conducteur, vous ne pouvez commander que la boisson B et C (sans alcool).")
return redirect(url_for('cocktail_page'))
# Insrer la commande dans la table cocktail_order
cursor.execute('''
INSERT INTO cocktail_order (uid, boissonA, boissonB, boissonC, peanut)
VALUES (?, ?, ?, ?, ?)
''', (uid, boissonA, boissonB, boissonC, want_peanut))
conn.commit()
# Dbiter de 10 le solde de l'utilisateur
cursor.execute('''
UPDATE user SET balance = balance - 10 WHERE username = ?
''', (session['username'],))
conn.commit()
flash("Cocktail command ! 10 ont t dbits de votre solde.")
return redirect(url_for('cocktail_page'))
@app.route('/admin')
def admin_dashboard():
# Vrification de l'accs admin (ici, on suppose que admin_list a le droit)
if 'username' not in session or session['username'] not in admin_list:
return redirect(url_for('home'))
with sqlite3.connect(DATABASE) as conn:
cursor = conn.cursor()
# Partie Physique : rcuprer la dernire donne LoRa
cursor.execute('''
SELECT poids_capteur1, poids_capteur2, poids_capteur3, timestamp FROM lora_data ORDER BY timestamp DESC LIMIT 1
''')
lora_data = cursor.fetchone()
# Rcuprer toutes les commandes en attente
cursor.execute('''
SELECT uid, boissonA, boissonB, boissonC, timestamp FROM cocktail_order
''')
cocktail_orders = cursor.fetchall()
# Rcuprer la liste des utilisateurs
cursor.execute('''
SELECT uid, username, first_name, last_name, balance, is_driver FROM user
''')
users = cursor.fetchall()
# Si des donnes LoRa existent, rcuprer les valeurs des capteurs
if lora_data:
sensor_A = lora_data[0]
sensor_B = lora_data[1]
sensor_C = lora_data[2]
else:
sensor_A = sensor_B = sensor_C = 0
# Calculer le poids total des commandes en attente pour chaque boisson
pending_weight_A = sum(estimated_weight(int(order[1])) for order in cocktail_orders)
pending_weight_B = sum(estimated_weight(int(order[2])) for order in cocktail_orders)
pending_weight_C = sum(estimated_weight(int(order[3])) for order in cocktail_orders)
# Calculer l'tat virtuel (disponibilit) des stocks
available_A = sensor_A - pending_weight_A
available_B = sensor_B - pending_weight_B
available_C = sensor_C - pending_weight_C
return render_template('admin_dashboard.html',
lora_data=lora_data,
available_A=available_A,
available_B=available_B,
available_C=available_C,
cocktail_orders=cocktail_orders,
users=users)
@app.route('/logout')
def logout():
"""Dconnecte l'utilisateur."""
session.pop('username', None)
return redirect(url_for('home'))
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=60100)
Comments
Please log in or sign up to comment.