Finalisation dashboard EcoCharge dynamique

This commit is contained in:
2026-05-05 03:46:22 +02:00
parent 9a1ec86d33
commit c60876e9a2
6 changed files with 447 additions and 285 deletions

144
app.py
View File

@@ -30,12 +30,39 @@ def get_connection():
)
def safe_float(value):
"""Convertit une valeur en float de façon sûre.
Accepte la virgule ou le point. Retourne None si impossible."""
if value is None:
return None
try:
return float(str(value).replace(",", ".").strip())
except (TypeError, ValueError):
return None
def normalize_number(value):
"""Normalise une saisie utilisateur (virgule -> point)."""
if value is None:
return None
return str(value).replace(",", ".").strip()
def get_setting_value(cur, key, default_value):
cur.execute("SELECT value FROM settings WHERE key = %s", (key,))
row = cur.fetchone()
return row[0] if row else default_value
def upsert_setting(cur, key, value):
cur.execute("""
INSERT INTO settings (key, value)
VALUES (%s, %s)
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value
""", (key, value))
def admin_required(view_func):
@wraps(view_func)
def wrapped_view(*args, **kwargs):
@@ -72,6 +99,7 @@ def receive_data():
if not data:
return jsonify({"error": "Aucune donnée JSON reçue"}), 400
# --- Noms JSON envoyés par l'ESP32 : INCHANGÉS ---
temperature_ext = data.get("temperature_ext")
humidity_ext = data.get("humidity_ext")
system_status_msg = data.get("system_status")
@@ -89,18 +117,29 @@ def receive_data():
conn = get_connection()
cur = conn.cursor()
max_battery_temperature = float(get_setting_value(cur, "max_battery_temperature", "60"))
min_battery_voltage = float(get_setting_value(cur, "min_battery_voltage", "11"))
min_solar_power = float(get_setting_value(cur, "min_solar_power", "5"))
# --- Seuils visuels (cohérents avec les VRAIS capteurs) ---
# Nouvelles clés pour ignorer d'anciennes valeurs incohérentes restées en base.
max_temperature = safe_float(get_setting_value(cur, "max_temp", "60")) or 60.0
min_voltage = safe_float(get_setting_value(cur, "min_voltage", "0.2")) or 0.2
min_power = safe_float(get_setting_value(cur, "min_power", "0.1")) or 0.1
battery_alert = "none"
# --- Alerte basée UNIQUEMENT sur les mesures réelles ---
# - température DHT11 (temperature_ext)
# - tension mesurée INA219 (voltage_pv)
# - puissance calculée (power_pv)
# Plus aucune référence à une "batterie" (pas de BMS sur ce montage).
temp_value = safe_float(temperature_ext)
voltage_value = safe_float(voltage_pv)
power_value = safe_float(power_pv)
if battery_temp is not None and float(battery_temp) > max_battery_temperature:
battery_alert = "Température batterie trop élevée"
elif voltage_battery is not None and float(voltage_battery) < min_battery_voltage:
battery_alert = "Tension batterie trop faible"
elif power_pv is not None and float(power_pv) < min_solar_power:
battery_alert = "Puissance solaire insuffisante"
battery_alert = "none" # nom de colonne conservé pour ne rien casser en base
if temp_value is not None and temp_value > max_temperature:
battery_alert = "Température trop élevée"
elif voltage_value is not None and voltage_value < min_voltage:
battery_alert = "Tension mesurée trop faible"
elif power_value is not None and power_value < min_power:
battery_alert = "Puissance calculée insuffisante"
cur.execute("""
INSERT INTO telemetry (
@@ -147,10 +186,13 @@ def latest_data():
conn = get_connection()
cur = conn.cursor()
# data_age_seconds = ancienneté de la dernière mesure, calculée côté serveur.
# Permet au dashboard de savoir si l'ESP32 envoie encore des données récentes.
cur.execute("""
SELECT
id,
created_at,
EXTRACT(EPOCH FROM (NOW() - created_at)) AS age_seconds,
temperature_ext,
humidity_ext,
system_status_msg,
@@ -176,30 +218,33 @@ def latest_data():
if not row:
return jsonify({"message": "Aucune donnée disponible"}), 404
age = row[2]
return jsonify({
"id": row[0],
"created_at": str(row[1]),
"temperature_ext": row[2],
"humidity_ext": row[3],
"system_status": row[4],
"voltage_pv": row[5],
"current_pv": row[6],
"power_pv": row[7],
"luminosity": row[8],
"voltage_battery": row[9],
"current_battery": row[10],
"battery_temp": row[11],
"battery_level": row[12],
"battery_alert": row[13]
"data_age_seconds": float(age) if age is not None else None,
"temperature_ext": row[3],
"humidity_ext": row[4],
"system_status": row[5],
"voltage_pv": row[6],
"current_pv": row[7],
"power_pv": row[8],
"luminosity": row[9],
"voltage_battery": row[10],
"current_battery": row[11],
"battery_temp": row[12],
"battery_level": row[13],
"battery_alert": row[14]
})
@app.route("/api/history", methods=["GET"])
def history_data():
limit = request.args.get("limit", default=10, type=int)
limit = request.args.get("limit", default=15, type=int)
if limit is None or limit <= 0:
limit = 10
limit = 15
if limit > 50:
limit = 50
@@ -207,6 +252,7 @@ def history_data():
conn = get_connection()
cur = conn.cursor()
# On renvoie maintenant current_pv (courant mesuré) pour le graphique électrique.
cur.execute("""
SELECT
id,
@@ -214,7 +260,7 @@ def history_data():
temperature_ext,
humidity_ext,
power_pv,
battery_level
current_pv
FROM telemetry
ORDER BY created_at DESC
LIMIT %s
@@ -233,7 +279,7 @@ def history_data():
"temperature_ext": row[2],
"humidity_ext": row[3],
"power_pv": row[4],
"battery_level": row[5]
"current_pv": row[5]
})
return jsonify(history)
@@ -286,43 +332,35 @@ def admin_dashboard():
cur = conn.cursor()
if request.method == "POST":
min_battery = request.form.get("min_battery")
max_temp = request.form.get("max_temp")
min_power = request.form.get("min_power")
# Normalisation : on accepte la virgule ET le point (ex : 0,3 -> 0.3)
min_voltage = normalize_number(request.form.get("min_battery"))
max_temp = normalize_number(request.form.get("max_temp"))
min_power = normalize_number(request.form.get("min_power"))
cur.execute("""
INSERT INTO settings (key, value)
VALUES ('min_battery_voltage', %s)
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value
""", (min_battery,))
cur.execute("""
INSERT INTO settings (key, value)
VALUES ('max_battery_temperature', %s)
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value
""", (max_temp,))
cur.execute("""
INSERT INTO settings (key, value)
VALUES ('min_solar_power', %s)
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value
""", (min_power,))
upsert_setting(cur, "min_voltage", min_voltage)
upsert_setting(cur, "max_temp", max_temp)
upsert_setting(cur, "min_power", min_power)
conn.commit()
cur.close()
conn.close()
# Post/Redirect/Get : évite le ré-envoi du formulaire si on actualise,
# et affiche proprement le message de succès.
return redirect(url_for("admin_dashboard", saved=1))
settings = {
"min_battery": get_setting_value(cur, "min_battery_voltage", "11"),
"max_temp": get_setting_value(cur, "max_battery_temperature", "60"),
"min_power": get_setting_value(cur, "min_solar_power", "5")
"min_battery": get_setting_value(cur, "min_voltage", "0.2"),
"max_temp": get_setting_value(cur, "max_temp", "60"),
"min_power": get_setting_value(cur, "min_power", "0.1")
}
cur.close()
conn.close()
return render_template("admin_dashboard.html", settings=settings)
saved = request.args.get("saved") == "1"
return render_template("admin_dashboard.html", settings=settings, saved=saved)
@app.route("/admin/logout", methods=["GET", "POST"])