Consolidation de Domo91 et cosmétique
This commit is contained in:
17
.env
17
.env
@@ -13,9 +13,11 @@ MQTT_PASS=3J@bjYP0
|
|||||||
MQTT_PORT_MEUDON=1883
|
MQTT_PORT_MEUDON=1883
|
||||||
|
|
||||||
# Boucle rapide du gyro
|
# Boucle rapide du gyro
|
||||||
GYRO_MODE=mqtt
|
GYRO_WINDOW_MIN=3
|
||||||
|
GYRO_NEEDED_POINTS=2
|
||||||
GYRO_CHECK_SEC=20
|
GYRO_CHECK_SEC=20
|
||||||
GYRO_NORMAL_CONFIRM=2
|
GYRO_NORMAL_CONFIRM=6
|
||||||
|
GYRO_MODE=mqtt
|
||||||
GYRO_MODE_CONTINUOUS=1
|
GYRO_MODE_CONTINUOUS=1
|
||||||
GYRO_HYSTERESIS=0.3
|
GYRO_HYSTERESIS=0.3
|
||||||
ALERT_OK_SMS_GYRO=0
|
ALERT_OK_SMS_GYRO=0
|
||||||
@@ -30,17 +32,6 @@ ALERT_LOOKBACK_MINUTES=120
|
|||||||
# Logs
|
# Logs
|
||||||
LOGLEVEL=INFO
|
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
|
# paramètres mail
|
||||||
SMTP_HOST=ssl0.ovh.net
|
SMTP_HOST=ssl0.ovh.net
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Principes :
|
|||||||
- DB_HOST / MQTT_HOST / SMTP_HOST : uniques (VPS unique)
|
- DB_HOST / MQTT_HOST / SMTP_HOST : uniques (VPS unique)
|
||||||
- Paramètres par site via env : MAIL_TO_{SITE}, ALERT_SMS_TO_{SITE}, etc.
|
- 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
|
- 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
|
import datetime as dt
|
||||||
@@ -274,7 +274,7 @@ def lire_cfg_chambres(site: str):
|
|||||||
"""
|
"""
|
||||||
dbname = os.getenv("DB_NAME", "Sondes")
|
dbname = os.getenv("DB_NAME", "Sondes")
|
||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT Sonde, Temp_Max, Etat, En_entretien
|
SELECT Sonde, Temp_Max, Etat
|
||||||
FROM `{dbname}`.`Chambres_froides`
|
FROM `{dbname}`.`Chambres_froides`
|
||||||
WHERE Lieu=%s
|
WHERE Lieu=%s
|
||||||
"""
|
"""
|
||||||
@@ -283,11 +283,10 @@ def lire_cfg_chambres(site: str):
|
|||||||
try:
|
try:
|
||||||
cur = cnx.cursor()
|
cur = cnx.cursor()
|
||||||
cur.execute(sql, (site,))
|
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)] = {
|
cfg[str(sonde)] = {
|
||||||
"temp_max": float(temp_max),
|
"temp_max": float(temp_max),
|
||||||
"active": str(etat).upper() == "ON",
|
"active": str(etat).upper() == "ON",
|
||||||
"entretien": bool(int(en_entretien or 0)),
|
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
except MySQLError as err:
|
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)
|
meta = cfg.get(sonde)
|
||||||
if not meta:
|
if not meta:
|
||||||
continue
|
continue
|
||||||
if (not meta["active"]) or meta["entretien"]:
|
if not meta["active"]:
|
||||||
continue
|
continue
|
||||||
temp = float(row["Temperature"])
|
temp = float(row["Temperature"])
|
||||||
seuil = float(meta["temp_max"])
|
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 True, (sonde, temp, seuil)
|
||||||
return False, None
|
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:
|
def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -820,9 +883,10 @@ class GyroPulseController:
|
|||||||
self._last_trigger = None
|
self._last_trigger = None
|
||||||
|
|
||||||
def _is_alarm_now(self) -> tuple[bool, tuple[str, float, float] | None]:
|
def _is_alarm_now(self) -> tuple[bool, tuple[str, float, float] | None]:
|
||||||
last_rows = lire_sondes_depuis_db(self.site)
|
# Déclenchement "2 minutes" (2 points au-dessus du seuil sur ~3 minutes)
|
||||||
cfg = lire_cfg_chambres(self.site)
|
window_min = int(os.getenv("GYRO_WINDOW_MIN", "3"))
|
||||||
return compute_site_alarm(last_rows, cfg, hysteresis=float(os.getenv("GYRO_HYSTERESIS", "0.0")))
|
needed = int(os.getenv("GYRO_NEEDED_POINTS", "2"))
|
||||||
|
return depassement_depuis_2min(self.site, window_min=window_min, needed_points=needed)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
while not self._stop.is_set():
|
while not self._stop.is_set():
|
||||||
@@ -944,7 +1008,7 @@ def run_monitor_cycle(site: str, notifier: Notifier):
|
|||||||
meta = cfg.get(nom)
|
meta = cfg.get(nom)
|
||||||
if not meta:
|
if not meta:
|
||||||
continue
|
continue
|
||||||
if (not meta["active"]) or meta["entretien"]:
|
if not meta["active"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
seuil = float(meta["temp_max"])
|
seuil = float(meta["temp_max"])
|
||||||
|
|||||||
@@ -165,24 +165,24 @@ def lire_sondes_depuis_db(site: str):
|
|||||||
|
|
||||||
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}}
|
||||||
depuis Chambres_froides pour le site.
|
depuis Chambres_froides pour le site.
|
||||||
"""
|
"""
|
||||||
sql = """
|
dbname = os.getenv("DB_NAME", "Sondes")
|
||||||
SELECT Sonde, Temp_Max, Etat, En_entretien
|
sql = f"""
|
||||||
FROM Sondes.Chambres_froides
|
SELECT Sonde, Temp_Max, Etat
|
||||||
|
FROM `{dbname}`.`Chambres_froides`
|
||||||
WHERE Lieu=%s
|
WHERE Lieu=%s
|
||||||
"""
|
"""
|
||||||
cnx = get_db()
|
cnx = get_db()
|
||||||
cfg: dict[str, dict] = {}
|
cfg: dict[str, dict] = {}
|
||||||
try:
|
try:
|
||||||
cur = cnx.cursor()
|
cur = cnx.cursor()
|
||||||
cur.execute(sql, (site, ))
|
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)] = {
|
cfg[str(sonde)] = {
|
||||||
"temp_max": float(temp_max),
|
"temp_max": float(temp_max),
|
||||||
"active": str(etat).upper() == "ON",
|
"active": str(etat).upper() == "ON",
|
||||||
"entretien": bool(int(en_entretien or 0)),
|
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
except MySQLError as err:
|
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:
|
for row in last_values:
|
||||||
sonde = str(row["Sonde"])
|
sonde = str(row["Sonde"])
|
||||||
meta = cfg.get(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
|
continue
|
||||||
temp = float(row["Temperature"])
|
temp = float(row["Temperature"])
|
||||||
if temp > float(meta["temp_max"]) + float(hysteresis):
|
seuil = float(meta["temp_max"])
|
||||||
return True, (sonde, temp, float(meta["temp_max"]))
|
if temp > seuil + float(hysteresis):
|
||||||
|
return True, (sonde, temp, seuil)
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
def lire_seuils_depuis_db(site: str):
|
def lire_seuils_depuis_db(site: str):
|
||||||
@@ -883,24 +884,32 @@ def run_monitor_cycle(site: str = SITE):
|
|||||||
for r in last_rows:
|
for r in last_rows:
|
||||||
nom = str(r["Sonde"])
|
nom = str(r["Sonde"])
|
||||||
temp = float(r["Temperature"])
|
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()
|
now_ = now_paris()
|
||||||
|
|
||||||
if temp > seuil:
|
if temp > seuil:
|
||||||
if depassement_depuis_30min(site, nom, seuil):
|
if depassement_depuis_30min(site, nom, seuil):
|
||||||
|
conn = None
|
||||||
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)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
if conn:
|
||||||
|
conn.close()
|
||||||
else:
|
else:
|
||||||
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
if close_alert(conn, f"Alertes_{site}", nom):
|
if close_alert(conn, f"Alertes_{site}", nom):
|
||||||
notifier_acquittement(site, nom, temp, seuil) # MAIL acquittement
|
notifier_acquittement(site, nom, temp, seuil)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def run_monitor_loop(site: str = SITE, period_sec: int = 300):
|
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)
|
log.info("%s démarré (site=%s, période=%ss) ✅", PROGRAM_NAME, site, period_sec)
|
||||||
|
|||||||
150
app/domo91.py
150
app/domo91.py
@@ -14,6 +14,7 @@ pd.set_option("future.no_silent_downcasting", True)
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
from dotenv import find_dotenv, load_dotenv
|
from dotenv import find_dotenv, load_dotenv
|
||||||
from fpdf import FPDF
|
from fpdf import FPDF
|
||||||
|
from streamlit_autorefresh import st_autorefresh
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# Config de page
|
# Config de page
|
||||||
@@ -35,6 +36,7 @@ db_config = {
|
|||||||
"password": os.getenv("DB_PASS"),
|
"password": os.getenv("DB_PASS"),
|
||||||
"database": os.getenv("DB_NAME"),
|
"database": os.getenv("DB_NAME"),
|
||||||
"autocommit": False,
|
"autocommit": False,
|
||||||
|
"consume_results": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Roissy n'existe pas actuellement => on garde Saclay / Meudon
|
# Roissy n'existe pas actuellement => on garde Saclay / Meudon
|
||||||
@@ -46,10 +48,9 @@ def get_connection():
|
|||||||
return mysql.connector.connect(**db_config)
|
return mysql.connector.connect(**db_config)
|
||||||
|
|
||||||
|
|
||||||
def assert_site_ok(site: str):
|
def assert_site_ok(site_name: str):
|
||||||
if site not in SITES_AUTORISES:
|
if site_name not in SITES_AUTORISES:
|
||||||
raise ValueError(f"Site invalide: {site}")
|
raise ValueError(f"Site invalide: {site_name}")
|
||||||
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# Session state
|
# Session state
|
||||||
@@ -79,9 +80,8 @@ def verifier_password(input_password: str, hash_en_base: str) -> bool:
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
# Gyro: lecture + badge
|
# Gyro: lecture + badge
|
||||||
# =========================================================
|
# =========================================================
|
||||||
def fetch_gyro(site: str):
|
def fetch_gyro(site_name: str):
|
||||||
"""Retourne (etat, ts) depuis la vue v_gyro_last pour le site donné."""
|
assert_site_ok(site_name)
|
||||||
assert_site_ok(site)
|
|
||||||
q = """
|
q = """
|
||||||
SELECT Etat, `Date`
|
SELECT Etat, `Date`
|
||||||
FROM Sondes.v_gyro_last
|
FROM Sondes.v_gyro_last
|
||||||
@@ -90,7 +90,7 @@ def fetch_gyro(site: str):
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
|
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()
|
row = cur.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
return None, None
|
return None, None
|
||||||
@@ -99,9 +99,9 @@ def fetch_gyro(site: str):
|
|||||||
return etat, ts
|
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."""
|
"""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"):
|
if etat in ("ON", "1"):
|
||||||
color, label = "#ef4444", "GYRO ON"
|
color, label = "#ef4444", "GYRO ON"
|
||||||
@@ -303,8 +303,8 @@ else:
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
# PDF
|
# PDF
|
||||||
# =========================================================
|
# =========================================================
|
||||||
def generer_pdf(site: str, date_str: str, periode: str):
|
def generer_pdf(site_name: str, date_str: str, periode: str):
|
||||||
assert_site_ok(site)
|
assert_site_ok(site_name)
|
||||||
st.info(f"Génération du rapport PDF pour {site} à la date {date_str} ({periode})")
|
st.info(f"Génération du rapport PDF pour {site} à la date {date_str} ({periode})")
|
||||||
|
|
||||||
plages = {
|
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)
|
return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols)
|
||||||
|
|
||||||
|
|
||||||
def load_anomalies_auto(site: str, jour: date):
|
def load_anomalies_auto(site_name: str, jour: date):
|
||||||
assert_site_ok(site)
|
assert_site_ok(site_name)
|
||||||
table_mesures = site
|
table_mesures = site_name
|
||||||
gap_threshold_min = 20
|
gap_threshold_min = 20
|
||||||
jump_deg = 10
|
jump_deg = 10
|
||||||
min_phys, max_phys = -60, 120
|
min_phys, max_phys = -60, 120
|
||||||
@@ -530,7 +530,7 @@ def load_anomalies_auto(site: str, jour: date):
|
|||||||
|
|
||||||
ORDER BY Sonde;
|
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:
|
with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
|
||||||
cur.execute(q, params)
|
cur.execute(q, params)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
@@ -783,6 +783,7 @@ if st.session_state.get("authenticated"):
|
|||||||
date_selectionnee = st.session_state.get("selected_date", date.today())
|
date_selectionnee = st.session_state.get("selected_date", date.today())
|
||||||
|
|
||||||
# ------------------ Accueil ------------------
|
# ------------------ Accueil ------------------
|
||||||
|
rows_mesures = []
|
||||||
if onglet_selectionne == "Accueil":
|
if onglet_selectionne == "Accueil":
|
||||||
try:
|
try:
|
||||||
# Site imposé ou sélection admin
|
# Site imposé ou sélection admin
|
||||||
@@ -804,7 +805,7 @@ if st.session_state.get("authenticated"):
|
|||||||
# Voyant Gyro
|
# Voyant Gyro
|
||||||
st.subheader(f"🚨 Statut Gyro — {site_actuel}")
|
st.subheader(f"🚨 Statut Gyro — {site_actuel}")
|
||||||
try:
|
try:
|
||||||
st.autorefresh(interval=30000, key="gyro_autorefresh")
|
st_autorefresh(interval=30_000, key="gyro_autorefresh")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
render_gyro_badge(site_actuel)
|
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)
|
date_selectionnee = st.date_input("📅 Date du relevé", value=date_selectionnee)
|
||||||
st.session_state["selected_date"] = date_selectionnee
|
st.session_state["selected_date"] = date_selectionnee
|
||||||
|
|
||||||
rows = []
|
|
||||||
df_sonde = pd.DataFrame()
|
df_sonde = pd.DataFrame()
|
||||||
seuil_temp = 10.0
|
seuil_temp = 10.0
|
||||||
sonde_choisie = None
|
sonde_choisie = None
|
||||||
|
|
||||||
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
|
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
|
||||||
cursor.execute(
|
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:
|
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
|
||||||
df = pd.DataFrame(rows)
|
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"])
|
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 = df[df["Sonde"] == sonde_choisie].copy()
|
||||||
df_sonde["Heure"] = df_sonde["Date"].dt.hour
|
df_sonde["Heure"] = df_sonde["Date"].dt.hour
|
||||||
|
|
||||||
@@ -855,42 +893,40 @@ if st.session_state.get("authenticated"):
|
|||||||
elif tranche == "Nuit (18h-6h)":
|
elif tranche == "Nuit (18h-6h)":
|
||||||
df_sonde = df_sonde[(df_sonde["Heure"] >= 18) | (df_sonde["Heure"] < 6)]
|
df_sonde = df_sonde[(df_sonde["Heure"] >= 18) | (df_sonde["Heure"] < 6)]
|
||||||
|
|
||||||
# Seuil
|
seuil_temp = seuils_on.get(sonde_choisie, 10.0)
|
||||||
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"])
|
|
||||||
|
|
||||||
if rows and not df_sonde.empty:
|
if not df_sonde.empty:
|
||||||
st.subheader("📊 Tableau des relevés")
|
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"])
|
def surlignage_temp(val):
|
||||||
st.dataframe(styled_df, use_container_width=True)
|
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:
|
except Exception as e:
|
||||||
st.error(f"Erreur : {e}")
|
st.error(f"Erreur : {e}")
|
||||||
st.text(traceback.format_exc())
|
st.text(traceback.format_exc())
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
# gyro_control.py
|
# gyro_control.py
|
||||||
import os, time, enum, logging, threading
|
import os, time, logging, threading
|
||||||
import mysql.connector # pip install mysql-connector-python
|
import mysql.connector
|
||||||
import paho.mqtt.client as mqtt # pip install paho-mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
log = logging.getLogger("gyro")
|
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_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"))
|
DEF_NORMAL_CONFIRM = int(os.getenv("GYRO_NORMAL_CONFIRM", "2"))
|
||||||
|
|
||||||
class GyroState(enum.Enum):
|
|
||||||
IDLE = 0
|
|
||||||
PULSE_ON = 1
|
|
||||||
COOLDOWN = 2
|
|
||||||
|
|
||||||
class MqttGyroDriver:
|
class MqttGyroDriver:
|
||||||
def __init__(self, host, port, user, password, topic_command):
|
def __init__(self, host, port, user, password, topic_cmd):
|
||||||
self.topic_command = topic_command
|
self.topic_cmd = topic_cmd
|
||||||
self.client = mqtt.Client()
|
self.client = mqtt.Client()
|
||||||
if user:
|
if user:
|
||||||
self.client.username_pw_set(user, password or "")
|
self.client.username_pw_set(user, password or "")
|
||||||
@@ -27,132 +20,105 @@ class MqttGyroDriver:
|
|||||||
|
|
||||||
def set(self, on: bool):
|
def set(self, on: bool):
|
||||||
payload = "ON" if on else "OFF"
|
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)
|
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):
|
def close(self):
|
||||||
try:
|
try:
|
||||||
self.client.loop_stop(); self.client.disconnect()
|
self.client.loop_stop()
|
||||||
|
self.client.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class GyroController:
|
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__(
|
def __init__(self, *, site_name: str, db_cfg: dict, alertes_table: str,
|
||||||
self,
|
mqtt_driver: MqttGyroDriver, check_sec: int = DEF_CHECK_SEC,
|
||||||
*,
|
normal_confirm: int = DEF_NORMAL_CONFIRM):
|
||||||
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,
|
|
||||||
):
|
|
||||||
self.site = site_name
|
self.site = site_name
|
||||||
self.db_cfg = db_cfg
|
self.db_cfg = db_cfg
|
||||||
self.alertes_table = alertes_table
|
self.alertes_table = alertes_table
|
||||||
self.mqtt = mqtt_driver
|
self.mqtt = mqtt_driver
|
||||||
self.check_sec = check_sec
|
self.check_sec = check_sec
|
||||||
self.pulse_sec = pulse_sec
|
|
||||||
self.cooldown_sec = cooldown_sec
|
|
||||||
self.normal_confirm = normal_confirm
|
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._stop = threading.Event()
|
||||||
self._current_gyro_on = None
|
|
||||||
self._thread = None
|
self._thread = None
|
||||||
|
self._current_on = None
|
||||||
|
self._normal_count = 0
|
||||||
|
|
||||||
# --- helpers ---
|
|
||||||
def _set_gyro(self, on: bool):
|
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.mqtt.set(on)
|
||||||
self._current_gyro_on = on
|
self._current_on = on
|
||||||
|
|
||||||
def _has_active_alert(self, cur) -> bool:
|
def _has_active_alert(self, cur) -> bool:
|
||||||
cur.execute(f"SELECT COUNT(*) FROM `{self.alertes_table}` WHERE Etat='En cours'")
|
cur.execute(f"SELECT COUNT(*) FROM `{self.alertes_table}` WHERE Etat='En cours'")
|
||||||
return cur.fetchone()[0] > 0
|
return cur.fetchone()[0] > 0
|
||||||
|
|
||||||
# --- lifecycle ---
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self._thread and self._thread.is_alive():
|
if self._thread and self._thread.is_alive():
|
||||||
return
|
return
|
||||||
self._stop.clear()
|
self._stop.clear()
|
||||||
self._thread = threading.Thread(target=self._run, daemon=True)
|
self._thread = threading.Thread(target=self._run, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
log.info("[%s] GyroController démarré (check=%ss, pulse=%ss, cooldown=%ss, confirm=%d)",
|
log.info("[%s] GyroController démarré (check=%ss, confirm=%d)",
|
||||||
self.site, self.check_sec, self.pulse_sec, self.cooldown_sec, self.normal_confirm)
|
self.site, self.check_sec, self.normal_confirm)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
|
|
||||||
# --- main loop ---
|
def _connect_mysql(self):
|
||||||
def _run(self):
|
|
||||||
# Ouverture connexion MySQL persistante
|
|
||||||
while not self._stop.is_set():
|
while not self._stop.is_set():
|
||||||
try:
|
try:
|
||||||
cnx = mysql.connector.connect(autocommit=True, **self.db_cfg)
|
cnx = mysql.connector.connect(autocommit=True, **self.db_cfg)
|
||||||
cur = cnx.cursor()
|
cur = cnx.cursor()
|
||||||
break
|
return cnx, cur
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("[%s] Connexion MySQL KO (%s). Retry 5s…", self.site, e)
|
log.error("[%s] Connexion MySQL KO (%s). Retry 5s…", self.site, e)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
cnx, cur = self._connect_mysql()
|
||||||
|
if not cnx:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
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():
|
while not self._stop.is_set():
|
||||||
now = time.time()
|
|
||||||
try:
|
try:
|
||||||
active = self._has_active_alert(cur)
|
active = self._has_active_alert(cur)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("[%s] Lecture alertes KO: %s", self.site, e)
|
log.error("[%s] Lecture alertes KO: %s -> reconnexion MySQL", self.site, e)
|
||||||
active = False # prudence
|
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:
|
||||||
if active:
|
self._normal_count = 0
|
||||||
self._set_gyro(True)
|
self._set_gyro(True)
|
||||||
self._t_pulse_end = now + self.pulse_sec
|
else:
|
||||||
self.state = GyroState.PULSE_ON
|
self._normal_count += 1
|
||||||
self._normal_count = 0
|
if self._normal_count >= self.normal_confirm:
|
||||||
log.info("[%s] Gyro ON (pulse %ss)", self.site, self.pulse_sec)
|
self._set_gyro(False)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
time.sleep(self.check_sec)
|
time.sleep(self.check_sec)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
self._set_gyro(False)
|
self._set_gyro(False)
|
||||||
@@ -162,4 +128,41 @@ class GyroController:
|
|||||||
cur.close(); cnx.close()
|
cur.close(); cnx.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
log.info("[%s] GyroController stoppé", self.site)
|
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()
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import paho.mqtt.client as mqtt
|
|||||||
from dotenv import load_dotenv; load_dotenv()
|
from dotenv import load_dotenv; load_dotenv()
|
||||||
|
|
||||||
# ---------- Configuration par défaut ----------
|
# ---------- 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_BROKER_PORT = int(os.getenv("MQTT_PORT", "1883"))
|
||||||
DEFAULT_MQTT_USER = os.getenv("MQTT_USER", "Bwps")
|
DEFAULT_MQTT_USER = os.getenv("MQTT_USER")
|
||||||
DEFAULT_MQTT_PASS = os.getenv("MQTT_PASS", "scJ5ACj2keRfI^")
|
DEFAULT_MQTT_PASS = os.getenv("MQTT_PASS")
|
||||||
|
|
||||||
# Email (OVH SMTP par ex.)
|
# Email (OVH SMTP par ex.)
|
||||||
SMTP_HOST = os.getenv("SMTP_HOST", "ssl0.ovh.net")
|
SMTP_HOST = os.getenv("SMTP_HOST", "ssl0.ovh.net")
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ This module returns the installation location of cacert.pem or its contents.
|
|||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
DEBIAN_CA_CERTS_PATH = '/etc/ssl/certs/ca-certificates.crt'
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
|
|
||||||
from importlib.resources import as_file, files
|
from importlib.resources import as_file, files
|
||||||
|
|||||||
Reference in New Issue
Block a user