diff --git a/Outils/Injection_tests.py b/Outils/Injection_tests.py new file mode 100644 index 0000000..2edc299 --- /dev/null +++ b/Outils/Injection_tests.py @@ -0,0 +1,236 @@ +import os +import datetime as dt +import pandas as pd +import streamlit as st +from dotenv import load_dotenv +import mysql.connector as mc + +# ---------------------- +# Config de la page +# ---------------------- +st.set_page_config(page_title="Injection de données de test", page_icon="🧪", layout="centered") +st.title("🧪 Injecteur de relevés de test (Sondes)") +st.caption("Crée ~10 lignes au-dessus d'un seuil pour tester les alertes") + +# ---------------------- +# Connexion MySQL depuis .env +# ---------------------- +@st.cache_resource(show_spinner=False) +def get_connection(): + load_dotenv() + try: + cnx = mc.connect( + host=os.getenv("DB_HOST"), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASS"), + database=os.getenv("DB_NAME"), + autocommit=True, + ) + return cnx + except Exception as e: + st.error(f"Échec de connexion MySQL : {e}") + raise + +# ---------------------- +# Helpers : liste des sondes actives et hors entretien +# ---------------------- +@st.cache_data(ttl=60, show_spinner=False) +def list_sondes(site: str) -> list: + """Retourne la liste des sondes actives (Etat=ON) et non en entretien pour le site. + Essaie d'abord monitor_{site}, puis Chambres_froides, sinon fallback via la table de mesures. + """ + cnx = get_connection() + cur = cnx.cursor() + # 1) monitor_{site} + try: + cur.execute( + f""" + SELECT Sonde + FROM `monitor_{site}` + WHERE (Etat='ON' OR Etat=1) + AND ( (Maintenance='OFF') OR (Maintenance=0) OR (Maintenance IS NULL) ) + ORDER BY Sonde + """ + ) + rows = [r[0] for r in cur.fetchall()] + if rows: + return rows + except Exception: + pass + # 2) Chambres_froides + try: + cur.execute( + """ + SELECT Sonde + FROM `Chambres_froides` + WHERE Lieu=%s AND (Etat='ON' OR Etat=1) + AND ( (Maintenance='OFF') OR (Maintenance=0) OR (Maintenance IS NULL) ) + ORDER BY Sonde + """, + (site,) + ) + rows = [r[0] for r in cur.fetchall()] + if rows: + return rows + except Exception: + pass + # 3) Fallback : dernier état via table de mesures (distinct) + try: + cur.execute( + f"SELECT DISTINCT Sonde FROM `{site}` ORDER BY Sonde LIMIT 200" + ) + return [r[0] for r in cur.fetchall()] + except Exception: + return [] + +# ---------------------- +# Helper : récupérer Temp_Max pour une sonde donnée +# ---------------------- +@st.cache_data(ttl=60, show_spinner=False) +def get_temp_max(site: str, sonde: str): + """Retourne Temp_Max pour (site, sonde) en cherchant d'abord dans monitor_{site}, puis Chambres_froides. + Renvoie None si non trouvé.""" + try: + cnx = get_connection() + cur = cnx.cursor() + # 1) monitor_{site} + try: + cur.execute( + f"SELECT Temp_Max FROM `monitor_{site}` WHERE Sonde=%s LIMIT 1", + (sonde,) + ) + row = cur.fetchone() + if row and row[0] is not None: + return float(row[0]) + except Exception: + pass + # 2) Chambres_froides + try: + cur.execute( + """ + SELECT Temp_Max + FROM `Chambres_froides` + WHERE Lieu=%s AND Sonde=%s + LIMIT 1 + """, + (site, sonde) + ) + row = cur.fetchone() + if row and row[0] is not None: + return float(row[0]) + except Exception: + pass + except Exception: + return None + return None + +# ---------------------- +# UI paramètres +# ---------------------- +with st.sidebar: + st.header("Paramètres") + site = st.selectbox("Site (table)", ["Saclay", "Meudon"], index=0) + + # Sélecteur de sonde depuis la liste active / hors entretien + options_sondes = list_sondes(site) + if not options_sondes: + st.warning("Aucune sonde active trouvée (ou table monitor introuvable). Vous pouvez saisir un nom manuel.") + sonde = st.text_input("Nom de la sonde", value="TEST_Chambre1") + else: + sonde = st.selectbox("Sonde (actives, hors entretien)", options_sondes) + + st.subheader("Température") + # Auto-remplissage du seuil depuis la base et verrouillage par défaut + _temp_db = get_temp_max(site, sonde) + if _temp_db is None: + st.warning("Temp_Max introuvable en base ; valeur par défaut 6.0°C.") + _temp_db = 6.0 + allow_edit = st.checkbox("Autoriser la modification du seuil", value=False) + temp_max = st.number_input("Seuil (Temp_Max)", value=float(_temp_db), step=0.1, disabled=not allow_edit) + + delta = st.number_input("Delta au-dessus du seuil", value=1.0, step=0.1) + absolute_override = st.checkbox("Définir une température absolue à la place") + absolute_temp = st.number_input( + "Température absolue (si coché)", value=12.5, step=0.1, disabled=not absolute_override + ) + + st.subheader("Série temporelle") + rows = st.number_input("Nombre de points", min_value=1, max_value=200, value=10, step=1) + step_min = st.number_input("Pas (minutes)", min_value=1, max_value=120, value=5, step=1) + start_offset = st.number_input("Début : il y a (minutes)", min_value=0, max_value=1440, value=45, step=5) + + st.markdown("---") + st.caption("Nettoyage rapide") + cleanup_scope = st.selectbox("Supprimer", ["Cette sonde", "Toutes les TEST_ des dernières 24h"]) + do_cleanup = st.button("🧹 Supprimer les données de test") +col1, col2 = st.columns(2) +# ---------------------- +# Actions +# ---------------------- +if col1.button("🚀 Injecter les données"): + try: + cnx = get_connection() + cur = cnx.cursor() + + # Calcul des timestamps et de la valeur + now = dt.datetime.now() + t0 = now - dt.timedelta(minutes=int(start_offset)) + if absolute_override: + value = float(absolute_temp) + else: + value = float(temp_max) + float(delta) + + # Préparation batch INSERT + sql = f"INSERT INTO `{site}` (Sonde, Temperature, Date) VALUES (%s,%s,%s)" + batch = [] + for i in range(int(rows)): + ts = t0 + dt.timedelta(minutes=i * int(step_min)) + batch.append((sonde, value, ts.strftime("%Y-%m-%d %H:%M:%S"))) + + cur.executemany(sql, batch) + st.success(f"{len(batch)} lignes insérées dans `{site}` pour **{sonde}** à **{value}°C**.") + + # Aperçu des données insérées + cur.execute( + f""" + SELECT Id, Sonde, Temperature, Date + FROM `{site}` + WHERE Sonde = %s AND Date >= %s AND Date <= %s + ORDER BY Date DESC + LIMIT 50 + """, + ( + sonde, + (t0 - dt.timedelta(minutes=1)).strftime("%Y-%m-%d %H:%M:%S"), + (t0 + dt.timedelta(minutes=int(rows)*int(step_min) + 1)).strftime("%Y-%m-%d %H:%M:%S"), + ), + ) + rows_preview = cur.fetchall() + if rows_preview: + df = pd.DataFrame(rows_preview, columns=["Id", "Sonde", "Temperature", "Date"]) + st.dataframe(df, use_container_width=True, hide_index=True) + else: + st.info("Aucune ligne trouvée pour l'aperçu (vérifiez les filtres/horaires).") + + except Exception as e: + st.error(f"Erreur lors de l'injection : {e}") + +# Nettoyage +def cleanup(): + cnx = get_connection() + cur = cnx.cursor() + if cleanup_scope == "Cette sonde": + cur.execute(f"DELETE FROM `{site}` WHERE Sonde = %s", (sonde,)) + st.success(f"Données supprimées pour la sonde **{sonde}** dans `{site}`.") + else: + cur.execute( + f"DELETE FROM `{site}` WHERE Sonde LIKE 'TEST\_%' ESCAPE '\\' AND Date >= NOW() - INTERVAL 1 DAY" + ) + st.success(f"Toutes les sondes **TEST_** des dernières 24h supprimées dans `{site}`.") + +if do_cleanup: + try: + cleanup() + except Exception as e: + st.error(f"Erreur de nettoyage : {e}") + diff --git a/app/tracker.py b/Outils/tracker.py similarity index 100% rename from app/tracker.py rename to Outils/tracker.py diff --git a/app/visualiseur_logs.py b/Outils/visualiseur_logs.py similarity index 100% rename from app/visualiseur_logs.py rename to Outils/visualiseur_logs.py diff --git a/app/Monitor.py b/app/Monitor.py deleted file mode 100644 index 6c3a989..0000000 --- a/app/Monitor.py +++ /dev/null @@ -1,212 +0,0 @@ -import os -import time -from datetime import datetime, timedelta -from pathlib import Path -from utils_db import connect_to_mysql -from dotenv import load_dotenv -from utils_sms import envoyer_sms - -# === AJOUT GYRO (MQTT) === -import paho.mqtt.client as mqtt -from contextlib import closing - -# ----------------------------------------------------------- -# Logs -# ----------------------------------------------------------- -if os.name != 'nt': - log_dir = Path('/home/debian/Gestion_sondes/Logs') -else: - log_dir = Path.cwd() / 'Logs' - -log_dir.mkdir(parents=True, exist_ok=True) - -# ----------------------------------------------------------- -# Env -# ----------------------------------------------------------- -load_dotenv() -ENVOI_SMS = os.getenv("ENVOI_SMS") == "1" - -# === AJOUT GYRO (MQTT) === -MQTT_HOST = os.getenv("MQTT_HOST") -MQTT_PORT = int(os.getenv("MQTT_PORT")) -MQTT_USER = os.getenv("MQTT_USER") -MQTT_PASS = os.getenv("MQTT_PASS", "") -MQTT_QOS = 1 -GYRO_PUBLISH_GLOBAL = os.getenv("GYRO_PUBLISH_GLOBAL", "0") == "1" - -print("▶️ Lancement Monitor.py") - -# --- Suivi des alertes actives pour rappels --- -alertes_actives = {} - -# === AJOUT GYRO (MQTT) === -def publish_gyro_states(states_by_site: dict): - """Publie Alarmes//Gyro = ON|OFF (retained). Optionnel : Alarmes/Global/Gyro.""" - client = mqtt.Client( - client_id="MonitorGyroPublisher", - clean_session=True, # OK si protocol = MQTTv311 - protocol=mqtt.MQTTv311, - callback_api_version=mqtt.CallbackAPIVersion.VERSION2 - ) - if MQTT_USER or MQTT_PASS: - client.username_pw_set(MQTT_USER, MQTT_PASS) - client.connect(MQTT_HOST, MQTT_PORT, keepalive=30) - client.loop_start() - try: - for site, state in states_by_site.items(): - topic = f"Alarmes/{site}/Gyro" - client.publish(topic, state, qos=MQTT_QOS, retain=True) - print(f"📣 MQTT publish {topic} = {state} (retain)", flush=True) - - if GYRO_PUBLISH_GLOBAL: - global_state = "ON" if any(v == "ON" for v in states_by_site.values()) else "OFF" - client.publish("Alarmes/Global/Gyro", global_state, qos=MQTT_QOS, retain=True) - print(f"📣 MQTT publish Alarmes/Global/Gyro = {global_state} (retain)", flush=True) - finally: - client.loop_stop() - client.disconnect() - -# --- Fonction de surveillance --- -def surveiller(): - global alertes_actives - log_entries = [] - try: - conn = connect_to_mysql() - cursor = conn.cursor(dictionary=True) - - # Liste des lieux - cursor.execute("SELECT DISTINCT Lieu FROM `Chambres_froides`") - lieux = [row['Lieu'] for row in cursor.fetchall()] - - for lieu in lieux: - table_temp = lieu - table_alertes = f"Alertes_{lieu}" - - # Sondes actives et non en entretien - cursor.execute(""" - SELECT Sonde, Temp_Max - FROM Sondes.Chambres_froides - WHERE Lieu = %s - AND Etat = 'ON' - AND En_entretien = 0 - """, (lieu,)) - sondes = cursor.fetchall() - - for sonde in sondes: - nom_sonde = sonde['Sonde'] - seuil = sonde['Temp_Max'] - - # Derniers relevés (30 min = 6 pas de 5 min) - cursor.execute(f""" - SELECT Date, Temperature FROM {table_temp} - WHERE Sonde = %s - ORDER BY Date DESC LIMIT 6 - """, (nom_sonde,)) - releves = cursor.fetchall() - - # Logging détaillé - for r in releves: - log_entries.append({ - "Date": r['Date'], - "Lieu": lieu, - "Sonde": nom_sonde, - "Température": r['Temperature'], - "Seuil": seuil, - "État": "Dépassement" if r['Temperature'] > seuil else "Normal" - }) - - # Détection dépassement > 30 min - if len(releves) == 6: - toutes_hors_seuil = all(r['Temperature'] > seuil for r in releves) - plus_ancien = releves[-1]['Date'] - maintenant = datetime.now() - - if toutes_hors_seuil and (maintenant - plus_ancien >= timedelta(minutes=30)): - cursor.execute(f""" - SELECT COUNT(*) as total FROM {table_alertes} - WHERE Sonde=%s AND Status='En cours' - """, (nom_sonde,)) - en_cours = cursor.fetchone() - if en_cours['total'] == 0: - cursor.execute( - f"INSERT INTO {table_alertes} (Sonde, Debut_defaut, Status) VALUES (%s, NOW(), 'En cours')", - (nom_sonde,) - ) - print(f"🚨 Alerte déclenchée pour {nom_sonde} ({lieu})", flush=True) - - message = ( - f"La sonde '{nom_sonde}' du site '{lieu}' a dépassé le seuil de {seuil}°C " - f"depuis plus de 30 minutes.\nHeure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}" - ) - if ENVOI_SMS: - envoyer_sms(lieu, message) - - alertes_actives[nom_sonde] = maintenant - - else: - # Rappel SMS toutes les 1h si toujours en défaut - dernier_envoi = alertes_actives.get(nom_sonde) - if dernier_envoi and (maintenant - dernier_envoi >= timedelta(hours=1)): - message = ( - f"La sonde '{nom_sonde}' du site '{lieu}' est TOUJOURS en dépassement de seuil (>{seuil}°C).\n" - f"Heure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}" - ) - if ENVOI_SMS: - envoyer_sms(lieu, message) - alertes_actives[nom_sonde] = maintenant - - # Vérifier retour à la normale (Acquittement) - cursor.execute(f""" - SELECT Temperature FROM {table_temp} - WHERE Sonde = %s - ORDER BY Date DESC LIMIT 1 - """, (nom_sonde,)) - derniere = cursor.fetchone() - if derniere and derniere['Temperature'] <= seuil: - cursor.execute(f""" - UPDATE {table_alertes} - SET Status = 'Acquitté' - WHERE Sonde = %s AND Status IN ('En cours', 'Test') - """, (nom_sonde,)) - - if nom_sonde in alertes_actives: - del alertes_actives[nom_sonde] - - # --- À ce stade : tables d'alertes mises à jour. - # On calcule l'état GYRO par site et on publie MQTT. - # === AJOUT GYRO (calcul + publication) === - states = {} - for lieu in lieux: - table_alertes = f"Alertes_{lieu}" - # Etat actif si Status <> 'Acquitté' - cursor.execute(f"SELECT EXISTS(SELECT 1 FROM {table_alertes} WHERE Status <> 'Acquitté' LIMIT 1) AS actif") - actif = cursor.fetchone() - states[lieu] = "ON" if actif and list(actif.values())[0] == 1 else "OFF" - - # Commit avant publication MQTT (les états reflètent la DB) - conn.commit() - - # Publie ON/OFF par site (retain) - publish_gyro_states(states) - - cursor.close() - conn.close() - - # Ecriture log CSV - if log_entries: - import pandas as pd - df_logs = pd.DataFrame(log_entries) - try: - df_logs.to_csv(log_dir / "monitor.csv", sep=";", index=False) - print(f"✅ Log écrit dans {log_dir}/monitor.csv", flush=True) - except Exception as e: - print(f"❌ Erreur lors de l'écriture du fichier de log : {e}", flush=True) - - except Exception as e: - print(f"Erreur : {e}", flush=True) - -# --- Boucle principale --- -while True: - print(f"📡 Vérification à {datetime.now()}", flush=True) - surveiller() - time.sleep(300) # 5 minutes diff --git a/app/_init_.py b/app/_init_.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/alerte_sms.py b/app/alerte_sms.py deleted file mode 100644 index 291b9aa..0000000 --- a/app/alerte_sms.py +++ /dev/null @@ -1,174 +0,0 @@ -import mysql.connector -from datetime import datetime, timedelta -import time -from dotenv import load_dotenv -import os -from pathlib import Path - - -if os.name != 'nt': - log_dir = Path('/home/debian/Gestion_sondes/Logs') -else: - log_dir = Path.cwd() / 'Logs' - -log_dir.mkdir(parents=True, exist_ok=True) - -load_dotenv() -ENVOI_SMS = os.getenv("ENVOI_SMS") == "1" - -# --- Config MySQL --- -config = { - "host": os.getenv("DB_HOST"), - "user": os.getenv("DB_USER"), - "password": os.getenv("DB_PASSWORD"), - "database": os.getenv("DB_NAME") -} - -# --- Suivi des alertes actives pour rappels --- -alertes_actives = {} - -# --- Fonction d'envoi de mail --- -def envoyer_sms_ovh(message, lieu): - try: - import requests - sms_data = { - "account": os.getenv("OVH_SMS_ACCOUNT"), - "login": os.getenv("OVH_SERVICE_NAME"), - "password": os.getenv("OVH_PASSWORD"), - "message": f"{lieu}: {message}", - "receivers": os.getenv("SMS_RECEIVER", "").split(","), - "sender": os.getenv("OVH_SMS_SENDER") - } - - # Exemple d'envoi avec l'API OVH (à adapter selon ton endpoint exact) - response = requests.post("https://www.ovh.com/cgi-bin/sms/http2sms.cgi", data=sms_data) - print(f"📱 SMS envoyé : {response.text}", flush=True) - - except Exception as e: - print(f"Erreur envoi SMS : {e}", flush=True) - -# --- Fonction de surveillance --- -def surveiller(): - global alertes_actives - log_entries = [] - try: - conn = mysql.connector.connect(**config) - cursor = conn.cursor(dictionary=True) - - cursor.execute("SELECT DISTINCT Lieu FROM Sondes.Chambres_froides") - lieux = [row['Lieu'] for row in cursor.fetchall()] - - for lieu in lieux: - table_temp = lieu - table_alertes = f"Alertes_{lieu}" - - cursor.execute("SELECT Sonde, Temp_Max FROM Sondes.Chambres_froides WHERE Lieu=%s AND Etat='ON'", (lieu,)) - sondes = cursor.fetchall() - - for sonde in sondes: - nom_sonde = sonde['Sonde'] - seuil = sonde['Temp_Max'] - - cursor.execute(f""" - SELECT Date, Temperature FROM {table_temp} - WHERE Sonde = %s - ORDER BY Date DESC LIMIT 6 - """, (nom_sonde,)) - releves = cursor.fetchall() - - for r in releves: - log_entries.append({ - "Date": r['Date'], - "Lieu": lieu, - "Sonde": nom_sonde, - "Température": r['Temperature'], - "Seuil": seuil, - "État": "Dépassement" if r['Temperature'] > seuil else "Normal" - }) - - if len(releves) == 6: - toutes_hors_seuil = all(r['Temperature'] > seuil for r in releves) - plus_ancien = releves[-1]['Date'] - maintenant = datetime.now() - - if toutes_hors_seuil and (maintenant - plus_ancien >= timedelta(minutes=30)): - cursor.execute(f""" - SELECT COUNT(*) as total FROM {table_alertes} - WHERE Sonde=%s AND Status='En cours' - """, (nom_sonde,)) - en_cours = cursor.fetchone() - if en_cours['total'] == 0: - cursor.execute( - f"INSERT INTO {table_alertes} (Sonde, Debut_defaut, Status) VALUES (%s, NOW(), 'En cours')", - (nom_sonde,) - ) - print(f"🚨 Alerte déclenchée pour {nom_sonde} ({lieu})", flush=True) - - sujet = f"🚨 ALERTE TEMPÉRATURE - {nom_sonde} ({lieu})" - message = ( - f"La sonde '{nom_sonde}' du site '{lieu}' a dépassé le seuil de {seuil}°C " - f"depuis plus de 30 minutes.\nHeure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}" - ) - - if ENVOI_SMS: - envoyer_sms_ovh(message, lieu) - - # Suivi pour rappels - alertes_actives[nom_sonde] = maintenant - - else: - # Alerte déjà en cours : vérifier s'il faut faire un rappel - dernier_envoi = alertes_actives.get(nom_sonde) - if dernier_envoi and (maintenant - dernier_envoi >= timedelta(hours=1)): - sujet = f"🔔 RAPPEL ALERTE TEMPÉRATURE - {nom_sonde} ({lieu})" - message = ( - f"La sonde '{nom_sonde}' du site '{lieu}' est TOUJOURS en dépassement de seuil (>{seuil}°C).\n" - f"Heure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}" - ) - - if ENVOI_SMS: - envoyer_sms_ovh(message, lieu) - alertes_actives[nom_sonde] = maintenant - - # Vérifier retour à la normale (Acquittement) - cursor.execute(f""" - SELECT Temperature FROM {table_temp} - WHERE Sonde = %s - ORDER BY Date DESC LIMIT 1 - """, (nom_sonde,)) - derniere = cursor.fetchone() - if derniere and derniere['Temperature'] <= seuil: - cursor.execute(f""" - UPDATE {table_alertes} - SET Status = 'Acquitté' - WHERE Sonde = %s AND Status IN ('En cours', 'Test') - """, (nom_sonde,)) - - # Nettoyage du suivi si normalisé - if nom_sonde in alertes_actives: - del alertes_actives[nom_sonde] - - conn.commit() - cursor.close() - conn.close() - - if log_entries: - import pandas as pd - df_logs = pd.DataFrame(log_entries) - - # Sauvegarde principale - df_logs.to_csv(log_dir / "monitor.csv", sep=";", index=False) - - # Sauvegarde secondaire (Linux uniquement) - if os.name != 'nt': - df_logs.to_csv("/var/log/monitor.csv", sep=";", index=False) - - except Exception as e: - print(f"Erreur : {e}", flush=True) - -# --- Boucle principale --- -while True: - print(f"📡 Vérification à {datetime.now()}", flush=True) - surveiller() - time.sleep(300) # 5 minutes - diff --git a/app/db.py b/app/db.py deleted file mode 100644 index 134dbbd..0000000 --- a/app/db.py +++ /dev/null @@ -1,82 +0,0 @@ -# utils/db.py - -from datetime import datetime -from app.utils_db import connect_to_mysql # ✅ Import centralisé - -def get_latest_chaufferie(): - """Renvoie les dernières valeurs par sonde dans la table 'Chaufferie'.""" - db = connect_to_mysql() - cursor = db.cursor(dictionary=True) - query = """ - SELECT c1.* - FROM Sondes.Chaufferie c1 - INNER JOIN ( - SELECT Sonde, MAX(Date) AS MaxDate - FROM Sondes.Chaufferie - GROUP BY Sonde - ) c2 ON c1.Sonde = c2.Sonde AND c1.Date = c2.MaxDate - ORDER BY c1.Sonde; - """ - cursor.execute(query) - result = cursor.fetchall() - cursor.close() - db.close() - return result - - -def get_history_by_sonde(sonde: str, start: datetime, end: datetime): - """Retourne l’historique des températures d’une sonde entre deux dates.""" - db = connect_to_mysql() - cursor = db.cursor(dictionary=True) - query = """ - SELECT * FROM Sondes.Chaufferie - WHERE Sonde = %s AND Date BETWEEN %s AND %s - ORDER BY Date; - """ - cursor.execute(query, (sonde, start, end)) - result = cursor.fetchall() - cursor.close() - db.close() - return result - - -def verifier_utilisateur_commun(utilisateur: str, motdepasse: str): - """Vérifie si un utilisateur (non superviseur) existe dans la table MotsDePasse.""" - db = connect_to_mysql() - cursor = db.cursor(dictionary=True) - query = """ - SELECT * FROM Sondes.MotsDePasse - WHERE utilisateur = %s AND mot_de_passe = %s AND role = 'utilisateur' - """ - cursor.execute(query, (utilisateur, motdepasse)) - result = cursor.fetchone() - cursor.close() - db.close() - return result - - -def lire_alertes_sondes(): - """Renvoie la liste des alertes non acquittées dans la table Alertes_Chaufferie.""" - db = connect_to_mysql() - cursor = db.cursor(dictionary=True) - query = """ - SELECT * FROM Sondes.Alertes_Chaufferie - WHERE Etat != 'Acquitté' - ORDER BY Debut_defaut DESC - """ - cursor.execute(query) - result = cursor.fetchall() - cursor.close() - db.close() - return result - - -def acquitter_alerte(id_alerte: int): - """Met à jour une alerte comme acquittée dans la base.""" - db = connect_to_mysql() - cursor = db.cursor() - query = "UPDATE Sondes.Alertes_Chaufferie SET Etat = 'Acquitté' WHERE Id = %s" - cursor.execute(query, (id_alerte,)) - db.commit() - cursor.close() - db.close() diff --git a/app/logger_config.py b/app/logger_config.py deleted file mode 100644 index a0e2d7d..0000000 --- a/app/logger_config.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import logging - -def setup_logger(log_filename: str, dossier_logs: str = "/var/log/Cuisine_Saclay") -> None: - """ - Configure le logger pour écrire à la fois dans un fichier et sur la console. - - :param log_filename: Nom du fichier de log (exemple : 'Cuisine_Saclay.log') - :param dossier_logs: Dossier où enregistrer les logs (par défaut : /var/log/Cuisine_Saclay) - """ - - # 📁 Créer le dossier s'il n'existe pas - os.makedirs(dossier_logs, exist_ok=True) - - # 📄 Chemin complet du fichier de log - logfile = os.path.join(dossier_logs, log_filename) - - # 📝 Configuration de base du logger (fichier) - logging.basicConfig( - filename=logfile, - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(message)s", - filemode="a" # ajouter au fichier existant - ) - - # 🔔 Ajout de la sortie console - console = logging.StreamHandler() - console.setLevel(logging.INFO) - formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") - console.setFormatter(formatter) - - # 👇 Ajouter le handler console au logger racine - logging.getLogger('').addHandler(console) - - logging.info(f"Logger initialisé. Fichier de log : {logfile}") diff --git a/app/mqtt_logger.py b/app/mqtt_logger.py deleted file mode 100644 index 9ffb9de..0000000 --- a/app/mqtt_logger.py +++ /dev/null @@ -1,48 +0,0 @@ -import argparse -import paho.mqtt.client as mqtt_client -from dotenv import load_dotenv -import logging -from logger_config import setup_logger -from utils_db import connect_to_mysql -from functools import partial - -def on_message(table_sql, _client, _userdata, msg): - try: - logging.info(f"Message reçu sur {msg.topic}: {msg.payload.decode()}") - cursor = mydb.cursor() - sonde_name = '/'.join(msg.topic.split('/')[1:]) - sql = f"INSERT INTO {table_sql} (Sonde, Temperature) VALUES (%s, %s)" - val = (sonde_name, msg.payload.decode()) - cursor.execute(sql, val) - mydb.commit() - logging.info(f"Insertion réussie : {val}") - except Exception as e: - logging.error(f"Erreur lors de l'insertion du message : {e}") - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--log", required=True, help="Nom du fichier de log") - parser.add_argument("--table", required=True, help="Nom complet de la table SQL") - parser.add_argument("--topic", required=True, help="Topic MQTT à écouter") - args = parser.parse_args() - - # 📋 Initialiser le logger - setup_logger(args.log) - - # 🔑 Charger les variables d'environnement - load_dotenv() - - # 🔌 Connexion MySQL - mydb = connect_to_mysql() - - # 📡 Connexion MQTT - try: - client = mqtt_client.Client() - client.username_pw_set("Bwps", "scJ5ACj2keRfI^") - client.on_message = partial(on_message, args.table) - client.connect("54.36.188.119", 1883, 60) - client.subscribe(args.topic) - logging.info(f"Connexion MQTT réussie et abonnement au topic '{args.topic}'.") - client.loop_forever() - except Exception as err: - logging.error(f"Erreur MQTT : {err}") diff --git a/app/utils_db.py b/app/utils_db.py deleted file mode 100644 index a6ea1df..0000000 --- a/app/utils_db.py +++ /dev/null @@ -1,68 +0,0 @@ -import mysql.connector -from dotenv import load_dotenv -import os - -load_dotenv() - -def connect_to_mysql(): - return mysql.connector.connect( - host=os.getenv("DB_HOST"), - user=os.getenv("DB_USER"), - password=os.getenv("DB_PASS"), - database=os.getenv("DB_NAME") - ) - -def get_latest_chaufferie(): - conn = connect_to_mysql() - cursor = conn.cursor(dictionary=True) - query = """ - SELECT Sonde, Temperature, Date, Topic - FROM Sondes.Chaufferie - WHERE Date >= NOW() - INTERVAL 5 MINUTE - ORDER BY Date DESC - """ - cursor.execute(query) - result = cursor.fetchall() - cursor.close() - conn.close() - return result - -def get_history_by_sonde(sonde): - conn = connect_to_mysql() - cursor = conn.cursor(dictionary=True) - query = """ - SELECT Sonde, Temperature, Date - FROM Sondes.Chaufferie - WHERE Sonde = %s - AND Date >= NOW() - INTERVAL 1 DAY - - """ - cursor.execute(query, (sonde,)) - result = cursor.fetchall() - cursor.close() - conn.close() - return result - -def lire_alertes_sondes(): - conn = connect_to_mysql() - cursor = conn.cursor(dictionary=True) - query = """ - SELECT Id, Sonde, Debut_defaut, Etat - FROM Sondes.Alertes_Chaufferie - WHERE Etat != 'Acquitté' - ORDER BY Debut_defaut DESC - """ - cursor.execute(query) - result = cursor.fetchall() - cursor.close() - conn.close() - return result - -def acquitter_alerte(id_alerte): - conn = connect_to_mysql() - cursor = conn.cursor() - query = "UPDATE Sondes.Alertes_Chaufferie SET Etat = 'Acquitté' WHERE Id = %s" - cursor.execute(query, (id_alerte,)) - conn.commit() - cursor.close() - conn.close() diff --git a/app/utils_sms.py b/app/utils_sms.py deleted file mode 100644 index ea3a292..0000000 --- a/app/utils_sms.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import ovh -from dotenv import load_dotenv - -load_dotenv() - -def envoyer_sms(message: str, lieu: str = ""): - try: - client = ovh.Client( - endpoint=os.getenv("OVH_ENDPOINT"), - application_key=os.getenv("OVH_APP_KEY"), - application_secret=os.getenv("OVH_APP_SECRET"), - consumer_key=os.getenv("OVH_CONSUMER_KEY"), - ) - - services = client.get('/sms/') - if not services: - print("❌ Aucun service SMS OVH trouvé", flush=True) - return - - service_name = services[0] - numero_dest = os.getenv("SMS_RECEIVER") - sender = os.getenv("OVH_SMS_SENDER") - - if numero_dest.startswith('+'): - numero_dest = '00' + numero_dest[1:] - - if not numero_dest or not numero_dest.isdigit(): - print(f"❌ Numéro de téléphone invalide ou manquant : '{numero_dest}'", flush=True) - return - - payload = { - "sender": sender, - "receivers": [numero_dest], - "message": message, # Pas d'encodage ni de nettoyage ici - "priority": "high", - "noStopClause": False - - } - - print("📤 Requête envoyée à OVH :") - print(payload) - - result = client.post(f'/sms/{service_name}/jobs', **payload) - - print(f"📱 SMS envoyé à {numero_dest} pour {lieu}. Job ID : {result['ids']}", flush=True) - - except Exception as e: - print(f"❌ Erreur envoi SMS : {e}", flush=True) - -if __name__ == "__main__": - envoyer_sms("Test SMS OVH", lieu="utils_sms")