Révision des codes alertes
This commit is contained in:
@@ -129,7 +129,7 @@ def lire_cfg_chambres(site: str):
|
||||
cfg: dict[str, dict] = {}
|
||||
try:
|
||||
cur = cnx.cursor()
|
||||
cur.execute(sql, (site,))
|
||||
cur.execute(sql, (site, ))
|
||||
for sonde, temp_max, etat, en_entretien in cur.fetchall():
|
||||
cfg[str(sonde)] = {
|
||||
"temp_max": float(temp_max),
|
||||
@@ -154,7 +154,7 @@ def compute_site_alarm(last_values: list[dict], cfg: dict[str, dict], hysteresis
|
||||
if not meta or not meta["active"] or meta["entretien"]:
|
||||
continue
|
||||
temp = float(row["Temperature"])
|
||||
if temp > float(meta["temp_max"]) + 0.0:
|
||||
if temp > float(meta["temp_max"]) + float(hysteresis):
|
||||
return True, (sonde, temp, float(meta["temp_max"]))
|
||||
return False, None
|
||||
|
||||
@@ -168,7 +168,7 @@ def lire_seuils_depuis_db(site: str):
|
||||
seuils = {}
|
||||
try:
|
||||
cur = cnx.cursor()
|
||||
cur.execute(sql, (site,))
|
||||
cur.execute(sql, (site, ))
|
||||
for sonde, s in cur.fetchall():
|
||||
seuils[str(sonde)] = float(s)
|
||||
return seuils
|
||||
@@ -190,7 +190,7 @@ def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
|
||||
WHERE Sonde=%s
|
||||
ORDER BY Date DESC
|
||||
LIMIT 1
|
||||
""", (sonde,))
|
||||
""", (sonde, ))
|
||||
last = cur.fetchone()
|
||||
if not last:
|
||||
return False
|
||||
@@ -229,6 +229,7 @@ def any_alert_open(site: str) -> bool:
|
||||
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()]
|
||||
@@ -401,18 +402,18 @@ def build_ok_text(site: str, sonde: str, temp: float, seuil: float, when: dt.dat
|
||||
# ========= Gyrophare MQTT =========
|
||||
class MQTTPublisher:
|
||||
def __init__(self, site: str):
|
||||
self.enabled = (_mqtt_ok and (os.getenv("GYRO_MODE", "").lower() == "mqtt"))
|
||||
self.enabled = bool(_mqtt_ok)
|
||||
self.site = site
|
||||
self.topic = (
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site}") or
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site.upper()}") or
|
||||
os.getenv("GYRO_MQTT_TOPIC") or
|
||||
f"Sondes/{site}/Gyro/cmd"
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site}") or
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site.upper()}") or
|
||||
os.getenv("GYRO_MQTT_TOPIC") or
|
||||
f"Sondes/{site}/Gyro/cmd"
|
||||
)
|
||||
self.last_state: bool | None = None
|
||||
|
||||
if not self.enabled:
|
||||
log.info("Gyro MQTT désactivé (GYRO_MODE != mqtt ou paho-mqtt absent).")
|
||||
log.info("Gyro MQTT désactivé (paho-mqtt absent).")
|
||||
return
|
||||
if not self.topic:
|
||||
log.warning("Topic MQTT manquant pour %s (GYRO_MQTT_TOPIC_%s)", site, site)
|
||||
@@ -428,20 +429,17 @@ class MQTTPublisher:
|
||||
# --- Création du client MQTT : compatible paho 1.x et 2.x ---
|
||||
cbver = getattr(mqtt, "CallbackAPIVersion", None)
|
||||
if cbver is not None:
|
||||
# paho >= 2.x : on choisit la meilleure constante disponible
|
||||
api_v = (
|
||||
getattr(cbver, "VERSION2", None) # paho 2.x
|
||||
or getattr(cbver, "V5", None) # certaines builds
|
||||
or getattr(cbver, "v5", None) # fallback
|
||||
or getattr(cbver, "V311", None) # dernier recours
|
||||
getattr(cbver, "VERSION2", None)
|
||||
or getattr(cbver, "V5", None)
|
||||
or getattr(cbver, "v5", None)
|
||||
or getattr(cbver, "V311", None)
|
||||
)
|
||||
try:
|
||||
self.client = mqtt.Client(callback_api_version=api_v) if api_v else mqtt.Client()
|
||||
except TypeError:
|
||||
# vieux paho ne supporte pas l’argument callback_api_version
|
||||
self.client = mqtt.Client()
|
||||
else:
|
||||
# paho 1.x
|
||||
self.client = mqtt.Client()
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -476,6 +474,125 @@ class MQTTPublisher:
|
||||
except Exception as e:
|
||||
log.exception("MQTT publish erreur: %s", e)
|
||||
|
||||
# ========= Contrôleur Gyro réactif (pulse/cooldown ou continu) =========
|
||||
import enum, threading
|
||||
|
||||
class _GyroState(enum.Enum):
|
||||
IDLE = 0
|
||||
PULSE_ON = 1
|
||||
COOLDOWN = 2
|
||||
|
||||
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.
|
||||
"""
|
||||
def __init__(self, site: str, beacon, *,
|
||||
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.check_sec = check_sec
|
||||
self.pulse_sec = pulse_sec
|
||||
self.cooldown_sec = cooldown_sec
|
||||
self.normal_confirm = normal_confirm
|
||||
|
||||
self.state = _GyroState.IDLE
|
||||
self._t_pulse_end = 0.0
|
||||
self._t_cooldown_end = 0.0
|
||||
self._normal_count = 0
|
||||
self._stop = threading.Event()
|
||||
self._thread = None
|
||||
self._current = None # dernier état envoyé pour éviter les doublons
|
||||
|
||||
def _set_gyro(self, on: bool):
|
||||
if self._current is not on:
|
||||
self.beacon.set(on)
|
||||
self._current = on
|
||||
|
||||
def start(self):
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
self._stop.clear()
|
||||
self._thread = threading.Thread(target=self._run, daemon=True)
|
||||
self._thread.start()
|
||||
log.info("GyroPulseController démarré (site=%s, check=%ss, pulse=%ss, cooldown=%ss, confirm=%d)",
|
||||
self.site, self.check_sec, self.pulse_sec, self.cooldown_sec, self.normal_confirm)
|
||||
|
||||
def stop(self):
|
||||
self._stop.set()
|
||||
|
||||
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}}
|
||||
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:
|
||||
active, trigger = self._is_alarm_now()
|
||||
except Exception as e:
|
||||
log.exception("Gyro fast-loop: erreur lecture état: %s", e)
|
||||
active, trigger = (False, None)
|
||||
|
||||
if self.state == _GyroState.IDLE:
|
||||
if active:
|
||||
self._set_gyro(True)
|
||||
self._t_pulse_end = now + self.pulse_sec
|
||||
self._normal_count = 0
|
||||
self.state = _GyroState.PULSE_ON
|
||||
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")
|
||||
|
||||
elif self.state == _GyroState.PULSE_ON:
|
||||
if not active:
|
||||
self._normal_count += 1
|
||||
if self._normal_count >= self.normal_confirm:
|
||||
self._set_gyro(False)
|
||||
self.state = _GyroState.IDLE
|
||||
self._normal_count = 0
|
||||
log.info("Gyro → OFF (retour à la normale confirmé)")
|
||||
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 now >= self._t_pulse_end:
|
||||
self._set_gyro(False)
|
||||
self._t_cooldown_end = now + self.cooldown_sec
|
||||
self.state = _GyroState.COOLDOWN
|
||||
log.info("Gyro → OFF, cooldown %ss (alerte persiste)", self.cooldown_sec)
|
||||
|
||||
elif self.state == _GyroState.COOLDOWN:
|
||||
if not active:
|
||||
self._normal_count += 1
|
||||
if self._normal_count >= self.normal_confirm:
|
||||
self.state = _GyroState.IDLE
|
||||
self._normal_count = 0
|
||||
log.info("Gyro: retour IDLE (plus d’alerte)")
|
||||
else:
|
||||
self._normal_count = 0
|
||||
if now >= self._t_cooldown_end:
|
||||
self._set_gyro(True)
|
||||
self._t_pulse_end = now + self.pulse_sec
|
||||
self.state = _GyroState.PULSE_ON
|
||||
log.info("Gyro → ON (re-pulse)")
|
||||
|
||||
time.sleep(self.check_sec)
|
||||
|
||||
# ========= Notifs haut-niveau =========
|
||||
notifier = Notifier()
|
||||
@@ -496,20 +613,18 @@ def run_monitor_cycle(site: str = SITE):
|
||||
last_rows = lire_sondes_depuis_db(site) # [{'Sonde','Temperature','Date'}]
|
||||
cfg = lire_cfg_chambres(site) # {sonde: {temp_max, active, entretien}}
|
||||
|
||||
# 2) Gyro instantané : ON si >=1 sonde active & non en entretien dépasse son seuil
|
||||
# 2) Gyro géré par le contrôleur rapide → ici, on ne touche plus au gyro
|
||||
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("Gyro %s => ON (déclenché par %s: %.2f > %.2f)", site, s, t, se)
|
||||
log.info("Dépassement détecté (pilotage gyro par boucle rapide) : %s %.2f > %.2f", s, t, se)
|
||||
else:
|
||||
log.info("Gyro %s => OFF (aucun dépassement)", site)
|
||||
beacon.set(gyro_on)
|
||||
log.info("Aucun dépassement au moment du cycle")
|
||||
except Exception as e:
|
||||
log.exception("Erreur calcul/publish gyrophare: %s", e)
|
||||
log.exception("Erreur calcul alarme (info): %s", e)
|
||||
|
||||
# 3) Alertes "officielles" (inchangées) avec temporisation 30 min
|
||||
# On reconstitue un dict seuils pour réutiliser ta logique existante en dessous.
|
||||
seuils = {s: meta["temp_max"] for s, meta in cfg.items() if meta.get("active", False)}
|
||||
|
||||
for r in last_rows:
|
||||
@@ -520,7 +635,6 @@ def run_monitor_cycle(site: str = SITE):
|
||||
now = now_paris()
|
||||
if temp > seuil:
|
||||
if depassement_depuis_30min(site, nom, seuil):
|
||||
# Ouvrir si pas déjà ouvert → notifier seulement si ouverture réelle
|
||||
try:
|
||||
conn = get_db()
|
||||
if open_alert(conn, f"Alertes_{site}", nom, now):
|
||||
@@ -528,7 +642,6 @@ def run_monitor_cycle(site: str = SITE):
|
||||
finally:
|
||||
conn.close()
|
||||
else:
|
||||
# Fermer si ouvert → notifier seulement si fermeture réelle
|
||||
try:
|
||||
conn = get_db()
|
||||
if close_alert(conn, f"Alertes_{site}", nom):
|
||||
@@ -536,10 +649,17 @@ def run_monitor_cycle(site: str = SITE):
|
||||
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)
|
||||
try:
|
||||
global _gyro_controller
|
||||
_gyro_controller = GyroPulseController(site, beacon)
|
||||
_gyro_controller.start()
|
||||
except Exception as e:
|
||||
log.exception("Impossible de démarrer le GyroPulseController: %s", e)
|
||||
|
||||
while True:
|
||||
t0 = time.time()
|
||||
try:
|
||||
@@ -560,10 +680,16 @@ if __name__ == "__main__":
|
||||
p.add_argument("--once", action="store_true")
|
||||
args = p.parse_args()
|
||||
|
||||
if args.test_sms: notifier.send_sms("TEST DOMO91 (transactionnel)")
|
||||
elif args.test_mail: notifier.send_email(f"[TEST {SITE}] Mail", "OK")
|
||||
elif args.test_alert: notifier_sur_depassement(SITE, "Congelateur", -14.5, -15.0)
|
||||
elif args.test_ok: notifier_acquittement(SITE, "Congelateur", -15.2, -15.0)
|
||||
if args.test_sms:
|
||||
notifier.send_sms("TEST DOMO91 (transactionnel)")
|
||||
elif args.test_mail:
|
||||
notifier.send_email(f"[TEST {SITE}] Mail", "OK")
|
||||
elif args.test_alert:
|
||||
notifier_sur_depassement(SITE, "Congelateur", -14.5, -15.0)
|
||||
elif args.test_ok:
|
||||
notifier_acquittement(SITE, "Congelateur", -15.2, -15.0)
|
||||
else:
|
||||
if args.once: run_monitor_cycle(SITE)
|
||||
else: run_monitor_loop(SITE, period_sec=args.period)
|
||||
if args.once:
|
||||
run_monitor_cycle(SITE)
|
||||
else:
|
||||
run_monitor_loop(SITE, period_sec=args.period)
|
||||
|
||||
Reference in New Issue
Block a user