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", "127.0.0.1") MQTT_PORT = int(os.getenv("MQTT_PORT", "1883")) 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