Règlages des alertes dans Monitor_Saclay
This commit is contained in:
19
.env
19
.env
@@ -1,4 +1,3 @@
|
||||
|
||||
# OVH_SMS_SENDER=DOMO91FR
|
||||
#connexion mysql
|
||||
DB_HOST=162.19.78.131
|
||||
@@ -10,15 +9,20 @@ DB_NAME=Sondes
|
||||
MQTT_HOST=54.36.188.119
|
||||
MQTT_USER=Bwps
|
||||
MQTT_PASS="scJ5ACj2keRfI^"
|
||||
|
||||
# Boucle rapide du gyro
|
||||
GYRO_MODE=mqtt
|
||||
GYRO_CHECK_SEC=20
|
||||
GYRO_NORMAL_CONFIRM=2
|
||||
GYRO_MODE_CONTINUOUS=1
|
||||
GYRO_HYSTERESIS=0.3
|
||||
ALERT_OK_SMS_GYRO=1
|
||||
ALERT_OK_SMS=0
|
||||
GYRO_SMS_MIN_SEC=120
|
||||
GYRO_MQTT_TOPIC_SACLAY=Saclay/gyrophare
|
||||
GYRO_MQTT_TOPIC_MEUDON=Meudon/gyrophare
|
||||
|
||||
|
||||
# paramètres mail
|
||||
SMTP_HOST=ssl0.ovh.net
|
||||
SMTP_PORT=587
|
||||
@@ -26,9 +30,9 @@ SMTP_SECURITY=STARTTLS
|
||||
SMTP_USER=services@domo91.fr
|
||||
SMTP_PASS=6ZiCsVtSf9@nEHv@$^0
|
||||
MAIL_FROM=services@domo91.fr
|
||||
MAIL_TO_SACLAY=robots@domo91.fr
|
||||
MAIL_TO_SACLAY=robots@domo91.fr,nicolas.thibaut@bw-paris-saclay.com
|
||||
MAIL_FROM_SACLAY="DOMO91 Saclay <services@domo91.fr>"
|
||||
MAIL_TO_MEUDON=robots@domo91.fr
|
||||
MAIL_TO_MEUDON=robots@domo91.fr,superviseur.restauration@parismeudonermitage.com,chef@parismeudonermitage.com
|
||||
MAIL_FROM_MEUDON="DOMO91 Meudon <services@domo91.fr>"
|
||||
|
||||
# --- Paramètres SMS ----
|
||||
@@ -41,9 +45,6 @@ OVH_SMS_SENDER=DOMO91FR
|
||||
SMS_RECEIVER=+33635164680
|
||||
ALERT_SMS_TO_SACLAY==Michel:+33635164680
|
||||
ALERT_SMS_TO_MEUDON=Michel:+33635164680
|
||||
|
||||
# --- Réserves destinataires ----
|
||||
SMS_SACLAY=Nicolas:+33682069405,Sabrina:+33650270939,Mirceta:+33601162960
|
||||
SMS_MEUDON=Sekou:+33625903364,Damien:+33680388259,Manon:+33631127248
|
||||
SACLAY_MAIL=nicolas.thibaut@bw-paris-saclay.com
|
||||
MEUDON_MAIL=superviseur.restauration@parismeudonermitage.com,chef@parismeudonermitage.com
|
||||
# ---- Reserve SMS
|
||||
#Nicolas:+33682069405,Sabrina:+33650270939,Mirceta:+33601162960
|
||||
#Sekou:+33625903364,Damien:+33680388259
|
||||
@@ -12,10 +12,13 @@ from email.message import EmailMessage
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
load_dotenv(find_dotenv(usecwd=True), override=False)
|
||||
from utils_sms import normaliser_sms
|
||||
|
||||
# MySQL
|
||||
import mysql.connector
|
||||
from mysql.connector import Error as MySQLError
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# OVH (SMS)
|
||||
try:
|
||||
@@ -43,7 +46,7 @@ if not log.handlers:
|
||||
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).
|
||||
Retourne True si une nouvelle alerte a été créée (→ notifier par mail).
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
@@ -52,20 +55,19 @@ def open_alert(conn, table_alertes: str, sonde: str, dt: datetime) -> bool:
|
||||
)
|
||||
if cur.fetchone():
|
||||
cur.close()
|
||||
return False # déjà ouverte → pas de notif
|
||||
|
||||
return False # déjà ouverte
|
||||
cur.execute(
|
||||
f"INSERT INTO `{table_alertes}` (Sonde, Debut_defaut, Etat) VALUES (%s, %s, 'En cours')",
|
||||
(sonde, dt.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
return True # nouvelle alerte → notifier
|
||||
return True
|
||||
|
||||
def close_alert(conn, table_alertes: str, sonde: str) -> bool:
|
||||
"""
|
||||
Ferme l'alerte 'En cours' si présente.
|
||||
Retourne True si une alerte est passée à 'Acquitté' (→ notifier).
|
||||
Retourne True si une alerte est passée à 'Acquitté' (→ notifier par mail).
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
@@ -77,10 +79,10 @@ def close_alert(conn, table_alertes: str, sonde: str) -> bool:
|
||||
changed = (cur.rowcount == 1)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
return changed # True → notifier, False → rien
|
||||
return changed
|
||||
|
||||
def get_db():
|
||||
cnx = mysql.connector.connect(
|
||||
return mysql.connector.connect(
|
||||
host=os.getenv("DB_HOST"),
|
||||
user=os.getenv("DB_USER"),
|
||||
password=os.getenv("DB_PASS"),
|
||||
@@ -88,7 +90,6 @@ def get_db():
|
||||
port=int(os.getenv("DB_PORT", "3306")),
|
||||
autocommit=True,
|
||||
)
|
||||
return cnx
|
||||
|
||||
def lire_sondes_depuis_db(site: str):
|
||||
table = site
|
||||
@@ -117,7 +118,7 @@ def lire_sondes_depuis_db(site: str):
|
||||
|
||||
def lire_cfg_chambres(site: str):
|
||||
"""
|
||||
Retourne un dict {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
||||
Retourne {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
||||
depuis Chambres_froides pour le site.
|
||||
"""
|
||||
sql = """
|
||||
@@ -145,8 +146,7 @@ def lire_cfg_chambres(site: str):
|
||||
|
||||
def compute_site_alarm(last_values: list[dict], cfg: dict[str, dict], hysteresis: float = 0.0):
|
||||
"""
|
||||
Retourne (is_on: bool, trigger: tuple[str,float,float] | None)
|
||||
trigger = (sonde, temp, seuil) si dépassement détecté.
|
||||
Retourne (is_on: bool, trigger: (sonde,temp,seuil) | None)
|
||||
"""
|
||||
for row in last_values:
|
||||
sonde = str(row["Sonde"])
|
||||
@@ -178,45 +178,54 @@ def lire_seuils_depuis_db(site: str):
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
|
||||
def depassement_depuis_30min(site: str, sonde: str, seuil: float, window_min: int = 180) -> 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.
|
||||
"""
|
||||
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
|
||||
LIMIT 1
|
||||
""", (sonde, ))
|
||||
last = cur.fetchone()
|
||||
if not last:
|
||||
""", (sonde, int(window_min)))
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
return False
|
||||
last_temp, last_date = float(last[0]), last[1]
|
||||
|
||||
# 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
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT MIN(Date)
|
||||
FROM `{table}`
|
||||
WHERE Sonde=%s
|
||||
AND Temperature > %s
|
||||
AND Date >= (NOW() - INTERVAL 120 MINUTE)
|
||||
""", (sonde, float(seuil)))
|
||||
first_over = cur.fetchone()[0]
|
||||
if not first_over:
|
||||
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)
|
||||
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é
|
||||
else:
|
||||
break # séquence continue terminée
|
||||
|
||||
# 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=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)
|
||||
log.exception("Erreur DB (depassement_depuis_30min, continu): %s", err)
|
||||
return False
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
|
||||
def any_alert_open(site: str) -> bool:
|
||||
table = f"Alertes_{site}"
|
||||
cnx = get_db()
|
||||
@@ -300,10 +309,13 @@ class Notifier:
|
||||
log.warning("ALERT_SMS_ONLY filtre tous les destinataires (aucun envoi).")
|
||||
return False
|
||||
|
||||
# ✅ Normalisation GSM-7 + préfixe site
|
||||
message = normaliser_sms(message, prefix=SITE)
|
||||
|
||||
payload = {
|
||||
"sender": self.ovh_sender,
|
||||
"receivers": receivers,
|
||||
"message": message[:1600],
|
||||
"message": message,
|
||||
"priority": "high",
|
||||
"coding": "7bit",
|
||||
"class": "phoneDisplay",
|
||||
@@ -388,14 +400,26 @@ def now_paris() -> dt.datetime:
|
||||
def build_alert_text(site: str, sonde: str, temp: float, seuil: float, when: dt.datetime | None = None):
|
||||
when = when or now_paris()
|
||||
subject = f"[ALERTE {site}] {sonde} au-dessus du seuil"
|
||||
lines = [subject + ":", f"Sonde: {sonde}", f"Température: {fmt_deg(temp)} (seuil {fmt_deg(seuil)})", f"Site: {site}", f"Heure: {when.strftime('%Y-%m-%d %H:%M:%S')}"]
|
||||
lines = [
|
||||
subject + ":",
|
||||
f"Sonde: {sonde}",
|
||||
f"Température: {fmt_deg(temp)} (seuil {fmt_deg(seuil)})",
|
||||
f"Site: {site}",
|
||||
f"Heure: {when.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
]
|
||||
txt = "\n".join(lines)
|
||||
return subject, txt, txt
|
||||
|
||||
def build_ok_text(site: str, sonde: str, temp: float, seuil: float, when: dt.datetime | None = None):
|
||||
when = when or now_paris()
|
||||
subject = f"[OK {site}] {sonde} revenue normale"
|
||||
lines = [subject + ":", f"Sonde: {sonde}", f"Température: {fmt_deg(temp)} <= seuil {fmt_deg(seuil)}", f"Site: {site}", f"Heure: {when.strftime('%Y-%m-%d %H:%M:%S')}"]
|
||||
lines = [
|
||||
subject + ":",
|
||||
f"Sonde: {sonde}",
|
||||
f"Température: {fmt_deg(temp)} <= seuil {fmt_deg(seuil)}",
|
||||
f"Site: {site}",
|
||||
f"Heure: {when.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
]
|
||||
txt = "\n".join(lines)
|
||||
return subject, txt, txt
|
||||
|
||||
@@ -461,7 +485,6 @@ class MQTTPublisher:
|
||||
return
|
||||
if self.last_state is not None and self.last_state == on:
|
||||
return
|
||||
|
||||
payload = "ON" if on else "OFF"
|
||||
try:
|
||||
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
||||
@@ -474,7 +497,7 @@ class MQTTPublisher:
|
||||
except Exception as e:
|
||||
log.exception("MQTT publish erreur: %s", e)
|
||||
|
||||
# ========= Contrôleur Gyro réactif (pulse/cooldown ou continu) =========
|
||||
# ========= Contrôleur Gyro réactif =========
|
||||
import enum, threading
|
||||
|
||||
class _GyroState(enum.Enum):
|
||||
@@ -484,18 +507,22 @@ class _GyroState(enum.Enum):
|
||||
|
||||
class GyroPulseController:
|
||||
"""
|
||||
Boucle rapide indépendante : lit les dernières mesures + config
|
||||
et applique un automate :
|
||||
- MODE CONTINU (par défaut) : ON tant que l’alarme persiste, OFF rapide si retour normal.
|
||||
- MODE PULSE : IDLE → PULSE_ON (ON PULSE_SEC) → COOLDOWN (OFF COOLDOWN_SEC) → re-PULSE tant que l’alarme persiste.
|
||||
Boucle rapide indépendante :
|
||||
- MODE CONTINU (défaut) : ON tant que l’alarme persiste, OFF quand normal confirmé.
|
||||
- MODE PULSE : ON (PULSE_SEC) puis OFF (COOLDOWN_SEC), tant que l’alarme persiste.
|
||||
|
||||
Ajouts :
|
||||
- SMS ALERTE immédiat à l’allumage
|
||||
- SMS OK immédiat à l’extinction (option activée par défaut)
|
||||
"""
|
||||
def __init__(self, site: str, beacon, *,
|
||||
def __init__(self, site: str, beacon, notifier, *,
|
||||
check_sec: int = int(os.getenv("GYRO_CHECK_SEC", "20")),
|
||||
pulse_sec: int = int(os.getenv("GYRO_PULSE_SEC", "60")),
|
||||
cooldown_sec: int = int(os.getenv("GYRO_COOLDOWN_SEC", "600")),
|
||||
normal_confirm: int = int(os.getenv("GYRO_NORMAL_CONFIRM", "2"))):
|
||||
self.site = site
|
||||
self.beacon = beacon
|
||||
self.notifier = notifier
|
||||
self.check_sec = check_sec
|
||||
self.pulse_sec = pulse_sec
|
||||
self.cooldown_sec = cooldown_sec
|
||||
@@ -507,7 +534,15 @@ class GyroPulseController:
|
||||
self._normal_count = 0
|
||||
self._stop = threading.Event()
|
||||
self._thread = None
|
||||
self._current = None # dernier état envoyé pour éviter les doublons
|
||||
self._current = None # dernier état effectif
|
||||
|
||||
# Anti-spam SMS & SMS OK activé par défaut (tranquilliser)
|
||||
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")
|
||||
|
||||
# Conserver le dernier déclencheur (pour SMS OK)
|
||||
self._last_trigger: tuple[str, float, float] | None = None # (sonde, temp, seuil)
|
||||
|
||||
def _set_gyro(self, on: bool):
|
||||
if self._current is not on:
|
||||
@@ -526,16 +561,48 @@ class GyroPulseController:
|
||||
def stop(self):
|
||||
self._stop.set()
|
||||
|
||||
def _sms_can_send(self, sonde: str) -> bool:
|
||||
t = time.time()
|
||||
last = self._last_sms.get(sonde, 0.0)
|
||||
if (t - last) >= self._sms_min_sec:
|
||||
self._last_sms[sonde] = t
|
||||
return True
|
||||
return False
|
||||
|
||||
def _send_alert_sms(self, trigger: tuple[str, float, float] | None):
|
||||
if not trigger:
|
||||
return
|
||||
sonde, temp, seuil = trigger
|
||||
if self._sms_can_send(sonde):
|
||||
_, sms_text, _ = build_alert_text(self.site, sonde, temp, seuil, when=now_paris())
|
||||
self.notifier.send_sms(sms_text)
|
||||
|
||||
def _send_ok_sms_from_last_trigger(self):
|
||||
if not self._send_ok or not self._last_trigger:
|
||||
return
|
||||
sonde, _temp_prev, seuil = self._last_trigger
|
||||
|
||||
# Température courante pour le SMS OK
|
||||
rows = lire_sondes_depuis_db(self.site)
|
||||
curr_temp = None
|
||||
for r in rows:
|
||||
if str(r["Sonde"]) == sonde:
|
||||
curr_temp = float(r["Temperature"]); break
|
||||
if curr_temp is None:
|
||||
curr_temp = seuil - 0.1 # fallback léger
|
||||
|
||||
if self._sms_can_send(sonde):
|
||||
_, sms_text, _ = build_ok_text(self.site, sonde, curr_temp, seuil, when=now_paris())
|
||||
self.notifier.send_sms(sms_text)
|
||||
|
||||
self._last_trigger = None # reset
|
||||
|
||||
def _is_alarm_now(self) -> tuple[bool, tuple[str, float, float] | None]:
|
||||
"""
|
||||
Réutilise tes fonctions existantes pour décider rapidement.
|
||||
"""
|
||||
last_rows = lire_sondes_depuis_db(self.site) # [{'Sonde','Temperature','Date'}]
|
||||
cfg = lire_cfg_chambres(self.site) # {sonde: {temp_max, active, entretien}}
|
||||
last_rows = lire_sondes_depuis_db(self.site)
|
||||
cfg = lire_cfg_chambres(self.site)
|
||||
return compute_site_alarm(last_rows, cfg, hysteresis=float(os.getenv("GYRO_HYSTERESIS", "0.0")))
|
||||
|
||||
def _run(self):
|
||||
import time
|
||||
while not self._stop.is_set():
|
||||
now = time.time()
|
||||
try:
|
||||
@@ -550,10 +617,13 @@ class GyroPulseController:
|
||||
self._t_pulse_end = now + self.pulse_sec
|
||||
self._normal_count = 0
|
||||
self.state = _GyroState.PULSE_ON
|
||||
self._last_trigger = trigger
|
||||
if trigger:
|
||||
s, t, se = trigger
|
||||
log.info("Gyro → ON déclenché par %s: %.2f > %.2f (mode %s)",
|
||||
s, t, se, "CONTINU" if os.getenv("GYRO_MODE_CONTINUOUS","1")=="1" else "PULSE")
|
||||
# SMS alerte immédiat
|
||||
self._send_alert_sms(trigger)
|
||||
|
||||
elif self.state == _GyroState.PULSE_ON:
|
||||
if not active:
|
||||
@@ -563,14 +633,11 @@ class GyroPulseController:
|
||||
self.state = _GyroState.IDLE
|
||||
self._normal_count = 0
|
||||
log.info("Gyro → OFF (retour à la normale confirmé)")
|
||||
# SMS OK immédiat
|
||||
self._send_ok_sms_from_last_trigger()
|
||||
else:
|
||||
self._normal_count = 0
|
||||
# --- MODE CONTINU par défaut ---
|
||||
if os.getenv("GYRO_MODE_CONTINUOUS", "1") == "1":
|
||||
# Rester ON tant que l’alerte persiste
|
||||
pass
|
||||
else:
|
||||
# --- MODE PULSE ---
|
||||
if os.getenv("GYRO_MODE_CONTINUOUS", "1") != "1":
|
||||
if now >= self._t_pulse_end:
|
||||
self._set_gyro(False)
|
||||
self._t_cooldown_end = now + self.cooldown_sec
|
||||
@@ -599,63 +666,73 @@ notifier = Notifier()
|
||||
beacon = MQTTPublisher(SITE)
|
||||
|
||||
def notifier_sur_depassement(site: str, sonde: str, temp: float, seuil: float):
|
||||
subject, sms_text, email_body = build_alert_text(site, sonde, temp, seuil)
|
||||
notifier.send_sms(sms_text)
|
||||
"""
|
||||
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.)
|
||||
"""
|
||||
subject, _sms_text, email_body = build_alert_text(site, sonde, temp, seuil)
|
||||
notifier.send_email(subject, email_body)
|
||||
|
||||
def notifier_acquittement(site: str, sonde: str, temp: float, seuil: float):
|
||||
subject, sms_text, _ = build_ok_text(site, sonde, temp, seuil)
|
||||
notifier.send_sms(sms_text)
|
||||
"""
|
||||
MAIL lorsque l’alerte est acquittée en base.
|
||||
(Le SMS "OK" a déjà été géré 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)
|
||||
if os.getenv("ALERT_OK_SMS", "0") == "1":
|
||||
notifier.send_sms(sms_text)
|
||||
|
||||
# ========= Cycle & boucle =========
|
||||
def run_monitor_cycle(site: str = SITE):
|
||||
# 1) Lecture dernières mesures + config chambres
|
||||
last_rows = lire_sondes_depuis_db(site) # [{'Sonde','Temperature','Date'}]
|
||||
cfg = lire_cfg_chambres(site) # {sonde: {temp_max, active, entretien}}
|
||||
# 1) Lecture dernières mesures + config
|
||||
last_rows = lire_sondes_depuis_db(site)
|
||||
cfg = lire_cfg_chambres(site)
|
||||
|
||||
# 2) Gyro géré par le contrôleur rapide → ici, on ne touche plus au gyro
|
||||
# 2) Info: état instantané (le gyro est piloté par la boucle rapide)
|
||||
try:
|
||||
gyro_on, trigger = compute_site_alarm(last_rows, cfg, hysteresis=float(os.getenv("GYRO_HYSTERESIS", "0.0")))
|
||||
if trigger:
|
||||
s, t, se = trigger
|
||||
log.info("Dépassement détecté (pilotage gyro par boucle rapide) : %s %.2f > %.2f", s, t, se)
|
||||
log.info("Dépassement détecté (gyro géré par boucle rapide) : %s %.2f > %.2f", s, t, se)
|
||||
else:
|
||||
log.info("Aucun dépassement au moment du cycle")
|
||||
except Exception as e:
|
||||
log.exception("Erreur calcul alarme (info): %s", e)
|
||||
|
||||
# 3) Alertes "officielles" (inchangées) avec temporisation 30 min
|
||||
# 3) Alertes "officielles" temporisées (≥30 min) → mails
|
||||
seuils = {s: meta["temp_max"] for s, meta in cfg.items() if meta.get("active", False)}
|
||||
|
||||
for r in last_rows:
|
||||
nom = str(r["Sonde"])
|
||||
temp = float(r["Temperature"])
|
||||
seuil = float(seuils.get(nom, 6.0))
|
||||
|
||||
now = now_paris()
|
||||
|
||||
if temp > seuil:
|
||||
if depassement_depuis_30min(site, nom, seuil):
|
||||
try:
|
||||
conn = get_db()
|
||||
if open_alert(conn, f"Alertes_{site}", nom, now):
|
||||
notifier_sur_depassement(site, nom, temp, seuil)
|
||||
notifier_sur_depassement(site, nom, temp, seuil) # MAIL alerte
|
||||
finally:
|
||||
conn.close()
|
||||
else:
|
||||
try:
|
||||
conn = get_db()
|
||||
if close_alert(conn, f"Alertes_{site}", nom):
|
||||
notifier_acquittement(site, nom, temp, seuil)
|
||||
notifier_acquittement(site, nom, temp, seuil) # MAIL acquittement
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def run_monitor_loop(site: str = SITE, period_sec: int = 300):
|
||||
log.info("%s démarré (site=%s, période=%ss) ✅", PROGRAM_NAME, site, period_sec)
|
||||
|
||||
# Démarrage du contrôleur gyro rapide (thread)
|
||||
# Démarrage du contrôleur gyro rapide (thread) + notifier pour SMS immédiats
|
||||
try:
|
||||
global _gyro_controller
|
||||
_gyro_controller = GyroPulseController(site, beacon)
|
||||
_gyro_controller = GyroPulseController(site, beacon, notifier)
|
||||
_gyro_controller.start()
|
||||
except Exception as e:
|
||||
log.exception("Impossible de démarrer le GyroPulseController: %s", e)
|
||||
@@ -681,7 +758,8 @@ if __name__ == "__main__":
|
||||
args = p.parse_args()
|
||||
|
||||
if args.test_sms:
|
||||
notifier.send_sms("TEST DOMO91 (transactionnel)")
|
||||
n = Notifier()
|
||||
n.send_sms("TEST DOMO91 (transactionnel)")
|
||||
elif args.test_mail:
|
||||
notifier.send_email(f"[TEST {SITE}] Mail", "OK")
|
||||
elif args.test_alert:
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import os
|
||||
import ovh
|
||||
from dotenv import load_dotenv
|
||||
import unicodedata, re
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def normaliser_sms(message: str, prefix: str = "") -> str:
|
||||
REPL = {"°": "C", "’": "'", "“": '"', "”": '"', "–": "-", "—": "-", "…": "..."}
|
||||
for k, v in REPL.items():
|
||||
message = message.replace(k, v)
|
||||
message = unicodedata.normalize("NFKD", message).encode("ascii", "ignore").decode("ascii")
|
||||
rules = [
|
||||
(r"\bTemperature\b", "T"),
|
||||
(r"\bTemp[ée]rature\b", "T"),
|
||||
(r"\bSeuil\b", "S"),
|
||||
(r"\bHeure\b", "H"),
|
||||
(r"\s{2,}", " "),
|
||||
]
|
||||
for pat, repl in rules:
|
||||
message = re.sub(pat, repl, message, flags=re.IGNORECASE)
|
||||
if prefix:
|
||||
message = f"[{prefix}] {message}"
|
||||
return message[:160]
|
||||
|
||||
def envoyer_sms(message: str, lieu: str = ""):
|
||||
try:
|
||||
client = ovh.Client(
|
||||
@@ -28,7 +47,7 @@ def envoyer_sms(message: str, lieu: str = ""):
|
||||
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
|
||||
|
||||
message = normaliser_sms(message, prefix=lieu)
|
||||
payload = {
|
||||
"sender": sender,
|
||||
"receivers": [numero_dest],
|
||||
|
||||
Reference in New Issue
Block a user