diff --git a/.env b/.env index 1d81e62..bea11f5 100644 --- a/.env +++ b/.env @@ -5,7 +5,6 @@ DB_PASS=TX.)-U1!zq5Axdk4 DB_NAME=Sondes AUTH_USERS=[{"user":"Michel","pass":"210462"}] - # MQTT MQTT_HOST=162.19.78.131 MQTT_USER=sondes @@ -39,9 +38,10 @@ SMTP_SECURITY=STARTTLS SMTP_USER=services@domo91.fr SMTP_PASS='VHq3278YA#sGV*bh#mR' MAIL_FROM=services@domo91.fr +MAIL_TO=services@domo91.fr MAIL_TO_SACLAY=robots@domo91.fr,nicolas.thibaut@bw-paris-saclay.com MAIL_FROM_SACLAY="DOMO91 Saclay " -MAIL_TO_MEUDON=robots@domo91.fr,superviseur.restauration@parismeudonermitage.com,chef@parismeudonermitage.com +MAIL_TO_MEUDON=robots@domo91.fr,chef@parismeudonermitage.com MAIL_FROM_MEUDON="DOMO91 Meudon " # --- Paramètres SMS ---- diff --git a/app/surveillance_releves.py b/app/surveillance_releves.py index 461810c..d52f074 100644 --- a/app/surveillance_releves.py +++ b/app/surveillance_releves.py @@ -1,39 +1,31 @@ #!/home/debian/Gestion_sondes/myenv/bin/python3 +# Surveillance de l'arrivée des relevés (par table/site) + SMS d'alerte / retour à la normale from datetime import datetime, timedelta from dotenv import load_dotenv import os -import utils_db import logging -from utils_sms import envoyer_sms -# Dossier Logs +import mysql.connector # important pour cibler les exceptions MySQL + +import utils_db +from utils_mail import envoyer_mail + +# -------------------- LOGS -------------------- LOG_DIR = '/home/debian/Gestion_sondes/Logs' os.makedirs(LOG_DIR, exist_ok=True) -# Fichier de log (nommé par date) log_filename = os.path.join(LOG_DIR, datetime.now().strftime("surveillance_%Y-%m-%d.log")) - logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(log_filename), - logging.StreamHandler() # Affiche aussi les logs dans la console - ] + handlers=[logging.FileHandler(log_filename), logging.StreamHandler()] ) -# Charger .env +# -------------------- ENV -------------------- load_dotenv('/home/debian/Gestion_sondes/.env') -# OVH SMS -APP_KEY = os.getenv('OVH_APPLICATION_KEY') -APP_SECRET = os.getenv('OVH_APPLICATION_SECRET') -CONSUMER_KEY = os.getenv('OVH_CONSUMER_KEY') -SERVICE_NAME = os.getenv('OVH_SMS_SERVICE') -SMS_RECEIVER = os.getenv('SMS_RECEIVER') -SMS_SENDER = os.getenv('OVH_SMS_SENDER') - +# -------------------- PARAMETRES -------------------- tables = ['Saclay', 'Meudon'] DELAI_MINUTES = 15 RAPPEL_HEURES = 6 @@ -41,61 +33,128 @@ RAPPEL_HEURES = 6 STATE_DIR = '/tmp/surveillance_states' os.makedirs(STATE_DIR, exist_ok=True) -def should_send_alert(site): - state_file = os.path.join(STATE_DIR, f'{site}.state') +TABLES_SET = set(tables) # whitelist simple + +def _state_file(site: str) -> str: + return os.path.join(STATE_DIR, f'{site}.state') + +def should_send_alert(site: str) -> bool: + """ + Retourne True si on doit envoyer une alerte maintenant (première fois ou rappel). + Met à jour le fichier d'état en cas d'envoi. + """ + sf = _state_file(site) now = datetime.now() - if not os.path.exists(state_file): - with open(state_file, 'w') as f: - f.write(now.isoformat()) + + if not os.path.exists(sf): + # première alerte + try: + with open(sf, 'w') as f: + f.write(now.isoformat()) + except OSError as e: + logging.warning(f"Impossible d'écrire l'état {sf} : {e} (on alerte quand même).") return True - with open(state_file, 'r') as f: - last_alert = datetime.fromisoformat(f.read().strip()) + + try: + with open(sf, 'r') as f: + raw = f.read().strip() + last_alert = datetime.fromisoformat(raw) + except (OSError, ValueError) as e: + # état illisible/corrompu -> on réinitialise et on alerte + logging.warning(f"Etat corrompu pour {site} ({sf}) : {e}. Réinitialisation.") + try: + with open(sf, 'w') as f: + f.write(now.isoformat()) + except OSError as e2: + logging.warning(f"Impossible de réécrire l'état {sf} : {e2}") + return True + if now - last_alert >= timedelta(hours=RAPPEL_HEURES): - with open(state_file, 'w') as f: - f.write(now.isoformat()) + try: + with open(sf, 'w') as f: + f.write(now.isoformat()) + except OSError as e: + logging.warning(f"Impossible de mettre à jour l'état {sf} : {e} (on alerte quand même).") return True + return False -def clear_state(site): - state_file = os.path.join(STATE_DIR, f'{site}.state') - if os.path.exists(state_file): - os.remove(state_file) +def clear_state(site: str) -> None: + sf = _state_file(site) + try: + if os.path.exists(sf): + os.remove(sf) + except OSError as e: + logging.warning(f"Impossible de supprimer l'état {sf} : {e}") def main(): - cnx = utils_db.connect_to_mysql() # ← appel via db_utils - cursor = cnx.cursor() - - now = datetime.now() - limite = now - timedelta(minutes=DELAI_MINUTES) problemes = [] + limite = datetime.now() - timedelta(minutes=DELAI_MINUTES) - for table in tables: - cursor.execute(f"SELECT MAX(Date) FROM {table}") - result = cursor.fetchone() - last_update = result[0] + # 1) Connexion MySQL (une seule fois) + try: + cnx = utils_db.connect_to_mysql() + cursor = cnx.cursor() + except mysql.connector.Error as e: + logging.error(f"MySQL KO : {e}") + envoyer_mail("⚠️ ALERTE : Base MySQL inaccessible (surveillance impossible).") + return - if not last_update or last_update < limite: - if should_send_alert(table): - problemes.append(f"{table} (dernier relevé : {last_update})") + # 2) Surveillance par table (try SQL à l'intérieur de la boucle) + try: + for table in tables: + if table not in TABLES_SET: + logging.warning(f"Table ignorée (non whitelistée) : {table}") + continue + + # 2a) Lecture de la dernière date (erreurs SQL gérées finement) + try: + cursor.execute(f"SELECT MAX(Date) FROM `{table}`") + (last_update,) = cursor.fetchone() + except mysql.connector.Error as e: + logging.error(f"Erreur SQL sur {table} : {e}") + # Vous pouvez décider ici si vous voulez un SMS ou seulement un log. + if should_send_alert(table): + envoyer_mail(f"⚠️ ALERTE : erreur SQL sur {table} (voir logs).") + continue + + # 2b) Logique métier (hors try SQL) + if (last_update is None) or (last_update < limite): + if should_send_alert(table): + problemes.append(f"{table} (dernier relevé : {last_update})") + logging.warning(f"⚠️ {table} en défaut (dernier relevé : {last_update})") + else: + logging.info(f"⏳ {table} déjà signalé, rappel dans {RAPPEL_HEURES}h.") else: - logging.info(f"⏳ Problème déjà signalé pour {table}, attente du délai de rappel.") - else: - if os.path.exists(os.path.join(STATE_DIR, f'{table}.state')): - message = f"✅ {table} : relevés à nouveau reçus. Situation normale." - envoyer_sms(message) - clear_state(table) - logging.info(f"📩 SMS de retour à la normale envoyé pour {table}.") - else: - logging.info(f"✅ {table} OK (dernier relevé : {last_update})") + # Retour à la normale uniquement si on était en défaut (state présent) + if os.path.exists(_state_file(table)): + message = f"✅ {table} : relevés à nouveau reçus (dernier : {last_update}). Situation normale." + envoyer_mail( + f"✅ OK : {table} relevés reçus", + message + ) + clear_state(table) + logging.info(f"📩 Retour à la normale envoyé pour {table}.") + else: + logging.info(f"✅ {table} OK (dernier relevé : {last_update})") - cursor.close() - cnx.close() + finally: + # 3) Nettoyage MySQL + try: + cursor.close() + cnx.close() + except mysql.connector.Error: + pass + # 4) Alerte groupée si besoin if problemes: - message = "⚠️ ALERTE : pas de relevés depuis >15min :\n" + "\n".join(problemes) - envoyer_sms(message) + message = f"⚠️ ALERTE : pas de relevés depuis >{DELAI_MINUTES}min :\n" + "\n".join(problemes) + envoyer_mail( + f"⚠️ ALERTE : absence de relevés > {DELAI_MINUTES} min", + message + ) else: - logging.info("👍 Tout est OK, aucun SMS envoyé.") + logging.info("👍 Tout est OK, aucun Mail envoyé.") if __name__ == "__main__": main() diff --git a/app/utils_db.py b/app/utils_db.py new file mode 100644 index 0000000..d355df8 --- /dev/null +++ b/app/utils_db.py @@ -0,0 +1,73 @@ +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_mail.py b/app/utils_mail.py new file mode 100644 index 0000000..41e6fd1 --- /dev/null +++ b/app/utils_mail.py @@ -0,0 +1,45 @@ +# /home/debian/Gestion_sondes/utils_mail.py +import os +import smtplib +import logging +from email.mime.text import MIMEText +from dotenv import load_dotenv + +load_dotenv('/home/debian/Gestion_sondes/.env') + +SMTP_HOST = os.getenv("SMTP_HOST", "smtp.mail.ovh.net") +SMTP_PORT = int(os.getenv("SMTP_PORT", "465")) + +SMTP_LOGIN = os.getenv("SMTP_LOGIN") # ex: services@domo91.fr +SMTP_PASSWORD = os.getenv("SMTP_PASSWORD") # mot de passe OVH +MAIL_FROM = os.getenv("MAIL_FROM", SMTP_LOGIN) +MAIL_TO = os.getenv("MAIL_TO") # ex: services@domo91.fr + +def envoyer_mail(sujet: str, contenu: str, destinataires=None) -> None: + """ + Envoi email via OVH SMTP SSL 465 (process identique à supervisor_watchdog.py). + destinataires: str unique ou liste; si None => MAIL_TO depuis .env + """ + if destinataires is None: + if not MAIL_TO: + raise ValueError("MAIL_TO manquant dans le .env") + destinataires = [a.strip() for a in MAIL_TO.split(",") if a.strip()] + elif isinstance(destinataires, str): + destinataires = [destinataires] + + if not SMTP_LOGIN or not SMTP_PASSWORD: + raise ValueError("SMTP_LOGIN / SMTP_PASSWORD manquants dans le .env") + + msg = MIMEText(contenu) + msg["Subject"] = sujet + msg["From"] = MAIL_FROM + msg["To"] = ", ".join(destinataires) + + try: + with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: + server.login(SMTP_LOGIN, SMTP_PASSWORD) + server.sendmail(MAIL_FROM, destinataires, msg.as_string()) + logging.info("📧 Mail envoyé: %s -> %s", sujet, destinataires) + except Exception as e: + logging.error("Erreur envoi mail: %s", e) + raise