Remise en état des userforms inventaire
This commit is contained in:
@@ -27,7 +27,7 @@ try:
|
||||
_ovh_available = True
|
||||
except Exception:
|
||||
ovh = None # type: ignore
|
||||
class OVHAPIError(Exception): pass
|
||||
class OVHAPIError(Exception): ...
|
||||
_ovh_available = False
|
||||
|
||||
# MQTT
|
||||
@@ -44,7 +44,7 @@ if not log.handlers:
|
||||
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:
|
||||
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 & SMS client).
|
||||
@@ -59,7 +59,7 @@ def open_alert(conn, table_alertes: str, sonde: str, dt: datetime) -> bool:
|
||||
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'))
|
||||
(sonde, dt_.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
@@ -92,6 +92,45 @@ def get_db():
|
||||
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):
|
||||
table = site
|
||||
sql = f"""
|
||||
@@ -100,8 +139,10 @@ def lire_sondes_depuis_db(site: str):
|
||||
JOIN (
|
||||
SELECT Sonde, MAX(Date) AS MaxDate
|
||||
FROM `{table}`
|
||||
WHERE Temperature IS NOT NULL
|
||||
GROUP BY Sonde
|
||||
) t2 ON t1.Sonde=t2.Sonde AND t1.Date=t2.MaxDate
|
||||
WHERE t1.Temperature IS NOT NULL
|
||||
"""
|
||||
cnx = get_db()
|
||||
try:
|
||||
@@ -109,7 +150,7 @@ def lire_sondes_depuis_db(site: str):
|
||||
cur.execute(sql)
|
||||
rows = cur.fetchall()
|
||||
for r in rows:
|
||||
r["Temperature"] = float(r["Temperature"])
|
||||
r["Temperature"] = float(r["Temperature"]) # garanti NOT NULL
|
||||
return rows
|
||||
except MySQLError as err:
|
||||
log.exception("Erreur DB (lire_sondes_depuis_db): %s", err)
|
||||
@@ -117,6 +158,7 @@ def lire_sondes_depuis_db(site: str):
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
|
||||
def lire_cfg_chambres(site: str):
|
||||
"""
|
||||
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]]:
|
||||
out: list[tuple[str, str]] = []
|
||||
for tok in re.split(r"[;,]", raw or ""):
|
||||
tok = tok.strip().strip('"').strip("'")
|
||||
tok = tok.strip()
|
||||
if not tok:
|
||||
continue
|
||||
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:
|
||||
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 =========
|
||||
@@ -494,6 +537,7 @@ class MQTTPublisher:
|
||||
pwd = os.getenv("MQTT_PASS")
|
||||
tls = (os.getenv("MQTT_TLS", "0") == "1")
|
||||
|
||||
# --- Création du client MQTT : compatible paho 1.x et 2.x ---
|
||||
cbver = getattr(mqtt, "CallbackAPIVersion", None)
|
||||
if cbver is not None:
|
||||
api_v = (
|
||||
@@ -508,6 +552,7 @@ class MQTTPublisher:
|
||||
self.client = mqtt.Client()
|
||||
else:
|
||||
self.client = mqtt.Client()
|
||||
# ------------------------------------------------------------
|
||||
|
||||
if user and pwd:
|
||||
self.client.username_pw_set(user, pwd)
|
||||
@@ -515,13 +560,70 @@ class MQTTPublisher:
|
||||
self.client.tls_set()
|
||||
|
||||
try:
|
||||
# Attacher le callback avant de s'abonner
|
||||
self.client.on_message = self._on_message
|
||||
|
||||
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()
|
||||
log.info("MQTT connecté (%s:%s), topic=%s", host, port, self.topic)
|
||||
except Exception as e:
|
||||
log.exception("MQTT connexion impossible: %s", e)
|
||||
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):
|
||||
if not self.enabled:
|
||||
return
|
||||
@@ -530,11 +632,27 @@ class MQTTPublisher:
|
||||
payload = "ON" if on else "OFF"
|
||||
try:
|
||||
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
||||
r.wait_for_publish(timeout=3)
|
||||
if r.rc != 0:
|
||||
log.warning("MQTT publish rc=%s (topic=%s)", r.rc, self.topic)
|
||||
try:
|
||||
r.wait_for_publish(timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
if getattr(r, 'rc', 0) != 0:
|
||||
log.warning("MQTT publish rc=%s (topic=%s)", getattr(r, 'rc', None), self.topic)
|
||||
else:
|
||||
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
|
||||
except Exception as 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)
|
||||
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":
|
||||
notifier.send_sms(sms_text)
|
||||
|
||||
@@ -755,13 +874,13 @@ def run_monitor_cycle(site: str = SITE):
|
||||
nom = str(r["Sonde"])
|
||||
temp = float(r["Temperature"])
|
||||
seuil = float(seuils.get(nom, 6.0))
|
||||
now = now_paris()
|
||||
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):
|
||||
if open_alert(conn, f"Alertes_{site}", nom, now_):
|
||||
notifier_sur_depassement(site, nom, temp, seuil) # MAIL + SMS client
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -810,9 +929,9 @@ if __name__ == "__main__":
|
||||
elif args.test_mail:
|
||||
notifier.send_email(f"[TEST {SITE}] Mail", "OK")
|
||||
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:
|
||||
notifier_acquittement(SITE, "Chambre_N1", -15.2, -15.0)
|
||||
notifier_acquittement(SITE, "Congelateur", -15.2, -15.0)
|
||||
else:
|
||||
if args.once:
|
||||
run_monitor_cycle(SITE)
|
||||
|
||||
Reference in New Issue
Block a user