Remise en état des userforms inventaire

This commit is contained in:
2025-10-03 13:52:13 +02:00
parent 807f2d318b
commit 4bd43a3611
4 changed files with 257 additions and 23 deletions

2
.env
View File

@@ -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

View File

@@ -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)
r.wait_for_publish(timeout=3) try:
if r.rc != 0: r.wait_for_publish(timeout=3)
log.warning("MQTT publish rc=%s (topic=%s)", r.rc, self.topic) except Exception:
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)

View File

@@ -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)
r.wait_for_publish(timeout=3) try:
if r.rc != 0: r.wait_for_publish(timeout=3)
log.warning("MQTT publish rc=%s (topic=%s)", r.rc, self.topic) except Exception:
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)

View File

@@ -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