Mise en place des services d'alerte Meudon
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
# ========= Imports & chargement .env =========
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import ssl
|
||||
import smtplib
|
||||
@@ -13,16 +14,22 @@ from typing import List
|
||||
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
load_dotenv(find_dotenv(usecwd=True), override=False)
|
||||
SITE = "Saclay"
|
||||
PROGRAM_NAME = f"Monitor_{SITE}"
|
||||
|
||||
# MySQL
|
||||
import mysql.connector
|
||||
from mysql.connector import Error as MySQLError
|
||||
|
||||
# OVH (optionnel : si non installé, SMS désactivé proprement)
|
||||
# OVH (robuste même si la lib n'est pas installée)
|
||||
try:
|
||||
import ovh
|
||||
from ovh.exceptions import APIError as OVHAPIError
|
||||
_ovh_available = True
|
||||
except Exception:
|
||||
ovh = None # type: ignore
|
||||
class OVHAPIError(Exception): # fallback pour les except
|
||||
pass
|
||||
_ovh_available = False
|
||||
|
||||
# ========= Logger =========
|
||||
@@ -133,7 +140,7 @@ def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
|
||||
if not first_over:
|
||||
return False
|
||||
|
||||
now = dt.datetime.now(tz=first_over.tzinfo) if hasattr(first_over, "tzinfo") else dt.datetime.now()
|
||||
now = dt.datetime.now(tz=getattr(first_over, "tzinfo", None))
|
||||
return (now - first_over) >= dt.timedelta(minutes=30)
|
||||
except MySQLError as err:
|
||||
log.exception("Erreur DB (depassement_depuis_30min): %s", err)
|
||||
@@ -187,10 +194,44 @@ def acquitter_alerte(site: str, sonde: str):
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
# ========= Notifications (OVH + SMTP) =========
|
||||
def _split_list(raw: str | None) -> List[str]:
|
||||
return [x.strip() for x in (raw or "").split(",") if x and x.strip()]
|
||||
# ========= Helpers destinataires =========
|
||||
def _split_list(raw: str | None) -> list[str]:
|
||||
"""Pour les emails (MAIL_TO) — accepte virgule ou point-virgule."""
|
||||
return [x.strip() for x in re.split(r"[;,]", raw or "") if x.strip()]
|
||||
|
||||
def _parse_labeled_phones(raw: str | None) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Transforme 'Nom:+336..., Autre:+336...' en [('Nom','+336...'), ('Autre','+336...')]
|
||||
Si pas de nom fourni -> ('', '+336...')
|
||||
"""
|
||||
out: list[tuple[str, str]] = []
|
||||
for tok in re.split(r"[;,]", raw or ""):
|
||||
tok = tok.strip()
|
||||
if not tok:
|
||||
continue
|
||||
if ":" in tok:
|
||||
name, num = tok.split(":", 1)
|
||||
out.append((name.strip(), num.strip()))
|
||||
else:
|
||||
out.append(("", tok))
|
||||
return out
|
||||
|
||||
def _resolve_sms_receivers(labeled: list[tuple[str, str]]) -> list[str]:
|
||||
"""
|
||||
Applique éventuellement ALERT_SMS_ONLY=Nom1,Nom2 ou numéros directs.
|
||||
Si ALERT_SMS_ONLY absent -> tous les numéros.
|
||||
"""
|
||||
only = os.getenv("ALERT_SMS_ONLY")
|
||||
if not only:
|
||||
return [num for (_name, num) in labeled]
|
||||
allow = {x.strip() for x in re.split(r"[;,]", only) if x.strip()}
|
||||
return [num for (name, num) in labeled if (name and name in allow) or (num in allow)]
|
||||
|
||||
def _human_labeled_list(labeled: list[tuple[str, str]]) -> str:
|
||||
"""Michel(+336...), Christian(+336...) pour les logs."""
|
||||
return ", ".join([f"{n}({p})" if n else p for n, p in labeled])
|
||||
|
||||
# ========= Notifications (OVH + SMTP) =========
|
||||
class Notifier:
|
||||
def __init__(self):
|
||||
# OVH
|
||||
@@ -208,10 +249,15 @@ class Notifier:
|
||||
consumer_key=os.getenv("OVH_CONSUMER_KEY"),
|
||||
)
|
||||
self.ovh_service = os.getenv("OVH_SMS_SERVICE")
|
||||
self.ovh_sender = os.getenv("OVH_SMS_SENDER")
|
||||
self.sms_to = _split_list(os.getenv("ALERT_SMS_TO"))
|
||||
self.ovh_sender = os.getenv("OVH_SMS_SENDER")
|
||||
|
||||
# <<< LIGNE CLÉ POUR SACLAY >>>
|
||||
raw_sms = (os.getenv("ALERT_SMS_TO_Saclay")
|
||||
or os.getenv("ALERT_SMS_TO_SACLAY")
|
||||
or os.getenv("ALERT_SMS_TO")) # fallback facultatif
|
||||
self.sms_labeled = _parse_labeled_phones(raw_sms)
|
||||
else:
|
||||
self.sms_to = []
|
||||
self.sms_labeled = []
|
||||
|
||||
# SMTP
|
||||
self.smtp_host = os.getenv("SMTP_HOST")
|
||||
@@ -221,15 +267,34 @@ class Notifier:
|
||||
self.mail_from = os.getenv("MAIL_FROM") or self.smtp_user
|
||||
self.mail_to = _split_list(os.getenv("MAIL_TO") or os.getenv("EMAIL_DESTINATAIRES"))
|
||||
self.smtp_security = (os.getenv("SMTP_SECURITY", "SSL") or "SSL").upper()
|
||||
# >>> NOUVEAU : destinataires/expéditeur par site, sinon valeur générique
|
||||
site_key = SITE # "Saclay" ou "Meudon" selon le fichier
|
||||
raw_mail_to = (os.getenv(f"MAIL_TO_{site_key}")
|
||||
or os.getenv(f"MAIL_TO_{site_key.upper()}")
|
||||
or os.getenv("MAIL_TO")
|
||||
or "")
|
||||
self.mail_to = _split_list(raw_mail_to)
|
||||
|
||||
self.mail_from = (os.getenv(f"MAIL_FROM_{site_key}")
|
||||
or os.getenv(f"MAIL_FROM_{site_key.upper()}")
|
||||
or os.getenv("MAIL_FROM")
|
||||
or self.smtp_user)
|
||||
|
||||
self.smtp_enabled = all([self.smtp_host, self.smtp_port, self.smtp_user, self.smtp_pass, self.mail_to])
|
||||
|
||||
def send_sms(self, message: str, tag: str = "monitor-saclay") -> bool:
|
||||
if not self.ovh_enabled or not self.sms_to:
|
||||
if not self.ovh_enabled or not self.sms_labeled:
|
||||
log.warning("SMS désactivé ou aucun destinataire.")
|
||||
return False
|
||||
|
||||
receivers = _resolve_sms_receivers(self.sms_labeled) # liste de numéros
|
||||
if not receivers:
|
||||
log.warning("ALERT_SMS_ONLY a filtré tous les destinataires (aucun envoi).")
|
||||
return False
|
||||
|
||||
payload = {
|
||||
"sender": self.ovh_sender,
|
||||
"receivers": self.sms_to,
|
||||
"receivers": receivers,
|
||||
"message": message[:1600],
|
||||
"priority": "high",
|
||||
"coding": "7bit",
|
||||
@@ -239,7 +304,9 @@ class Notifier:
|
||||
"validityPeriod": 2880,
|
||||
"tag": tag,
|
||||
}
|
||||
|
||||
try:
|
||||
log.info("Envoi SMS vers: %s", _human_labeled_list([(n, p) for (n, p) in self.sms_labeled if p in receivers]))
|
||||
resp = self.ovh_client.post(f"/sms/{self.ovh_service}/jobs", **payload)
|
||||
ids = resp.get("ids") or []
|
||||
log.info("SMS OVH envoyé (job ids=%s)", ids)
|
||||
@@ -256,10 +323,11 @@ class Notifier:
|
||||
except Exception as err:
|
||||
log.debug("Suivi job OVH indisponible (OK): %s", err)
|
||||
return True
|
||||
except ovh.exceptions.APIError as err:
|
||||
|
||||
except OVHAPIError as err:
|
||||
log.exception("Erreur API OVH: %s", err)
|
||||
return False
|
||||
except Exception as err: # volontaire: ne jamais faire planter le service sur notif
|
||||
except Exception as err:
|
||||
log.exception("Echec envoi SMS OVH: %s", err)
|
||||
return False
|
||||
|
||||
@@ -291,7 +359,7 @@ class Notifier:
|
||||
except (smtplib.SMTPException, ssl.SSLError) as err:
|
||||
log.exception("Erreur SMTP: %s", err)
|
||||
return False
|
||||
except Exception as err: # pare-chocs
|
||||
except Exception as err:
|
||||
log.exception("Echec envoi email: %s", err)
|
||||
return False
|
||||
|
||||
@@ -349,8 +417,8 @@ def notifier_sur_depassement(site: str, sonde: str, temp: float, seuil: float):
|
||||
log.error("Alerte: SMS et mail KO pour %s/%s", site, sonde)
|
||||
|
||||
def notifier_acquittement(site: str, sonde: str, temp: float, seuil: float):
|
||||
subject, sms_text, email_body = build_ok_text(site, sonde, temp, seuil)
|
||||
sms_ok = notifier.send_sms(sms_text) # retour à la normale: SMS seul (comme convenu)
|
||||
subject, sms_text, _ = build_ok_text(site, sonde, temp, seuil)
|
||||
sms_ok = notifier.send_sms(sms_text) # retour à la normale: SMS seul
|
||||
if sms_ok:
|
||||
log.info("Acquittement envoyé (SMS) pour %s/%s", site, sonde)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user