From d54832e558a636091dd809202a81fe421bc4d7f8 Mon Sep 17 00:00:00 2001 From: Michel Date: Mon, 15 Dec 2025 11:43:33 +0100 Subject: [PATCH] =?UTF-8?q?Consolidation=20de=20Domo91=20et=20cosm=C3=A9ti?= =?UTF-8?q?que?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 17 +- app/Monitor_Meudon.py | 82 +++++++- app/Monitor_Saclay.py | 39 ++-- app/domo91.py | 150 +++++++++------ app/gyro_control.py | 179 +++++++++--------- app/mqtt_watchdog.py | 6 +- .../site-packages/pip/_vendor/certifi/core.py | 2 - 7 files changed, 288 insertions(+), 187 deletions(-) diff --git a/.env b/.env index a344576..1d81e62 100644 --- a/.env +++ b/.env @@ -13,9 +13,11 @@ MQTT_PASS=3J@bjYP0 MQTT_PORT_MEUDON=1883 # Boucle rapide du gyro -GYRO_MODE=mqtt +GYRO_WINDOW_MIN=3 +GYRO_NEEDED_POINTS=2 GYRO_CHECK_SEC=20 -GYRO_NORMAL_CONFIRM=2 +GYRO_NORMAL_CONFIRM=6 +GYRO_MODE=mqtt GYRO_MODE_CONTINUOUS=1 GYRO_HYSTERESIS=0.3 ALERT_OK_SMS_GYRO=0 @@ -30,17 +32,6 @@ ALERT_LOOKBACK_MINUTES=120 # Logs LOGLEVEL=INFO -# === Connexion SSH pour visualiseur_logs.py === -SSH_HOST=162.19.78.131 -SSH_PORT=22 -SSH_USER=debian -SSH_KEY_PATH=/home/debian/.ssh/id_ed25519 -SSH_KEY_PASSPHRASE='gaby' -SSH_LOG_DIR=/home/debian/Gestion_sondes/Logs -VPS_HOST=162.19.78.131 - - - # paramètres mail SMTP_HOST=ssl0.ovh.net SMTP_PORT=587 diff --git a/app/Monitor_Meudon.py b/app/Monitor_Meudon.py index 47b99fb..510109b 100644 --- a/app/Monitor_Meudon.py +++ b/app/Monitor_Meudon.py @@ -9,7 +9,7 @@ Principes : - DB_HOST / MQTT_HOST / SMTP_HOST : uniques (VPS unique) - Paramètres par site via env : MAIL_TO_{SITE}, ALERT_SMS_TO_{SITE}, etc. - Les alertes ne concernent QUE les sondes présentes dans Chambres_froides pour le site - et avec Etat=ON et En_entretien=0. + et avec Etat=ON . """ import datetime as dt @@ -274,7 +274,7 @@ def lire_cfg_chambres(site: str): """ dbname = os.getenv("DB_NAME", "Sondes") sql = f""" - SELECT Sonde, Temp_Max, Etat, En_entretien + SELECT Sonde, Temp_Max, Etat FROM `{dbname}`.`Chambres_froides` WHERE Lieu=%s """ @@ -283,11 +283,10 @@ def lire_cfg_chambres(site: str): try: cur = cnx.cursor() cur.execute(sql, (site,)) - for sonde, temp_max, etat, en_entretien in cur.fetchall(): + for sonde, temp_max, etat in cur.fetchall(): cfg[str(sonde)] = { "temp_max": float(temp_max), "active": str(etat).upper() == "ON", - "entretien": bool(int(en_entretien or 0)), } return cfg except MySQLError as err: @@ -307,7 +306,7 @@ def compute_site_alarm(last_values: list[dict], cfg: dict[str, dict], hysteresis meta = cfg.get(sonde) if not meta: continue - if (not meta["active"]) or meta["entretien"]: + if not meta["active"]: continue temp = float(row["Temperature"]) seuil = float(meta["temp_max"]) @@ -315,6 +314,70 @@ def compute_site_alarm(last_values: list[dict], cfg: dict[str, dict], hysteresis return True, (sonde, temp, seuil) return False, None +def depassement_depuis_2min(site: str, window_min: int = 3, needed_points: int = 2): + """ + Retourne (active: bool, trigger: (sonde, temp, seuil) | None) + active = True si une sonde dépasse son seuil sur au moins 'needed_points' mesures + dans les 'window_min' dernières minutes. + + window_min=3 rend le système tolérant aux petits décalages (ex: sonde en retard). + needed_points=2 correspond à votre objectif "2 minutes" (sondes toutes les minutes). + """ + table = safe_site(site) + dbname = os.getenv("DB_NAME", "Sondes") + + cnx = get_db() + try: + cur = cnx.cursor() + + # 1) Cherche une sonde qui dépasse >=2 fois dans la fenêtre + cur.execute(f""" + SELECT m.Sonde + FROM `{table}` m + JOIN `{dbname}`.`Chambres_froides` c + ON c.Lieu=%s + AND c.Sonde=m.Sonde + AND UPPER(c.Etat)='ON' + WHERE m.Date >= NOW() - INTERVAL %s MINUTE + AND m.Temperature IS NOT NULL + AND m.Temperature > c.Temp_Max + GROUP BY m.Sonde + HAVING COUNT(*) >= %s + LIMIT 1 + """, (site, window_min, needed_points)) + + row = cur.fetchone() + if not row: + return False, None + + sonde = str(row[0]) + + # 2) Récupère dernière température + seuil pour le trigger + cur.execute(f""" + SELECT m.Temperature, c.Temp_Max + FROM `{table}` m + JOIN `{dbname}`.`Chambres_froides` c + ON c.Lieu=%s AND c.Sonde=%s + WHERE m.Sonde=%s + AND m.Temperature IS NOT NULL + ORDER BY m.Date DESC + LIMIT 1 + """, (site, sonde, sonde)) + + trow = cur.fetchone() + if not trow: + return True, (sonde, 0.0, 0.0) + + temp = float(trow[0]) + seuil = float(trow[1]) + return True, (sonde, temp, seuil) + + except MySQLError as err: + log.exception("Erreur DB (depassement_depuis_2min): %s", err) + return False, None + finally: + cnx.close() + def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool: """ @@ -820,9 +883,10 @@ class GyroPulseController: self._last_trigger = None def _is_alarm_now(self) -> tuple[bool, tuple[str, float, float] | None]: - 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"))) + # Déclenchement "2 minutes" (2 points au-dessus du seuil sur ~3 minutes) + window_min = int(os.getenv("GYRO_WINDOW_MIN", "3")) + needed = int(os.getenv("GYRO_NEEDED_POINTS", "2")) + return depassement_depuis_2min(self.site, window_min=window_min, needed_points=needed) def _run(self): while not self._stop.is_set(): @@ -944,7 +1008,7 @@ def run_monitor_cycle(site: str, notifier: Notifier): meta = cfg.get(nom) if not meta: continue - if (not meta["active"]) or meta["entretien"]: + if not meta["active"]: continue seuil = float(meta["temp_max"]) diff --git a/app/Monitor_Saclay.py b/app/Monitor_Saclay.py index 4dc42e5..849d9a8 100644 --- a/app/Monitor_Saclay.py +++ b/app/Monitor_Saclay.py @@ -165,24 +165,24 @@ def lire_sondes_depuis_db(site: str): def lire_cfg_chambres(site: str): """ - Retourne {sonde: {"temp_max": float, "active": bool, "entretien": bool}} + Retourne {sonde: {"temp_max": float, "active": bool}} depuis Chambres_froides pour le site. """ - sql = """ - SELECT Sonde, Temp_Max, Etat, En_entretien - FROM Sondes.Chambres_froides + dbname = os.getenv("DB_NAME", "Sondes") + sql = f""" + SELECT Sonde, Temp_Max, Etat + FROM `{dbname}`.`Chambres_froides` WHERE Lieu=%s """ cnx = get_db() cfg: dict[str, dict] = {} try: cur = cnx.cursor() - cur.execute(sql, (site, )) - for sonde, temp_max, etat, en_entretien in cur.fetchall(): + cur.execute(sql, (site,)) + for sonde, temp_max, etat in cur.fetchall(): cfg[str(sonde)] = { "temp_max": float(temp_max), "active": str(etat).upper() == "ON", - "entretien": bool(int(en_entretien or 0)), } return cfg except MySQLError as err: @@ -198,11 +198,12 @@ def compute_site_alarm(last_values: list[dict], cfg: dict[str, dict], hysteresis for row in last_values: sonde = str(row["Sonde"]) meta = cfg.get(sonde) - if not meta or not meta["active"] or meta["entretien"]: + if not meta or not meta.get("active", False): continue temp = float(row["Temperature"]) - if temp > float(meta["temp_max"]) + float(hysteresis): - return True, (sonde, temp, float(meta["temp_max"])) + seuil = float(meta["temp_max"]) + if temp > seuil + float(hysteresis): + return True, (sonde, temp, seuil) return False, None def lire_seuils_depuis_db(site: str): @@ -883,24 +884,32 @@ def run_monitor_cycle(site: str = SITE): for r in last_rows: nom = str(r["Sonde"]) temp = float(r["Temperature"]) - seuil = float(seuils.get(nom, 6.0)) + + if nom not in seuils: + continue # sonde non gérée dans Chambres_froides → ignorée + + seuil = float(seuils[nom]) now_ = now_paris() if temp > seuil: if depassement_depuis_30min(site, nom, seuil): + conn = None try: conn = get_db() if open_alert(conn, f"Alertes_{site}", nom, now_): - notifier_sur_depassement(site, nom, temp, seuil) # MAIL + SMS client + notifier_sur_depassement(site, nom, temp, seuil) finally: - conn.close() + if conn: + conn.close() else: + conn = None try: conn = get_db() if close_alert(conn, f"Alertes_{site}", nom): - notifier_acquittement(site, nom, temp, seuil) # MAIL acquittement + notifier_acquittement(site, nom, temp, seuil) finally: - conn.close() + if conn: + 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) diff --git a/app/domo91.py b/app/domo91.py index 5e02d1e..5341b8a 100644 --- a/app/domo91.py +++ b/app/domo91.py @@ -14,6 +14,7 @@ pd.set_option("future.no_silent_downcasting", True) import streamlit as st from dotenv import find_dotenv, load_dotenv from fpdf import FPDF +from streamlit_autorefresh import st_autorefresh # ========================================================= # Config de page @@ -35,6 +36,7 @@ db_config = { "password": os.getenv("DB_PASS"), "database": os.getenv("DB_NAME"), "autocommit": False, + "consume_results": True, } # Roissy n'existe pas actuellement => on garde Saclay / Meudon @@ -46,10 +48,9 @@ def get_connection(): return mysql.connector.connect(**db_config) -def assert_site_ok(site: str): - if site not in SITES_AUTORISES: - raise ValueError(f"Site invalide: {site}") - +def assert_site_ok(site_name: str): + if site_name not in SITES_AUTORISES: + raise ValueError(f"Site invalide: {site_name}") # ========================================================= # Session state @@ -79,9 +80,8 @@ def verifier_password(input_password: str, hash_en_base: str) -> bool: # ========================================================= # Gyro: lecture + badge # ========================================================= -def fetch_gyro(site: str): - """Retourne (etat, ts) depuis la vue v_gyro_last pour le site donné.""" - assert_site_ok(site) +def fetch_gyro(site_name: str): + assert_site_ok(site_name) q = """ SELECT Etat, `Date` FROM Sondes.v_gyro_last @@ -90,7 +90,7 @@ def fetch_gyro(site: str): LIMIT 1 """ with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: - cur.execute(q, (site,)) + cur.execute(q, (site_name,)) # <-- FIX row = cur.fetchone() if not row: return None, None @@ -99,9 +99,9 @@ def fetch_gyro(site: str): return etat, ts -def render_gyro_badge(site: str, stale_after_min: int = 10): +def render_gyro_badge(site_name: str, stale_after_min: int = 10): """Affiche un voyant Gyro (vert/rouge/orange) + fraîcheur des données.""" - etat, ts = fetch_gyro(site) + etat, ts = fetch_gyro(site_name) if etat in ("ON", "1"): color, label = "#ef4444", "GYRO ON" @@ -303,8 +303,8 @@ else: # ========================================================= # PDF # ========================================================= -def generer_pdf(site: str, date_str: str, periode: str): - assert_site_ok(site) +def generer_pdf(site_name: str, date_str: str, periode: str): + assert_site_ok(site_name) st.info(f"Génération du rapport PDF pour {site} à la date {date_str} ({periode})") plages = { @@ -467,9 +467,9 @@ def load_alertes(site: str, jour: date): return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols) -def load_anomalies_auto(site: str, jour: date): - assert_site_ok(site) - table_mesures = site +def load_anomalies_auto(site_name: str, jour: date): + assert_site_ok(site_name) + table_mesures = site_name gap_threshold_min = 20 jump_deg = 10 min_phys, max_phys = -60, 120 @@ -530,7 +530,7 @@ def load_anomalies_auto(site: str, jour: date): ORDER BY Sonde; """ - params = (jour, site, jour, site, jour, site, jour) + params = (jour, site_name, jour, site_name, jour, site_name, jour) with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: cur.execute(q, params) rows = cur.fetchall() @@ -783,6 +783,7 @@ if st.session_state.get("authenticated"): date_selectionnee = st.session_state.get("selected_date", date.today()) # ------------------ Accueil ------------------ + rows_mesures = [] if onglet_selectionne == "Accueil": try: # Site imposé ou sélection admin @@ -804,7 +805,7 @@ if st.session_state.get("authenticated"): # Voyant Gyro st.subheader(f"🚨 Statut Gyro — {site_actuel}") try: - st.autorefresh(interval=30000, key="gyro_autorefresh") + st_autorefresh(interval=30_000, key="gyro_autorefresh") except Exception: pass render_gyro_badge(site_actuel) @@ -813,24 +814,61 @@ if st.session_state.get("authenticated"): date_selectionnee = st.date_input("📅 Date du relevé", value=date_selectionnee) st.session_state["selected_date"] = date_selectionnee - rows = [] df_sonde = pd.DataFrame() seuil_temp = 10.0 sonde_choisie = None - with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor: cursor.execute( - f"SELECT * FROM `{site_actuel}` WHERE DATE(Date) = %s ORDER BY Sonde, Date DESC", - (date_selectionnee.strftime("%Y-%m-%d"),), + """ + SELECT Sonde, Temp_Max + FROM Sondes.Chambres_froides + WHERE Lieu = %s + AND UPPER(Etat) = 'ON' + ORDER BY Sonde + """, + (site_actuel,), ) - rows = cursor.fetchall() + rows_mesures = [] # important pour éviter NameError - if rows: - df = pd.DataFrame(rows) + with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor: + cursor.execute( + """ + SELECT Sonde, Temp_Max + FROM Sondes.Chambres_froides + WHERE Lieu = %s + AND UPPER(Etat) = 'ON' + ORDER BY Sonde + """, + (site_actuel,), + ) + cfg_on = cursor.fetchall() + + sondes_on = [r["Sonde"] for r in cfg_on] + seuils_on = {r["Sonde"]: float(r["Temp_Max"]) for r in cfg_on} + + # IMPORTANT: test avant la requête mesures + if not sondes_on: + st.warning("Aucune sonde active (Etat=ON) dans Chambres_froides pour ce site.") + st.stop() + + with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor: + placeholders = ", ".join(["%s"] * len(sondes_on)) + q = f""" + SELECT Sonde, Date, Temperature + FROM `{site_actuel}` + WHERE DATE(Date) = %s + AND Sonde IN ({placeholders}) + ORDER BY Sonde, Date DESC + """ + params = [date_selectionnee.strftime("%Y-%m-%d")] + sondes_on + cursor.execute(q, params) + rows_mesures = cursor.fetchall() + + if rows_mesures: + df = pd.DataFrame(rows_mesures) df["Date"] = pd.to_datetime(df["Date"]) - sondes = sorted(df["Sonde"].unique()) - sonde_choisie = st.selectbox("🧪 Choisissez une sonde :", sondes) + sonde_choisie = st.selectbox("🧪 Choisissez une sonde :", sondes_on) df_sonde = df[df["Sonde"] == sonde_choisie].copy() df_sonde["Heure"] = df_sonde["Date"].dt.hour @@ -855,42 +893,40 @@ if st.session_state.get("authenticated"): elif tranche == "Nuit (18h-6h)": df_sonde = df_sonde[(df_sonde["Heure"] >= 18) | (df_sonde["Heure"] < 6)] - # Seuil - cursor.execute( - "SELECT Temp_Max FROM Sondes.Chambres_froides WHERE Lieu = %s AND Sonde = %s", - (site_actuel, sonde_choisie), - ) - seuil = cursor.fetchone() - if seuil and seuil.get("Temp_Max") is not None: - seuil_temp = float(seuil["Temp_Max"]) + seuil_temp = seuils_on.get(sonde_choisie, 10.0) - if rows and not df_sonde.empty: - st.subheader("📊 Tableau des relevés") + if not df_sonde.empty: + st.subheader("📊 Tableau des relevés") - def surlignage_temp(val): - try: - if float(val) > seuil_temp: - return "color: red; font-weight: bold" - except Exception: - pass - return "" - styled_df = df_sonde.style.map(surlignage_temp, subset=["Temperature"]) - st.dataframe(styled_df, use_container_width=True) + def surlignage_temp(val): + try: + if float(val) > seuil_temp: + return "color: red; font-weight: bold" + except Exception: + pass + return "" - st.subheader("📈 Évolution de la température") - fig, ax = plt.subplots(figsize=(10, 4)) - ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o") - ax.axhline(seuil_temp, linestyle="--", label=f"Seuil {seuil_temp}°C") - ax.set_xlabel("Heure") - ax.set_ylabel("Température (°C)") - ax.set_title(f"{sonde_choisie} - {date_selectionnee.strftime('%d/%m/%Y')}") - ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) - ax.legend() - st.pyplot(fig) - elif not rows: - st.info("Aucun relevé pour cette date.") + styled_df = df_sonde.style.map(surlignage_temp, subset=["Temperature"]) + st.dataframe(styled_df, use_container_width=True) + + st.subheader("📈 Évolution de la température") + fig, ax = plt.subplots(figsize=(10, 4)) + ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o") + ax.axhline(seuil_temp, linestyle="--", label=f"Seuil {seuil_temp}°C") + ax.set_xlabel("Heure") + ax.set_ylabel("Température (°C)") + ax.set_title(f"{sonde_choisie} - {date_selectionnee.strftime('%d/%m/%Y')}") + ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) + ax.legend() + st.pyplot(fig) + else: + st.info("Aucun relevé pour cette date.") + + if not sondes_on: + st.warning("Aucune sonde active (Etat=ON) dans Chambres_froides pour ce site.") + st.stop() except Exception as e: st.error(f"Erreur : {e}") st.text(traceback.format_exc()) diff --git a/app/gyro_control.py b/app/gyro_control.py index 4499f65..d120094 100644 --- a/app/gyro_control.py +++ b/app/gyro_control.py @@ -1,24 +1,17 @@ # gyro_control.py -import os, time, enum, logging, threading -import mysql.connector # pip install mysql-connector-python -import paho.mqtt.client as mqtt # pip install paho-mqtt +import os, time, logging, threading +import mysql.connector +import paho.mqtt.client as mqtt log = logging.getLogger("gyro") +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") -# Paramètres par défaut (surclassables via env ou arguments) DEF_CHECK_SEC = int(os.getenv("GYRO_CHECK_SEC", "20")) -DEF_PULSE_SEC = int(os.getenv("GYRO_PULSE_SEC", "60")) -DEF_COOLDOWN_SEC = int(os.getenv("GYRO_COOLDOWN_SEC", "600")) DEF_NORMAL_CONFIRM = int(os.getenv("GYRO_NORMAL_CONFIRM", "2")) -class GyroState(enum.Enum): - IDLE = 0 - PULSE_ON = 1 - COOLDOWN = 2 - class MqttGyroDriver: - def __init__(self, host, port, user, password, topic_command): - self.topic_command = topic_command + def __init__(self, host, port, user, password, topic_cmd): + self.topic_cmd = topic_cmd self.client = mqtt.Client() if user: self.client.username_pw_set(user, password or "") @@ -27,132 +20,105 @@ class MqttGyroDriver: def set(self, on: bool): payload = "ON" if on else "OFF" - res = self.client.publish(self.topic_command, payload=payload, qos=1, retain=False) + res = self.client.publish(self.topic_cmd, payload=payload, qos=1, retain=False) res.wait_for_publish(timeout=5) - log.info("MQTT → %s : %s", self.topic_command, payload) + log.info("MQTT → %s : %s", self.topic_cmd, payload) def close(self): try: - self.client.loop_stop(); self.client.disconnect() + self.client.loop_stop() + self.client.disconnect() except Exception: pass class GyroController: """ - Boucle indépendante et légère : lit l'état d'alerte en SQL et pulse le gyro via MQTT. + Gyro ON en continu tant qu'il existe au moins une alerte Etat='En cours'. + Gyro OFF après 'normal_confirm' lectures consécutives sans alerte. """ - def __init__( - self, - *, - site_name: str, - db_cfg: dict, - alertes_table: str, - mqtt_driver: MqttGyroDriver, - check_sec: int = DEF_CHECK_SEC, - pulse_sec: int = DEF_PULSE_SEC, - cooldown_sec: int = DEF_COOLDOWN_SEC, - normal_confirm: int = DEF_NORMAL_CONFIRM, - ): + def __init__(self, *, site_name: str, db_cfg: dict, alertes_table: str, + mqtt_driver: MqttGyroDriver, check_sec: int = DEF_CHECK_SEC, + normal_confirm: int = DEF_NORMAL_CONFIRM): self.site = site_name self.db_cfg = db_cfg self.alertes_table = alertes_table self.mqtt = mqtt_driver 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._current_gyro_on = None self._thread = None + self._current_on = None + self._normal_count = 0 - # --- helpers --- def _set_gyro(self, on: bool): - if self._current_gyro_on is not on: + if self._current_on is not on: self.mqtt.set(on) - self._current_gyro_on = on + self._current_on = on def _has_active_alert(self, cur) -> bool: cur.execute(f"SELECT COUNT(*) FROM `{self.alertes_table}` WHERE Etat='En cours'") return cur.fetchone()[0] > 0 - # --- lifecycle --- 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("[%s] GyroController démarré (check=%ss, pulse=%ss, cooldown=%ss, confirm=%d)", - self.site, self.check_sec, self.pulse_sec, self.cooldown_sec, self.normal_confirm) + log.info("[%s] GyroController démarré (check=%ss, confirm=%d)", + self.site, self.check_sec, self.normal_confirm) def stop(self): self._stop.set() - # --- main loop --- - def _run(self): - # Ouverture connexion MySQL persistante + def _connect_mysql(self): while not self._stop.is_set(): try: cnx = mysql.connector.connect(autocommit=True, **self.db_cfg) cur = cnx.cursor() - break + return cnx, cur except Exception as e: log.error("[%s] Connexion MySQL KO (%s). Retry 5s…", self.site, e) time.sleep(5) + return None, None + + def _run(self): + cnx, cur = self._connect_mysql() + if not cnx: + return + try: + # au démarrage, on force OFF par sécurité (optionnel) + try: + self._set_gyro(False) + except Exception: + pass + while not self._stop.is_set(): - now = time.time() try: active = self._has_active_alert(cur) except Exception as e: - log.error("[%s] Lecture alertes KO: %s", self.site, e) - active = False # prudence + log.error("[%s] Lecture alertes KO: %s -> reconnexion MySQL", self.site, e) + try: + cur.close(); cnx.close() + except Exception: + pass + cnx, cur = self._connect_mysql() + if not cnx: + break + active = False - if self.state == GyroState.IDLE: - if active: - self._set_gyro(True) - self._t_pulse_end = now + self.pulse_sec - self.state = GyroState.PULSE_ON - self._normal_count = 0 - log.info("[%s] Gyro ON (pulse %ss)", self.site, self.pulse_sec) - - 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("[%s] Gyro OFF (retour à la normale)", self.site) - else: - self._normal_count = 0 - if now >= self._t_pulse_end: - self._set_gyro(False) - self._t_cooldown_end = now + self.cooldown_sec - self.state = GyroState.COOLDOWN - log.info("[%s] Gyro OFF → cooldown %ss", self.site, 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("[%s] Retour IDLE", self.site) - 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("[%s] Gyro ON (re-pulse)", self.site) + if active: + self._normal_count = 0 + self._set_gyro(True) + else: + self._normal_count += 1 + if self._normal_count >= self.normal_confirm: + self._set_gyro(False) time.sleep(self.check_sec) + finally: try: self._set_gyro(False) @@ -162,4 +128,41 @@ class GyroController: cur.close(); cnx.close() except Exception: pass - log.info("[%s] GyroController stoppé", self.site) \ No newline at end of file + log.info("[%s] GyroController stoppé", self.site) + +if __name__ == "__main__": + # ---- CONFIG À ADAPTER ---- + SITE = "Meudon" + ALERTES_TABLE = "Alertes_Meudon" # adaptez au nom réel + + DB_CFG = dict( + host=(os.getenv("DB_HOST") or "162.19.78.131").strip(), + user=(os.getenv("DB_USER") or "sondes").strip(), + password=os.getenv("DB_PASSWORD") or "TX.)-U1!zq5Axdk4", + database=(os.getenv("DB_NAME") or "Sondes").strip(), + port=int(os.getenv("DB_PORT") or 3306), + ) + + MQTT_HOST = (os.getenv("MQTT_HOST") or "162.19.78.131").strip() + MQTT_PORT = int(os.getenv("MQTT_PORT") or 1883) + MQTT_USER = os.getenv("MQTT_USER") or "sondes" + MQTT_PASS = os.getenv("MQTT_PASSWORD") or "3J@bjYP0" + + TOPIC_CMD = "Meudon/gyrophare/cmd" + + print("MQTT_HOST =", repr(MQTT_HOST)) + print("MQTT_PORT =", repr(MQTT_PORT)) + + drv = MqttGyroDriver(MQTT_HOST, MQTT_PORT, MQTT_USER, MQTT_PASS, TOPIC_CMD) + ctl = GyroController(site_name=SITE, db_cfg=DB_CFG, alertes_table=ALERTES_TABLE, + mqtt_driver=drv, check_sec=DEF_CHECK_SEC, normal_confirm=DEF_NORMAL_CONFIRM) + ctl.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + ctl.stop() + drv.close() diff --git a/app/mqtt_watchdog.py b/app/mqtt_watchdog.py index ad23b23..f986625 100644 --- a/app/mqtt_watchdog.py +++ b/app/mqtt_watchdog.py @@ -16,10 +16,10 @@ import paho.mqtt.client as mqtt from dotenv import load_dotenv; load_dotenv() # ---------- Configuration par défaut ---------- -DEFAULT_BROKER_HOST = os.getenv("MQTT_HOST", "54.36.188.119") +DEFAULT_BROKER_HOST = os.getenv("MQTT_HOST") DEFAULT_BROKER_PORT = int(os.getenv("MQTT_PORT", "1883")) -DEFAULT_MQTT_USER = os.getenv("MQTT_USER", "Bwps") -DEFAULT_MQTT_PASS = os.getenv("MQTT_PASS", "scJ5ACj2keRfI^") +DEFAULT_MQTT_USER = os.getenv("MQTT_USER") +DEFAULT_MQTT_PASS = os.getenv("MQTT_PASS") # Email (OVH SMTP par ex.) SMTP_HOST = os.getenv("SMTP_HOST", "ssl0.ovh.net") diff --git a/myenv/lib/python3.11/site-packages/pip/_vendor/certifi/core.py b/myenv/lib/python3.11/site-packages/pip/_vendor/certifi/core.py index 5c67600..71765b3 100644 --- a/myenv/lib/python3.11/site-packages/pip/_vendor/certifi/core.py +++ b/myenv/lib/python3.11/site-packages/pip/_vendor/certifi/core.py @@ -6,8 +6,6 @@ This module returns the installation location of cacert.pem or its contents. """ import sys -DEBIAN_CA_CERTS_PATH = '/etc/ssl/certs/ca-certificates.crt' - if sys.version_info >= (3, 11): from importlib.resources import as_file, files