Règlages des alertes dans Monitor_Meudon
This commit is contained in:
@@ -38,15 +38,16 @@ except Exception:
|
||||
_mqtt_ok = False
|
||||
|
||||
# ========= Logger =========
|
||||
level = getattr(logging, os.getenv("LOGLEVEL", "INFO").upper(), logging.INFO)
|
||||
log = logging.getLogger(PROGRAM_NAME.lower())
|
||||
if not log.handlers:
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(message)s")
|
||||
|
||||
# ========= DB utils =========
|
||||
def open_alert(conn, table_alertes: str, sonde: str, dt: datetime) -> bool:
|
||||
"""
|
||||
Ouvre UNE alerte si aucune alerte 'En cours' n'existe encore pour la sonde.
|
||||
Retourne True si une nouvelle alerte a été créée (→ notifier par mail).
|
||||
Retourne True si une nouvelle alerte a été créée (→ notifier par mail & SMS client).
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
@@ -178,46 +179,49 @@ def lire_seuils_depuis_db(site: str):
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
def depassement_depuis_30min(site: str, sonde: str, seuil: float, window_min: int = 180) -> bool:
|
||||
# --- Dépassement continu (configurable) ---
|
||||
def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
|
||||
"""
|
||||
True si la sonde est au-dessus du seuil de façon CONTINUE depuis ≥ 30 min.
|
||||
On lit l'historique (par défaut 3h) et on remonte jusqu'au début de la séquence > seuil.
|
||||
True si la sonde est > seuil de façon CONTINUE depuis CONT_MIN minutes.
|
||||
CONT_MIN = ALERT_CONTINUOUS_MINUTES (defaut 30)
|
||||
LOOKBACK = ALERT_LOOKBACK_MINUTES (defaut max(60, CONT_MIN*3))
|
||||
"""
|
||||
CONT_MIN = int(os.getenv("ALERT_CONTINUOUS_MINUTES", "30"))
|
||||
LOOKBACK = int(os.getenv("ALERT_LOOKBACK_MINUTES", str(max(60, int(os.getenv("ALERT_CONTINUOUS_MINUTES", "30"))*3))))
|
||||
|
||||
table = site
|
||||
cnx = get_db()
|
||||
try:
|
||||
cur = cnx.cursor()
|
||||
|
||||
# 1) Récupère l'historique récent de la sonde (ordre : plus récent -> plus ancien)
|
||||
cur.execute(f"""
|
||||
SELECT Temperature, Date
|
||||
FROM `{table}`
|
||||
WHERE Sonde=%s
|
||||
AND Date >= (NOW() - INTERVAL %s MINUTE)
|
||||
ORDER BY Date DESC
|
||||
""", (sonde, int(window_min)))
|
||||
""", (sonde, LOOKBACK))
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
return False
|
||||
|
||||
# 2) La dernière valeur doit être > seuil, sinon aucun dépassement en cours
|
||||
last_temp, last_dt = float(rows[0][0]), rows[0][1]
|
||||
if last_temp <= float(seuil):
|
||||
return False
|
||||
|
||||
# 3) Remonter jusqu'au début de la séquence continue > seuil
|
||||
# (dès qu'on rencontre une valeur <= seuil, on s'arrête)
|
||||
# Début de la séquence continue > seuil
|
||||
start_dt = last_dt
|
||||
for temp, d in rows[1:]:
|
||||
if float(temp) > float(seuil):
|
||||
start_dt = d # on prolonge la séquence vers le passé
|
||||
start_dt = d
|
||||
else:
|
||||
break # séquence continue terminée
|
||||
break
|
||||
|
||||
# 4) Durée de la séquence continue
|
||||
tzinfo = getattr(start_dt, "tzinfo", None)
|
||||
now = dt.datetime.now(tz=tzinfo) # naïf si la date DB est naïve
|
||||
return (now - start_dt) >= dt.timedelta(minutes=30)
|
||||
now = dt.datetime.now(tz=tzinfo)
|
||||
dur_min = (now - start_dt).total_seconds() / 60.0
|
||||
log.debug("Seq>seuil %s: start=%s, now=%s, dur=%.1fmin, need>=%d",
|
||||
sonde, start_dt, now, dur_min, CONT_MIN)
|
||||
return dur_min >= CONT_MIN
|
||||
|
||||
except MySQLError as err:
|
||||
log.exception("Erreur DB (depassement_depuis_30min, continu): %s", err)
|
||||
@@ -225,20 +229,6 @@ def depassement_depuis_30min(site: str, sonde: str, seuil: float, window_min: in
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
|
||||
def any_alert_open(site: str) -> bool:
|
||||
table = f"Alertes_{site}"
|
||||
cnx = get_db()
|
||||
try:
|
||||
cur = cnx.cursor()
|
||||
cur.execute(f"SELECT 1 FROM `{table}` WHERE `Etat`='En cours' LIMIT 1")
|
||||
return cur.fetchone() is not None
|
||||
except MySQLError as err:
|
||||
log.exception("Erreur DB (any_alert_open): %s", err)
|
||||
return False
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
# ========= Helpers listes/numéros =========
|
||||
def _split_list(raw: str | None) -> list[str]:
|
||||
return [x.strip() for x in re.split(r"[;,]", raw or "") if x.strip()]
|
||||
@@ -266,7 +256,7 @@ def _resolve_sms_receivers(labeled: list[tuple[str, str]]) -> list[str]:
|
||||
def _human_labeled_list(labeled: list[tuple[str, str]]) -> str:
|
||||
return ", ".join([f"{n}({p})" if n else p for n, p in labeled])
|
||||
|
||||
# ========= Notifier (SMS + Mail) =========
|
||||
# ========= Notifier (SMS interne + SMS client + Mail) =========
|
||||
class Notifier:
|
||||
def __init__(self):
|
||||
# OVH SMS
|
||||
@@ -282,11 +272,22 @@ class Notifier:
|
||||
)
|
||||
self.ovh_service = os.getenv("OVH_SMS_SERVICE")
|
||||
self.ovh_sender = os.getenv("OVH_SMS_SENDER")
|
||||
raw_sms = (os.getenv("ALERT_SMS_TO_Saclay") or os.getenv("ALERT_SMS_TO_SACLAY") or os.getenv("ALERT_SMS_TO"))
|
||||
raw_sms = (os.getenv(f"ALERT_SMS_TO_{SITE}") or os.getenv(f"ALERT_SMS_TO_{SITE.upper()}") or os.getenv("ALERT_SMS_TO"))
|
||||
self.sms_labeled = _parse_labeled_phones(raw_sms)
|
||||
else:
|
||||
self.sms_labeled = []
|
||||
|
||||
# SMS CLIENTS (site-spécifique + génériques + compat FR)
|
||||
raw_sms_client = (
|
||||
os.getenv(f"ALERT_SMS_CLIENT_TO_{SITE}") or
|
||||
os.getenv(f"ALERT_SMS_CLIENT_TO_{SITE.upper()}") or
|
||||
os.getenv("ALERT_SMS_CLIENT_TO") or
|
||||
os.getenv(f"ALERTE_CLIENT_{SITE}") or
|
||||
os.getenv("ALERTE_CLIENT")
|
||||
)
|
||||
self.sms_client_labeled = _parse_labeled_phones(raw_sms_client)
|
||||
self.sms_client_enabled = (os.getenv("ALERT_SMS_CLIENT_ENABLED", "1") == "1")
|
||||
|
||||
# SMTP
|
||||
self.smtp_host = os.getenv("SMTP_HOST")
|
||||
self.smtp_port = int(os.getenv("SMTP_PORT","465"))
|
||||
@@ -294,9 +295,9 @@ class Notifier:
|
||||
self.smtp_pass = os.getenv("SMTP_PASS")
|
||||
self.smtp_security = (os.getenv("SMTP_SECURITY","SSL") or "SSL").upper()
|
||||
|
||||
raw_mail_to = (os.getenv("MAIL_TO_Saclay") or os.getenv("MAIL_TO_SACLAY") or os.getenv("MAIL_TO") or "")
|
||||
raw_mail_to = (os.getenv(f"MAIL_TO_{SITE}") or os.getenv(f"MAIL_TO_{SITE.upper()}") or os.getenv("MAIL_TO") or "")
|
||||
self.mail_to = _split_list(raw_mail_to)
|
||||
self.mail_from = (os.getenv("MAIL_FROM_Saclay") or os.getenv("MAIL_FROM_SACLAY") or os.getenv("MAIL_FROM") or self.smtp_user)
|
||||
self.mail_from = (os.getenv(f"MAIL_FROM_{SITE}") or os.getenv(f"MAIL_FROM_{SITE.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])
|
||||
|
||||
@@ -345,6 +346,45 @@ class Notifier:
|
||||
except Exception as err:
|
||||
log.exception("Echec envoi SMS OVH: %s", err); return False
|
||||
|
||||
def send_sms_client(self, message: str, tag: str = f"monitor-client-{SITE.lower()}") -> bool:
|
||||
if not self.ovh_enabled:
|
||||
log.warning("SMS client: OVH non configuré."); return False
|
||||
if not self.sms_client_enabled or not self.sms_client_labeled:
|
||||
log.info("SMS client: désactivé ou aucun destinataire."); return False
|
||||
|
||||
only = os.getenv("ALERT_SMS_CLIENT_ONLY")
|
||||
if only:
|
||||
allow = {x.strip() for x in re.split(r"[;,]", only) if x.strip()}
|
||||
labeled = [(n, p) for (n, p) in self.sms_client_labeled if (n and n in allow) or (p in allow)]
|
||||
else:
|
||||
labeled = self.sms_client_labeled
|
||||
|
||||
receivers = [num for (_n, num) in labeled]
|
||||
if not receivers:
|
||||
log.info("SMS client: filtre vide → aucun envoi."); return False
|
||||
|
||||
message = normaliser_sms(message, prefix=SITE)
|
||||
|
||||
payload = {
|
||||
"sender": self.ovh_sender,
|
||||
"receivers": receivers,
|
||||
"message": message,
|
||||
"priority": "high",
|
||||
"coding": "7bit",
|
||||
"class": "phoneDisplay",
|
||||
"noStopClause": True,
|
||||
"senderForResponse": False,
|
||||
"validityPeriod": 2880,
|
||||
"tag": tag,
|
||||
}
|
||||
try:
|
||||
log.info("Envoi SMS CLIENT vers: %s", _human_labeled_list(labeled))
|
||||
resp = self.ovh_client.post(f"/sms/{self.ovh_service}/jobs", **payload)
|
||||
log.info("SMS CLIENT OVH envoyé (job ids=%s)", resp.get("ids"))
|
||||
return True
|
||||
except Exception as err:
|
||||
log.exception("Echec SMS CLIENT OVH: %s", err); return False
|
||||
|
||||
def send_email(self, subject: str, body: str) -> bool:
|
||||
if not self.smtp_enabled:
|
||||
log.warning("SMTP non configuré, email non envoyé."); return False
|
||||
@@ -423,6 +463,11 @@ def build_ok_text(site: str, sonde: str, temp: float, seuil: float, when: dt.dat
|
||||
txt = "\n".join(lines)
|
||||
return subject, txt, txt
|
||||
|
||||
def build_client_alert_sms(site: str, sonde: str, temp: float, seuil: float, when: dt.datetime | None = None) -> str:
|
||||
when = when or now_paris()
|
||||
# Court, 1 ligne; accents/° nettoyés par normaliser_sms
|
||||
return f"ALERTE CLIENT {sonde}: T={fmt_deg(temp)} > S={fmt_deg(seuil)} H:{when.strftime('%H:%M')}"
|
||||
|
||||
# ========= Gyrophare MQTT =========
|
||||
class MQTTPublisher:
|
||||
def __init__(self, site: str):
|
||||
@@ -513,7 +558,7 @@ class GyroPulseController:
|
||||
|
||||
Ajouts :
|
||||
- SMS ALERTE immédiat à l’allumage
|
||||
- SMS OK immédiat à l’extinction (option activée par défaut)
|
||||
- SMS OK immédiat à l’extinction (activé par défaut)
|
||||
"""
|
||||
def __init__(self, site: str, beacon, notifier, *,
|
||||
check_sec: int = int(os.getenv("GYRO_CHECK_SEC", "20")),
|
||||
@@ -536,7 +581,7 @@ class GyroPulseController:
|
||||
self._thread = None
|
||||
self._current = None # dernier état effectif
|
||||
|
||||
# Anti-spam SMS & SMS OK activé par défaut (tranquilliser)
|
||||
# Anti-spam SMS & SMS OK activé par défaut
|
||||
self._last_sms: dict[str, float] = {} # {sonde: ts dernier envoi}
|
||||
self._sms_min_sec = int(os.getenv("GYRO_SMS_MIN_SEC", "120"))
|
||||
self._send_ok = (os.getenv("ALERT_OK_SMS_GYRO", "1") == "1")
|
||||
@@ -668,19 +713,25 @@ beacon = MQTTPublisher(SITE)
|
||||
def notifier_sur_depassement(site: str, sonde: str, temp: float, seuil: float):
|
||||
"""
|
||||
MAIL quand l'alerte est confirmée (≥30 min) et ouverte en base.
|
||||
(Le SMS d'alerte est déjà envoyé par la boucle gyro, immédiat.)
|
||||
+ SMS CLIENT couplé (ALERTE_CLIENT_{SITE}).
|
||||
(Le SMS d'alerte interne est envoyé immédiatement par la boucle gyro.)
|
||||
"""
|
||||
subject, _sms_text, email_body = build_alert_text(site, sonde, temp, seuil)
|
||||
notifier.send_email(subject, email_body)
|
||||
notifier.send_email(subject, email_body) # MAIL (≥30 min)
|
||||
|
||||
# ➕ SMS client couplé au mail 30 min
|
||||
if os.getenv("ALERT_SMS_CLIENT_ENABLED", "1") == "1":
|
||||
client_msg = build_client_alert_sms(site, sonde, temp, seuil)
|
||||
notifier.send_sms_client(client_msg, tag=f"client-{SITE.lower()}")
|
||||
|
||||
def notifier_acquittement(site: str, sonde: str, temp: float, seuil: float):
|
||||
"""
|
||||
MAIL lorsque l’alerte est acquittée en base.
|
||||
(Le SMS "OK" a déjà été géré par la boucle gyro.)
|
||||
(Le SMS "OK" est envoyé immédiatement par la boucle gyro.)
|
||||
"""
|
||||
subject, sms_text, email_body = build_ok_text(site, sonde, temp, seuil)
|
||||
notifier.send_email(subject, email_body) # mail d'acquittement
|
||||
# Optionnel: SMS "OK" côté cycle si tu souhaites un doublon (désactivé par défaut)
|
||||
# Optionnel: SMS "OK" côté cycle si souhaité
|
||||
if os.getenv("ALERT_OK_SMS", "0") == "1":
|
||||
notifier.send_sms(sms_text)
|
||||
|
||||
@@ -701,7 +752,7 @@ def run_monitor_cycle(site: str = SITE):
|
||||
except Exception as e:
|
||||
log.exception("Erreur calcul alarme (info): %s", e)
|
||||
|
||||
# 3) Alertes "officielles" temporisées (≥30 min) → mails
|
||||
# 3) Alertes "officielles" temporisées (≥30 min) → mail + SMS client
|
||||
seuils = {s: meta["temp_max"] for s, meta in cfg.items() if meta.get("active", False)}
|
||||
|
||||
for r in last_rows:
|
||||
@@ -715,7 +766,7 @@ def run_monitor_cycle(site: str = SITE):
|
||||
try:
|
||||
conn = get_db()
|
||||
if open_alert(conn, f"Alertes_{site}", nom, now):
|
||||
notifier_sur_depassement(site, nom, temp, seuil) # MAIL alerte
|
||||
notifier_sur_depassement(site, nom, temp, seuil) # MAIL + SMS client
|
||||
finally:
|
||||
conn.close()
|
||||
else:
|
||||
@@ -771,3 +822,4 @@ if __name__ == "__main__":
|
||||
run_monitor_cycle(SITE)
|
||||
else:
|
||||
run_monitor_loop(SITE, period_sec=args.period)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user