Remise en état des userforms inventaire
This commit is contained in:
2
.env
2
.env
@@ -47,7 +47,7 @@ OVH_APPLICATION_SECRET=5ca392a0a728e2395edd426bb1e11ad6
|
|||||||
OVH_CONSUMER_KEY=305f2e8611e58b83930de84ee65c99f9
|
OVH_CONSUMER_KEY=305f2e8611e58b83930de84ee65c99f9
|
||||||
OVH_SMS_SERVICE=sms-jm164396-1
|
OVH_SMS_SERVICE=sms-jm164396-1
|
||||||
OVH_SMS_SENDER=DOMO91FR
|
OVH_SMS_SENDER=DOMO91FR
|
||||||
SMS_RECEIVER=+33635164680
|
SMS_RECEIVER=+33759600180
|
||||||
ALERT_SMS_TO_SACLAY=Michel:+33635164680
|
ALERT_SMS_TO_SACLAY=Michel:+33635164680
|
||||||
ALERT_SMS_TO_MEUDON=Michel:+33635164680
|
ALERT_SMS_TO_MEUDON=Michel:+33635164680
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ try:
|
|||||||
_ovh_available = True
|
_ovh_available = True
|
||||||
except Exception:
|
except Exception:
|
||||||
ovh = None # type: ignore
|
ovh = None # type: ignore
|
||||||
class OVHAPIError(Exception): pass
|
class OVHAPIError(Exception): ...
|
||||||
_ovh_available = False
|
_ovh_available = False
|
||||||
|
|
||||||
# MQTT
|
# MQTT
|
||||||
@@ -44,7 +44,7 @@ if not log.handlers:
|
|||||||
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(message)s")
|
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(message)s")
|
||||||
|
|
||||||
# ========= DB utils =========
|
# ========= DB utils =========
|
||||||
def open_alert(conn, table_alertes: str, sonde: str, dt: datetime) -> bool:
|
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.
|
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 & SMS client).
|
Retourne True si une nouvelle alerte a été créée (→ notifier par mail & SMS client).
|
||||||
@@ -59,7 +59,7 @@ def open_alert(conn, table_alertes: str, sonde: str, dt: datetime) -> bool:
|
|||||||
return False # déjà ouverte
|
return False # déjà ouverte
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"INSERT INTO `{table_alertes}` (Sonde, Debut_defaut, Etat) VALUES (%s, %s, 'En cours')",
|
f"INSERT INTO `{table_alertes}` (Sonde, Debut_defaut, Etat) VALUES (%s, %s, 'En cours')",
|
||||||
(sonde, dt.strftime('%Y-%m-%d %H:%M:%S'))
|
(sonde, dt_.strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -92,6 +92,45 @@ def get_db():
|
|||||||
autocommit=True,
|
autocommit=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Journalisation Gyro en table dédiée `Gyro` ---
|
||||||
|
def insert_gyro_log(lieu: str, etat: str, topic: str, payload_raw: str,
|
||||||
|
qos: int | None, retained: int | None, when: datetime):
|
||||||
|
cnx = get_db()
|
||||||
|
try:
|
||||||
|
cur = cnx.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO `Gyro` (Lieu, Sonde, Etat, Date, Topic, Payload, QoS, Retained) "
|
||||||
|
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
|
||||||
|
(
|
||||||
|
lieu,
|
||||||
|
os.getenv("GYRO_SONDE_NAME", "Gyro"),
|
||||||
|
etat, # 'ON' ou 'OFF'
|
||||||
|
when.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
topic,
|
||||||
|
payload_raw,
|
||||||
|
qos,
|
||||||
|
retained
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cnx.commit()
|
||||||
|
log.info("Gyro inséré: %s %s (%s)", lieu, etat, topic)
|
||||||
|
except MySQLError as err:
|
||||||
|
log.exception("Erreur DB insert_gyro_log: %s", err)
|
||||||
|
finally:
|
||||||
|
cnx.close()
|
||||||
|
|
||||||
|
def should_insert_gyro(lieu: str, etat: str, sonde: str = "Gyro") -> bool:
|
||||||
|
sql = "SELECT Etat FROM `Gyro` WHERE Lieu=%s AND Sonde=%s ORDER BY Date DESC LIMIT 1"
|
||||||
|
cnx = get_db()
|
||||||
|
try:
|
||||||
|
cur = cnx.cursor()
|
||||||
|
cur.execute(sql, (lieu, sonde))
|
||||||
|
row = cur.fetchone()
|
||||||
|
return (row is None) or (row[0] != etat)
|
||||||
|
finally:
|
||||||
|
cnx.close()
|
||||||
|
|
||||||
|
# --- Lecture des dernières mesures de température (en ignorant lignes d'état) ---
|
||||||
def lire_sondes_depuis_db(site: str):
|
def lire_sondes_depuis_db(site: str):
|
||||||
table = site
|
table = site
|
||||||
sql = f"""
|
sql = f"""
|
||||||
@@ -100,8 +139,10 @@ def lire_sondes_depuis_db(site: str):
|
|||||||
JOIN (
|
JOIN (
|
||||||
SELECT Sonde, MAX(Date) AS MaxDate
|
SELECT Sonde, MAX(Date) AS MaxDate
|
||||||
FROM `{table}`
|
FROM `{table}`
|
||||||
|
WHERE Temperature IS NOT NULL
|
||||||
GROUP BY Sonde
|
GROUP BY Sonde
|
||||||
) t2 ON t1.Sonde=t2.Sonde AND t1.Date=t2.MaxDate
|
) t2 ON t1.Sonde=t2.Sonde AND t1.Date=t2.MaxDate
|
||||||
|
WHERE t1.Temperature IS NOT NULL
|
||||||
"""
|
"""
|
||||||
cnx = get_db()
|
cnx = get_db()
|
||||||
try:
|
try:
|
||||||
@@ -109,7 +150,7 @@ def lire_sondes_depuis_db(site: str):
|
|||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
for r in rows:
|
for r in rows:
|
||||||
r["Temperature"] = float(r["Temperature"])
|
r["Temperature"] = float(r["Temperature"]) # garanti NOT NULL
|
||||||
return rows
|
return rows
|
||||||
except MySQLError as err:
|
except MySQLError as err:
|
||||||
log.exception("Erreur DB (lire_sondes_depuis_db): %s", err)
|
log.exception("Erreur DB (lire_sondes_depuis_db): %s", err)
|
||||||
@@ -117,6 +158,7 @@ def lire_sondes_depuis_db(site: str):
|
|||||||
finally:
|
finally:
|
||||||
cnx.close()
|
cnx.close()
|
||||||
|
|
||||||
|
|
||||||
def lire_cfg_chambres(site: str):
|
def lire_cfg_chambres(site: str):
|
||||||
"""
|
"""
|
||||||
Retourne {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
Retourne {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
||||||
@@ -236,7 +278,7 @@ def _split_list(raw: str | None) -> list[str]:
|
|||||||
def _parse_labeled_phones(raw: str | None) -> list[tuple[str, str]]:
|
def _parse_labeled_phones(raw: str | None) -> list[tuple[str, str]]:
|
||||||
out: list[tuple[str, str]] = []
|
out: list[tuple[str, str]] = []
|
||||||
for tok in re.split(r"[;,]", raw or ""):
|
for tok in re.split(r"[;,]", raw or ""):
|
||||||
tok = tok.strip().strip('"').strip("'")
|
tok = tok.strip()
|
||||||
if not tok:
|
if not tok:
|
||||||
continue
|
continue
|
||||||
if ":" in tok:
|
if ":" in tok:
|
||||||
@@ -465,6 +507,7 @@ def build_ok_text(site: str, sonde: str, temp: float, seuil: float, when: dt.dat
|
|||||||
|
|
||||||
def build_client_alert_sms(site: str, sonde: str, temp: float, seuil: float, when: dt.datetime | None = None) -> str:
|
def build_client_alert_sms(site: str, sonde: str, temp: float, seuil: float, when: dt.datetime | None = None) -> str:
|
||||||
when = when or now_paris()
|
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')}"
|
return f"ALERTE CLIENT {sonde}: T={fmt_deg(temp)} > S={fmt_deg(seuil)} H:{when.strftime('%H:%M')}"
|
||||||
|
|
||||||
# ========= Gyrophare MQTT =========
|
# ========= Gyrophare MQTT =========
|
||||||
@@ -494,6 +537,7 @@ class MQTTPublisher:
|
|||||||
pwd = os.getenv("MQTT_PASS")
|
pwd = os.getenv("MQTT_PASS")
|
||||||
tls = (os.getenv("MQTT_TLS", "0") == "1")
|
tls = (os.getenv("MQTT_TLS", "0") == "1")
|
||||||
|
|
||||||
|
# --- Création du client MQTT : compatible paho 1.x et 2.x ---
|
||||||
cbver = getattr(mqtt, "CallbackAPIVersion", None)
|
cbver = getattr(mqtt, "CallbackAPIVersion", None)
|
||||||
if cbver is not None:
|
if cbver is not None:
|
||||||
api_v = (
|
api_v = (
|
||||||
@@ -508,6 +552,7 @@ class MQTTPublisher:
|
|||||||
self.client = mqtt.Client()
|
self.client = mqtt.Client()
|
||||||
else:
|
else:
|
||||||
self.client = mqtt.Client()
|
self.client = mqtt.Client()
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
if user and pwd:
|
if user and pwd:
|
||||||
self.client.username_pw_set(user, pwd)
|
self.client.username_pw_set(user, pwd)
|
||||||
@@ -515,13 +560,70 @@ class MQTTPublisher:
|
|||||||
self.client.tls_set()
|
self.client.tls_set()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Attacher le callback avant de s'abonner
|
||||||
|
self.client.on_message = self._on_message
|
||||||
|
|
||||||
self.client.connect(host, port, keepalive=30)
|
self.client.connect(host, port, keepalive=30)
|
||||||
|
|
||||||
|
# Abonnements (depuis env ou valeurs par défaut raisonnables)
|
||||||
|
subs_env = (
|
||||||
|
os.getenv(f"GYRO_MQTT_SUB_{site}") or
|
||||||
|
os.getenv(f"GYRO_MQTT_SUB_{site.upper()}") or
|
||||||
|
os.getenv("GYRO_MQTT_SUB") or
|
||||||
|
""
|
||||||
|
)
|
||||||
|
subs = [t.strip() for t in subs_env.split(",") if t.strip()]
|
||||||
|
if not subs:
|
||||||
|
subs = [
|
||||||
|
self.topic, # ex: Sondes/Saclay/Gyro/cmd
|
||||||
|
f"Sondes/{site}/Gyro/#",
|
||||||
|
f"{site}/Gyro/#",
|
||||||
|
"Gyro/#",
|
||||||
|
]
|
||||||
|
for t in subs:
|
||||||
|
try:
|
||||||
|
self.client.subscribe(t, qos=2)
|
||||||
|
log.info("MQTT subscribe: %s", t)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Subscribe échoué (%s): %s", t, e)
|
||||||
|
|
||||||
self.client.loop_start()
|
self.client.loop_start()
|
||||||
log.info("MQTT connecté (%s:%s), topic=%s", host, port, self.topic)
|
log.info("MQTT connecté (%s:%s), topic=%s", host, port, self.topic)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("MQTT connexion impossible: %s", e)
|
log.exception("MQTT connexion impossible: %s", e)
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
|
|
||||||
|
# --- Callback réception MQTT ---
|
||||||
|
def _on_message(self, client, userdata, msg):
|
||||||
|
lieu = self.site
|
||||||
|
topic = msg.topic
|
||||||
|
payload_raw = msg.payload.decode(errors="ignore").strip()
|
||||||
|
upper = payload_raw.upper()
|
||||||
|
|
||||||
|
# 1) Évènements gyrophare
|
||||||
|
if upper in ("ON", "OFF") or "gyro" in topic.lower() or "gyrophare" in topic.lower():
|
||||||
|
etat = upper if upper in ("ON", "OFF") else ("ON" if "on" in upper else "OFF")
|
||||||
|
try:
|
||||||
|
if should_insert_gyro(lieu, etat):
|
||||||
|
insert_gyro_log(
|
||||||
|
lieu=lieu,
|
||||||
|
etat=etat,
|
||||||
|
topic=topic,
|
||||||
|
payload_raw=payload_raw,
|
||||||
|
qos=getattr(msg, "qos", None),
|
||||||
|
retained=getattr(msg, "retain", None),
|
||||||
|
when=now_paris()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Insert Gyro échoué: %s", e)
|
||||||
|
return # ne pas poursuivre vers un parse température ici
|
||||||
|
|
||||||
|
# 2) Pas du gyro → ignorer ici (la collecte T° est gérée ailleurs)
|
||||||
|
try:
|
||||||
|
float(payload_raw.replace(",", "."))
|
||||||
|
except ValueError:
|
||||||
|
log.debug("Payload non géré (ni gyro ni nombre): %s %s", topic, payload_raw)
|
||||||
|
|
||||||
def set(self, on: bool):
|
def set(self, on: bool):
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return
|
return
|
||||||
@@ -530,11 +632,27 @@ class MQTTPublisher:
|
|||||||
payload = "ON" if on else "OFF"
|
payload = "ON" if on else "OFF"
|
||||||
try:
|
try:
|
||||||
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
||||||
|
try:
|
||||||
r.wait_for_publish(timeout=3)
|
r.wait_for_publish(timeout=3)
|
||||||
if r.rc != 0:
|
except Exception:
|
||||||
log.warning("MQTT publish rc=%s (topic=%s)", r.rc, self.topic)
|
pass
|
||||||
|
if getattr(r, 'rc', 0) != 0:
|
||||||
|
log.warning("MQTT publish rc=%s (topic=%s)", getattr(r, 'rc', None), self.topic)
|
||||||
else:
|
else:
|
||||||
log.info("Gyro %s -> %s (MQTT)", self.site, payload.upper())
|
log.info("Gyro %s -> %s (MQTT)", self.site, payload.upper())
|
||||||
|
# Enregistrer en base l'événement gyro
|
||||||
|
try:
|
||||||
|
insert_gyro_log(
|
||||||
|
lieu=self.site,
|
||||||
|
etat=payload,
|
||||||
|
topic=self.topic,
|
||||||
|
payload_raw=payload,
|
||||||
|
qos=2,
|
||||||
|
retained=1 if getattr(r, 'is_published', lambda: False)() else None,
|
||||||
|
when=now_paris()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Insert événement gyro en base a échoué: %s", e)
|
||||||
self.last_state = on
|
self.last_state = on
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("MQTT publish erreur: %s", e)
|
log.exception("MQTT publish erreur: %s", e)
|
||||||
@@ -728,6 +846,7 @@ def notifier_acquittement(site: str, sonde: str, temp: float, seuil: float):
|
|||||||
"""
|
"""
|
||||||
subject, sms_text, email_body = build_ok_text(site, sonde, temp, seuil)
|
subject, sms_text, email_body = build_ok_text(site, sonde, temp, seuil)
|
||||||
notifier.send_email(subject, email_body) # mail d'acquittement
|
notifier.send_email(subject, email_body) # mail d'acquittement
|
||||||
|
# Optionnel: SMS "OK" côté cycle si souhaité
|
||||||
if os.getenv("ALERT_OK_SMS", "0") == "1":
|
if os.getenv("ALERT_OK_SMS", "0") == "1":
|
||||||
notifier.send_sms(sms_text)
|
notifier.send_sms(sms_text)
|
||||||
|
|
||||||
@@ -755,13 +874,13 @@ def run_monitor_cycle(site: str = SITE):
|
|||||||
nom = str(r["Sonde"])
|
nom = str(r["Sonde"])
|
||||||
temp = float(r["Temperature"])
|
temp = float(r["Temperature"])
|
||||||
seuil = float(seuils.get(nom, 6.0))
|
seuil = float(seuils.get(nom, 6.0))
|
||||||
now = now_paris()
|
now_ = now_paris()
|
||||||
|
|
||||||
if temp > seuil:
|
if temp > seuil:
|
||||||
if depassement_depuis_30min(site, nom, seuil):
|
if depassement_depuis_30min(site, nom, seuil):
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if open_alert(conn, f"Alertes_{site}", nom, now):
|
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) # MAIL + SMS client
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -810,9 +929,9 @@ if __name__ == "__main__":
|
|||||||
elif args.test_mail:
|
elif args.test_mail:
|
||||||
notifier.send_email(f"[TEST {SITE}] Mail", "OK")
|
notifier.send_email(f"[TEST {SITE}] Mail", "OK")
|
||||||
elif args.test_alert:
|
elif args.test_alert:
|
||||||
notifier_sur_depassement(SITE, "Chambre_N1", -14.5, -15.0)
|
notifier_sur_depassement(SITE, "Congelateur", -14.5, -15.0)
|
||||||
elif args.test_ok:
|
elif args.test_ok:
|
||||||
notifier_acquittement(SITE, "Chambre_N1", -15.2, -15.0)
|
notifier_acquittement(SITE, "Congelateur", -15.2, -15.0)
|
||||||
else:
|
else:
|
||||||
if args.once:
|
if args.once:
|
||||||
run_monitor_cycle(SITE)
|
run_monitor_cycle(SITE)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ try:
|
|||||||
_ovh_available = True
|
_ovh_available = True
|
||||||
except Exception:
|
except Exception:
|
||||||
ovh = None # type: ignore
|
ovh = None # type: ignore
|
||||||
class OVHAPIError(Exception): pass
|
class OVHAPIError(Exception): ...
|
||||||
_ovh_available = False
|
_ovh_available = False
|
||||||
|
|
||||||
# MQTT
|
# MQTT
|
||||||
@@ -44,7 +44,7 @@ if not log.handlers:
|
|||||||
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(message)s")
|
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(message)s")
|
||||||
|
|
||||||
# ========= DB utils =========
|
# ========= DB utils =========
|
||||||
def open_alert(conn, table_alertes: str, sonde: str, dt: datetime) -> bool:
|
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.
|
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 & SMS client).
|
Retourne True si une nouvelle alerte a été créée (→ notifier par mail & SMS client).
|
||||||
@@ -59,7 +59,7 @@ def open_alert(conn, table_alertes: str, sonde: str, dt: datetime) -> bool:
|
|||||||
return False # déjà ouverte
|
return False # déjà ouverte
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"INSERT INTO `{table_alertes}` (Sonde, Debut_defaut, Etat) VALUES (%s, %s, 'En cours')",
|
f"INSERT INTO `{table_alertes}` (Sonde, Debut_defaut, Etat) VALUES (%s, %s, 'En cours')",
|
||||||
(sonde, dt.strftime('%Y-%m-%d %H:%M:%S'))
|
(sonde, dt_.strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -92,6 +92,45 @@ def get_db():
|
|||||||
autocommit=True,
|
autocommit=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Journalisation Gyro en table dédiée `Gyro` ---
|
||||||
|
def insert_gyro_log(lieu: str, etat: str, topic: str, payload_raw: str,
|
||||||
|
qos: int | None, retained: int | None, when: datetime):
|
||||||
|
cnx = get_db()
|
||||||
|
try:
|
||||||
|
cur = cnx.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO Sondes.Gyro (Lieu, Sonde, Etat, Date, Topic, Payload, QoS, Retained) "
|
||||||
|
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
|
||||||
|
(
|
||||||
|
lieu,
|
||||||
|
os.getenv("GYRO_SONDE_NAME", "Gyro"),
|
||||||
|
etat, # 'ON' ou 'OFF'
|
||||||
|
when.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
topic,
|
||||||
|
payload_raw,
|
||||||
|
qos,
|
||||||
|
retained
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cnx.commit()
|
||||||
|
log.info("Gyro inséré: %s %s (%s)", lieu, etat, topic)
|
||||||
|
except MySQLError as err:
|
||||||
|
log.exception("Erreur DB insert_gyro_log: %s", err)
|
||||||
|
finally:
|
||||||
|
cnx.close()
|
||||||
|
|
||||||
|
def should_insert_gyro(lieu: str, etat: str, sonde: str = "Gyro") -> bool:
|
||||||
|
sql = "SELECT Etat FROM Sondes.Gyro WHERE Lieu=%s AND Sonde=%s ORDER BY Date DESC LIMIT 1"
|
||||||
|
cnx = get_db()
|
||||||
|
try:
|
||||||
|
cur = cnx.cursor()
|
||||||
|
cur.execute(sql, (lieu, sonde))
|
||||||
|
row = cur.fetchone()
|
||||||
|
return (row is None) or (row[0] != etat)
|
||||||
|
finally:
|
||||||
|
cnx.close()
|
||||||
|
|
||||||
|
# --- Lecture des dernières mesures de température (en ignorant lignes d'état) ---
|
||||||
def lire_sondes_depuis_db(site: str):
|
def lire_sondes_depuis_db(site: str):
|
||||||
table = site
|
table = site
|
||||||
sql = f"""
|
sql = f"""
|
||||||
@@ -100,8 +139,10 @@ def lire_sondes_depuis_db(site: str):
|
|||||||
JOIN (
|
JOIN (
|
||||||
SELECT Sonde, MAX(Date) AS MaxDate
|
SELECT Sonde, MAX(Date) AS MaxDate
|
||||||
FROM `{table}`
|
FROM `{table}`
|
||||||
|
WHERE Temperature IS NOT NULL
|
||||||
GROUP BY Sonde
|
GROUP BY Sonde
|
||||||
) t2 ON t1.Sonde=t2.Sonde AND t1.Date=t2.MaxDate
|
) t2 ON t1.Sonde=t2.Sonde AND t1.Date=t2.MaxDate
|
||||||
|
WHERE t1.Temperature IS NOT NULL
|
||||||
"""
|
"""
|
||||||
cnx = get_db()
|
cnx = get_db()
|
||||||
try:
|
try:
|
||||||
@@ -109,7 +150,7 @@ def lire_sondes_depuis_db(site: str):
|
|||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
for r in rows:
|
for r in rows:
|
||||||
r["Temperature"] = float(r["Temperature"])
|
r["Temperature"] = float(r["Temperature"]) # garanti NOT NULL
|
||||||
return rows
|
return rows
|
||||||
except MySQLError as err:
|
except MySQLError as err:
|
||||||
log.exception("Erreur DB (lire_sondes_depuis_db): %s", err)
|
log.exception("Erreur DB (lire_sondes_depuis_db): %s", err)
|
||||||
@@ -117,6 +158,7 @@ def lire_sondes_depuis_db(site: str):
|
|||||||
finally:
|
finally:
|
||||||
cnx.close()
|
cnx.close()
|
||||||
|
|
||||||
|
|
||||||
def lire_cfg_chambres(site: str):
|
def lire_cfg_chambres(site: str):
|
||||||
"""
|
"""
|
||||||
Retourne {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
Retourne {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
||||||
@@ -518,13 +560,70 @@ class MQTTPublisher:
|
|||||||
self.client.tls_set()
|
self.client.tls_set()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Attacher le callback avant de s'abonner
|
||||||
|
self.client.on_message = self._on_message
|
||||||
|
|
||||||
self.client.connect(host, port, keepalive=30)
|
self.client.connect(host, port, keepalive=30)
|
||||||
|
|
||||||
|
# Abonnements (depuis env ou valeurs par défaut raisonnables)
|
||||||
|
subs_env = (
|
||||||
|
os.getenv(f"GYRO_MQTT_SUB_{site}") or
|
||||||
|
os.getenv(f"GYRO_MQTT_SUB_{site.upper()}") or
|
||||||
|
os.getenv("GYRO_MQTT_SUB") or
|
||||||
|
""
|
||||||
|
)
|
||||||
|
subs = [t.strip() for t in subs_env.split(",") if t.strip()]
|
||||||
|
if not subs:
|
||||||
|
subs = [
|
||||||
|
self.topic, # ex: Sondes/Saclay/Gyro/cmd
|
||||||
|
f"Sondes/{site}/Gyro/#",
|
||||||
|
f"{site}/Gyro/#",
|
||||||
|
"Gyro/#",
|
||||||
|
]
|
||||||
|
for t in subs:
|
||||||
|
try:
|
||||||
|
self.client.subscribe(t, qos=2)
|
||||||
|
log.info("MQTT subscribe: %s", t)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Subscribe échoué (%s): %s", t, e)
|
||||||
|
|
||||||
self.client.loop_start()
|
self.client.loop_start()
|
||||||
log.info("MQTT connecté (%s:%s), topic=%s", host, port, self.topic)
|
log.info("MQTT connecté (%s:%s), topic=%s", host, port, self.topic)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("MQTT connexion impossible: %s", e)
|
log.exception("MQTT connexion impossible: %s", e)
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
|
|
||||||
|
# --- Callback réception MQTT ---
|
||||||
|
def _on_message(self, client, userdata, msg):
|
||||||
|
lieu = self.site
|
||||||
|
topic = msg.topic
|
||||||
|
payload_raw = msg.payload.decode(errors="ignore").strip()
|
||||||
|
upper = payload_raw.upper()
|
||||||
|
|
||||||
|
# 1) Évènements gyrophare
|
||||||
|
if upper in ("ON", "OFF") or "gyro" in topic.lower() or "gyrophare" in topic.lower():
|
||||||
|
etat = upper if upper in ("ON", "OFF") else ("ON" if "on" in upper else "OFF")
|
||||||
|
try:
|
||||||
|
if should_insert_gyro(lieu, etat):
|
||||||
|
insert_gyro_log(
|
||||||
|
lieu=lieu,
|
||||||
|
etat=etat,
|
||||||
|
topic=topic,
|
||||||
|
payload_raw=payload_raw,
|
||||||
|
qos=getattr(msg, "qos", None),
|
||||||
|
retained=getattr(msg, "retain", None),
|
||||||
|
when=now_paris()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Insert Gyro échoué: %s", e)
|
||||||
|
return # ne pas poursuivre vers un parse température ici
|
||||||
|
|
||||||
|
# 2) Pas du gyro → ignorer ici (la collecte T° est gérée ailleurs)
|
||||||
|
try:
|
||||||
|
float(payload_raw.replace(",", "."))
|
||||||
|
except ValueError:
|
||||||
|
log.debug("Payload non géré (ni gyro ni nombre): %s %s", topic, payload_raw)
|
||||||
|
|
||||||
def set(self, on: bool):
|
def set(self, on: bool):
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return
|
return
|
||||||
@@ -533,11 +632,27 @@ class MQTTPublisher:
|
|||||||
payload = "ON" if on else "OFF"
|
payload = "ON" if on else "OFF"
|
||||||
try:
|
try:
|
||||||
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
||||||
|
try:
|
||||||
r.wait_for_publish(timeout=3)
|
r.wait_for_publish(timeout=3)
|
||||||
if r.rc != 0:
|
except Exception:
|
||||||
log.warning("MQTT publish rc=%s (topic=%s)", r.rc, self.topic)
|
pass
|
||||||
|
if getattr(r, 'rc', 0) != 0:
|
||||||
|
log.warning("MQTT publish rc=%s (topic=%s)", getattr(r, 'rc', None), self.topic)
|
||||||
else:
|
else:
|
||||||
log.info("Gyro %s -> %s (MQTT)", self.site, payload.upper())
|
log.info("Gyro %s -> %s (MQTT)", self.site, payload.upper())
|
||||||
|
# Enregistrer en base l'événement gyro
|
||||||
|
try:
|
||||||
|
insert_gyro_log(
|
||||||
|
lieu=self.site,
|
||||||
|
etat=payload,
|
||||||
|
topic=self.topic,
|
||||||
|
payload_raw=payload,
|
||||||
|
qos=2,
|
||||||
|
retained=1 if getattr(r, 'is_published', lambda: False)() else None,
|
||||||
|
when=now_paris()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Insert événement gyro en base a échoué: %s", e)
|
||||||
self.last_state = on
|
self.last_state = on
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("MQTT publish erreur: %s", e)
|
log.exception("MQTT publish erreur: %s", e)
|
||||||
@@ -759,13 +874,13 @@ def run_monitor_cycle(site: str = SITE):
|
|||||||
nom = str(r["Sonde"])
|
nom = str(r["Sonde"])
|
||||||
temp = float(r["Temperature"])
|
temp = float(r["Temperature"])
|
||||||
seuil = float(seuils.get(nom, 6.0))
|
seuil = float(seuils.get(nom, 6.0))
|
||||||
now = now_paris()
|
now_ = now_paris()
|
||||||
|
|
||||||
if temp > seuil:
|
if temp > seuil:
|
||||||
if depassement_depuis_30min(site, nom, seuil):
|
if depassement_depuis_30min(site, nom, seuil):
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if open_alert(conn, f"Alertes_{site}", nom, now):
|
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) # MAIL + SMS client
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -822,4 +937,3 @@ if __name__ == "__main__":
|
|||||||
run_monitor_cycle(SITE)
|
run_monitor_cycle(SITE)
|
||||||
else:
|
else:
|
||||||
run_monitor_loop(SITE, period_sec=args.period)
|
run_monitor_loop(SITE, period_sec=args.period)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import matplotlib.dates as mdates
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
pd.set_option('future.no_silent_downcasting', True)
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|||||||
Reference in New Issue
Block a user