Remise en état des fichiers Gestion_sondes
This commit is contained in:
236
Outils/Injection_tests.py
Normal file
236
Outils/Injection_tests.py
Normal file
@@ -0,0 +1,236 @@
|
||||
import os
|
||||
import datetime as dt
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
from dotenv import load_dotenv
|
||||
import mysql.connector as mc
|
||||
|
||||
# ----------------------
|
||||
# Config de la page
|
||||
# ----------------------
|
||||
st.set_page_config(page_title="Injection de données de test", page_icon="🧪", layout="centered")
|
||||
st.title("🧪 Injecteur de relevés de test (Sondes)")
|
||||
st.caption("Crée ~10 lignes au-dessus d'un seuil pour tester les alertes")
|
||||
|
||||
# ----------------------
|
||||
# Connexion MySQL depuis .env
|
||||
# ----------------------
|
||||
@st.cache_resource(show_spinner=False)
|
||||
def get_connection():
|
||||
load_dotenv()
|
||||
try:
|
||||
cnx = mc.connect(
|
||||
host=os.getenv("DB_HOST"),
|
||||
user=os.getenv("DB_USER"),
|
||||
password=os.getenv("DB_PASS"),
|
||||
database=os.getenv("DB_NAME"),
|
||||
autocommit=True,
|
||||
)
|
||||
return cnx
|
||||
except Exception as e:
|
||||
st.error(f"Échec de connexion MySQL : {e}")
|
||||
raise
|
||||
|
||||
# ----------------------
|
||||
# Helpers : liste des sondes actives et hors entretien
|
||||
# ----------------------
|
||||
@st.cache_data(ttl=60, show_spinner=False)
|
||||
def list_sondes(site: str) -> list:
|
||||
"""Retourne la liste des sondes actives (Etat=ON) et non en entretien pour le site.
|
||||
Essaie d'abord monitor_{site}, puis Chambres_froides, sinon fallback via la table de mesures.
|
||||
"""
|
||||
cnx = get_connection()
|
||||
cur = cnx.cursor()
|
||||
# 1) monitor_{site}
|
||||
try:
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT Sonde
|
||||
FROM `monitor_{site}`
|
||||
WHERE (Etat='ON' OR Etat=1)
|
||||
AND ( (Maintenance='OFF') OR (Maintenance=0) OR (Maintenance IS NULL) )
|
||||
ORDER BY Sonde
|
||||
"""
|
||||
)
|
||||
rows = [r[0] for r in cur.fetchall()]
|
||||
if rows:
|
||||
return rows
|
||||
except Exception:
|
||||
pass
|
||||
# 2) Chambres_froides
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT Sonde
|
||||
FROM `Chambres_froides`
|
||||
WHERE Lieu=%s AND (Etat='ON' OR Etat=1)
|
||||
AND ( (Maintenance='OFF') OR (Maintenance=0) OR (Maintenance IS NULL) )
|
||||
ORDER BY Sonde
|
||||
""",
|
||||
(site,)
|
||||
)
|
||||
rows = [r[0] for r in cur.fetchall()]
|
||||
if rows:
|
||||
return rows
|
||||
except Exception:
|
||||
pass
|
||||
# 3) Fallback : dernier état via table de mesures (distinct)
|
||||
try:
|
||||
cur.execute(
|
||||
f"SELECT DISTINCT Sonde FROM `{site}` ORDER BY Sonde LIMIT 200"
|
||||
)
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# ----------------------
|
||||
# Helper : récupérer Temp_Max pour une sonde donnée
|
||||
# ----------------------
|
||||
@st.cache_data(ttl=60, show_spinner=False)
|
||||
def get_temp_max(site: str, sonde: str):
|
||||
"""Retourne Temp_Max pour (site, sonde) en cherchant d'abord dans monitor_{site}, puis Chambres_froides.
|
||||
Renvoie None si non trouvé."""
|
||||
try:
|
||||
cnx = get_connection()
|
||||
cur = cnx.cursor()
|
||||
# 1) monitor_{site}
|
||||
try:
|
||||
cur.execute(
|
||||
f"SELECT Temp_Max FROM `monitor_{site}` WHERE Sonde=%s LIMIT 1",
|
||||
(sonde,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row[0] is not None:
|
||||
return float(row[0])
|
||||
except Exception:
|
||||
pass
|
||||
# 2) Chambres_froides
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT Temp_Max
|
||||
FROM `Chambres_froides`
|
||||
WHERE Lieu=%s AND Sonde=%s
|
||||
LIMIT 1
|
||||
""",
|
||||
(site, sonde)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row[0] is not None:
|
||||
return float(row[0])
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
# ----------------------
|
||||
# UI paramètres
|
||||
# ----------------------
|
||||
with st.sidebar:
|
||||
st.header("Paramètres")
|
||||
site = st.selectbox("Site (table)", ["Saclay", "Meudon"], index=0)
|
||||
|
||||
# Sélecteur de sonde depuis la liste active / hors entretien
|
||||
options_sondes = list_sondes(site)
|
||||
if not options_sondes:
|
||||
st.warning("Aucune sonde active trouvée (ou table monitor introuvable). Vous pouvez saisir un nom manuel.")
|
||||
sonde = st.text_input("Nom de la sonde", value="TEST_Chambre1")
|
||||
else:
|
||||
sonde = st.selectbox("Sonde (actives, hors entretien)", options_sondes)
|
||||
|
||||
st.subheader("Température")
|
||||
# Auto-remplissage du seuil depuis la base et verrouillage par défaut
|
||||
_temp_db = get_temp_max(site, sonde)
|
||||
if _temp_db is None:
|
||||
st.warning("Temp_Max introuvable en base ; valeur par défaut 6.0°C.")
|
||||
_temp_db = 6.0
|
||||
allow_edit = st.checkbox("Autoriser la modification du seuil", value=False)
|
||||
temp_max = st.number_input("Seuil (Temp_Max)", value=float(_temp_db), step=0.1, disabled=not allow_edit)
|
||||
|
||||
delta = st.number_input("Delta au-dessus du seuil", value=1.0, step=0.1)
|
||||
absolute_override = st.checkbox("Définir une température absolue à la place")
|
||||
absolute_temp = st.number_input(
|
||||
"Température absolue (si coché)", value=12.5, step=0.1, disabled=not absolute_override
|
||||
)
|
||||
|
||||
st.subheader("Série temporelle")
|
||||
rows = st.number_input("Nombre de points", min_value=1, max_value=200, value=10, step=1)
|
||||
step_min = st.number_input("Pas (minutes)", min_value=1, max_value=120, value=5, step=1)
|
||||
start_offset = st.number_input("Début : il y a (minutes)", min_value=0, max_value=1440, value=45, step=5)
|
||||
|
||||
st.markdown("---")
|
||||
st.caption("Nettoyage rapide")
|
||||
cleanup_scope = st.selectbox("Supprimer", ["Cette sonde", "Toutes les TEST_ des dernières 24h"])
|
||||
do_cleanup = st.button("🧹 Supprimer les données de test")
|
||||
col1, col2 = st.columns(2)
|
||||
# ----------------------
|
||||
# Actions
|
||||
# ----------------------
|
||||
if col1.button("🚀 Injecter les données"):
|
||||
try:
|
||||
cnx = get_connection()
|
||||
cur = cnx.cursor()
|
||||
|
||||
# Calcul des timestamps et de la valeur
|
||||
now = dt.datetime.now()
|
||||
t0 = now - dt.timedelta(minutes=int(start_offset))
|
||||
if absolute_override:
|
||||
value = float(absolute_temp)
|
||||
else:
|
||||
value = float(temp_max) + float(delta)
|
||||
|
||||
# Préparation batch INSERT
|
||||
sql = f"INSERT INTO `{site}` (Sonde, Temperature, Date) VALUES (%s,%s,%s)"
|
||||
batch = []
|
||||
for i in range(int(rows)):
|
||||
ts = t0 + dt.timedelta(minutes=i * int(step_min))
|
||||
batch.append((sonde, value, ts.strftime("%Y-%m-%d %H:%M:%S")))
|
||||
|
||||
cur.executemany(sql, batch)
|
||||
st.success(f"{len(batch)} lignes insérées dans `{site}` pour **{sonde}** à **{value}°C**.")
|
||||
|
||||
# Aperçu des données insérées
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT Id, Sonde, Temperature, Date
|
||||
FROM `{site}`
|
||||
WHERE Sonde = %s AND Date >= %s AND Date <= %s
|
||||
ORDER BY Date DESC
|
||||
LIMIT 50
|
||||
""",
|
||||
(
|
||||
sonde,
|
||||
(t0 - dt.timedelta(minutes=1)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
(t0 + dt.timedelta(minutes=int(rows)*int(step_min) + 1)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
),
|
||||
)
|
||||
rows_preview = cur.fetchall()
|
||||
if rows_preview:
|
||||
df = pd.DataFrame(rows_preview, columns=["Id", "Sonde", "Temperature", "Date"])
|
||||
st.dataframe(df, use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.info("Aucune ligne trouvée pour l'aperçu (vérifiez les filtres/horaires).")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de l'injection : {e}")
|
||||
|
||||
# Nettoyage
|
||||
def cleanup():
|
||||
cnx = get_connection()
|
||||
cur = cnx.cursor()
|
||||
if cleanup_scope == "Cette sonde":
|
||||
cur.execute(f"DELETE FROM `{site}` WHERE Sonde = %s", (sonde,))
|
||||
st.success(f"Données supprimées pour la sonde **{sonde}** dans `{site}`.")
|
||||
else:
|
||||
cur.execute(
|
||||
f"DELETE FROM `{site}` WHERE Sonde LIKE 'TEST\_%' ESCAPE '\\' AND Date >= NOW() - INTERVAL 1 DAY"
|
||||
)
|
||||
st.success(f"Toutes les sondes **TEST_** des dernières 24h supprimées dans `{site}`.")
|
||||
|
||||
if do_cleanup:
|
||||
try:
|
||||
cleanup()
|
||||
except Exception as e:
|
||||
st.error(f"Erreur de nettoyage : {e}")
|
||||
|
||||
212
app/Monitor.py
212
app/Monitor.py
@@ -1,212 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from utils_db import connect_to_mysql
|
||||
from dotenv import load_dotenv
|
||||
from utils_sms import envoyer_sms
|
||||
|
||||
# === AJOUT GYRO (MQTT) ===
|
||||
import paho.mqtt.client as mqtt
|
||||
from contextlib import closing
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# Logs
|
||||
# -----------------------------------------------------------
|
||||
if os.name != 'nt':
|
||||
log_dir = Path('/home/debian/Gestion_sondes/Logs')
|
||||
else:
|
||||
log_dir = Path.cwd() / 'Logs'
|
||||
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# Env
|
||||
# -----------------------------------------------------------
|
||||
load_dotenv()
|
||||
ENVOI_SMS = os.getenv("ENVOI_SMS") == "1"
|
||||
|
||||
# === AJOUT GYRO (MQTT) ===
|
||||
MQTT_HOST = os.getenv("MQTT_HOST")
|
||||
MQTT_PORT = int(os.getenv("MQTT_PORT"))
|
||||
MQTT_USER = os.getenv("MQTT_USER")
|
||||
MQTT_PASS = os.getenv("MQTT_PASS", "")
|
||||
MQTT_QOS = 1
|
||||
GYRO_PUBLISH_GLOBAL = os.getenv("GYRO_PUBLISH_GLOBAL", "0") == "1"
|
||||
|
||||
print("▶️ Lancement Monitor.py")
|
||||
|
||||
# --- Suivi des alertes actives pour rappels ---
|
||||
alertes_actives = {}
|
||||
|
||||
# === AJOUT GYRO (MQTT) ===
|
||||
def publish_gyro_states(states_by_site: dict):
|
||||
"""Publie Alarmes/<Site>/Gyro = ON|OFF (retained). Optionnel : Alarmes/Global/Gyro."""
|
||||
client = mqtt.Client(
|
||||
client_id="MonitorGyroPublisher",
|
||||
clean_session=True, # OK si protocol = MQTTv311
|
||||
protocol=mqtt.MQTTv311,
|
||||
callback_api_version=mqtt.CallbackAPIVersion.VERSION2
|
||||
)
|
||||
if MQTT_USER or MQTT_PASS:
|
||||
client.username_pw_set(MQTT_USER, MQTT_PASS)
|
||||
client.connect(MQTT_HOST, MQTT_PORT, keepalive=30)
|
||||
client.loop_start()
|
||||
try:
|
||||
for site, state in states_by_site.items():
|
||||
topic = f"Alarmes/{site}/Gyro"
|
||||
client.publish(topic, state, qos=MQTT_QOS, retain=True)
|
||||
print(f"📣 MQTT publish {topic} = {state} (retain)", flush=True)
|
||||
|
||||
if GYRO_PUBLISH_GLOBAL:
|
||||
global_state = "ON" if any(v == "ON" for v in states_by_site.values()) else "OFF"
|
||||
client.publish("Alarmes/Global/Gyro", global_state, qos=MQTT_QOS, retain=True)
|
||||
print(f"📣 MQTT publish Alarmes/Global/Gyro = {global_state} (retain)", flush=True)
|
||||
finally:
|
||||
client.loop_stop()
|
||||
client.disconnect()
|
||||
|
||||
# --- Fonction de surveillance ---
|
||||
def surveiller():
|
||||
global alertes_actives
|
||||
log_entries = []
|
||||
try:
|
||||
conn = connect_to_mysql()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# Liste des lieux
|
||||
cursor.execute("SELECT DISTINCT Lieu FROM `Chambres_froides`")
|
||||
lieux = [row['Lieu'] for row in cursor.fetchall()]
|
||||
|
||||
for lieu in lieux:
|
||||
table_temp = lieu
|
||||
table_alertes = f"Alertes_{lieu}"
|
||||
|
||||
# Sondes actives et non en entretien
|
||||
cursor.execute("""
|
||||
SELECT Sonde, Temp_Max
|
||||
FROM Sondes.Chambres_froides
|
||||
WHERE Lieu = %s
|
||||
AND Etat = 'ON'
|
||||
AND En_entretien = 0
|
||||
""", (lieu,))
|
||||
sondes = cursor.fetchall()
|
||||
|
||||
for sonde in sondes:
|
||||
nom_sonde = sonde['Sonde']
|
||||
seuil = sonde['Temp_Max']
|
||||
|
||||
# Derniers relevés (30 min = 6 pas de 5 min)
|
||||
cursor.execute(f"""
|
||||
SELECT Date, Temperature FROM {table_temp}
|
||||
WHERE Sonde = %s
|
||||
ORDER BY Date DESC LIMIT 6
|
||||
""", (nom_sonde,))
|
||||
releves = cursor.fetchall()
|
||||
|
||||
# Logging détaillé
|
||||
for r in releves:
|
||||
log_entries.append({
|
||||
"Date": r['Date'],
|
||||
"Lieu": lieu,
|
||||
"Sonde": nom_sonde,
|
||||
"Température": r['Temperature'],
|
||||
"Seuil": seuil,
|
||||
"État": "Dépassement" if r['Temperature'] > seuil else "Normal"
|
||||
})
|
||||
|
||||
# Détection dépassement > 30 min
|
||||
if len(releves) == 6:
|
||||
toutes_hors_seuil = all(r['Temperature'] > seuil for r in releves)
|
||||
plus_ancien = releves[-1]['Date']
|
||||
maintenant = datetime.now()
|
||||
|
||||
if toutes_hors_seuil and (maintenant - plus_ancien >= timedelta(minutes=30)):
|
||||
cursor.execute(f"""
|
||||
SELECT COUNT(*) as total FROM {table_alertes}
|
||||
WHERE Sonde=%s AND Status='En cours'
|
||||
""", (nom_sonde,))
|
||||
en_cours = cursor.fetchone()
|
||||
if en_cours['total'] == 0:
|
||||
cursor.execute(
|
||||
f"INSERT INTO {table_alertes} (Sonde, Debut_defaut, Status) VALUES (%s, NOW(), 'En cours')",
|
||||
(nom_sonde,)
|
||||
)
|
||||
print(f"🚨 Alerte déclenchée pour {nom_sonde} ({lieu})", flush=True)
|
||||
|
||||
message = (
|
||||
f"La sonde '{nom_sonde}' du site '{lieu}' a dépassé le seuil de {seuil}°C "
|
||||
f"depuis plus de 30 minutes.\nHeure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
if ENVOI_SMS:
|
||||
envoyer_sms(lieu, message)
|
||||
|
||||
alertes_actives[nom_sonde] = maintenant
|
||||
|
||||
else:
|
||||
# Rappel SMS toutes les 1h si toujours en défaut
|
||||
dernier_envoi = alertes_actives.get(nom_sonde)
|
||||
if dernier_envoi and (maintenant - dernier_envoi >= timedelta(hours=1)):
|
||||
message = (
|
||||
f"La sonde '{nom_sonde}' du site '{lieu}' est TOUJOURS en dépassement de seuil (>{seuil}°C).\n"
|
||||
f"Heure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
if ENVOI_SMS:
|
||||
envoyer_sms(lieu, message)
|
||||
alertes_actives[nom_sonde] = maintenant
|
||||
|
||||
# Vérifier retour à la normale (Acquittement)
|
||||
cursor.execute(f"""
|
||||
SELECT Temperature FROM {table_temp}
|
||||
WHERE Sonde = %s
|
||||
ORDER BY Date DESC LIMIT 1
|
||||
""", (nom_sonde,))
|
||||
derniere = cursor.fetchone()
|
||||
if derniere and derniere['Temperature'] <= seuil:
|
||||
cursor.execute(f"""
|
||||
UPDATE {table_alertes}
|
||||
SET Status = 'Acquitté'
|
||||
WHERE Sonde = %s AND Status IN ('En cours', 'Test')
|
||||
""", (nom_sonde,))
|
||||
|
||||
if nom_sonde in alertes_actives:
|
||||
del alertes_actives[nom_sonde]
|
||||
|
||||
# --- À ce stade : tables d'alertes mises à jour.
|
||||
# On calcule l'état GYRO par site et on publie MQTT.
|
||||
# === AJOUT GYRO (calcul + publication) ===
|
||||
states = {}
|
||||
for lieu in lieux:
|
||||
table_alertes = f"Alertes_{lieu}"
|
||||
# Etat actif si Status <> 'Acquitté'
|
||||
cursor.execute(f"SELECT EXISTS(SELECT 1 FROM {table_alertes} WHERE Status <> 'Acquitté' LIMIT 1) AS actif")
|
||||
actif = cursor.fetchone()
|
||||
states[lieu] = "ON" if actif and list(actif.values())[0] == 1 else "OFF"
|
||||
|
||||
# Commit avant publication MQTT (les états reflètent la DB)
|
||||
conn.commit()
|
||||
|
||||
# Publie ON/OFF par site (retain)
|
||||
publish_gyro_states(states)
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
# Ecriture log CSV
|
||||
if log_entries:
|
||||
import pandas as pd
|
||||
df_logs = pd.DataFrame(log_entries)
|
||||
try:
|
||||
df_logs.to_csv(log_dir / "monitor.csv", sep=";", index=False)
|
||||
print(f"✅ Log écrit dans {log_dir}/monitor.csv", flush=True)
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de l'écriture du fichier de log : {e}", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erreur : {e}", flush=True)
|
||||
|
||||
# --- Boucle principale ---
|
||||
while True:
|
||||
print(f"📡 Vérification à {datetime.now()}", flush=True)
|
||||
surveiller()
|
||||
time.sleep(300) # 5 minutes
|
||||
@@ -1,174 +0,0 @@
|
||||
import mysql.connector
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
if os.name != 'nt':
|
||||
log_dir = Path('/home/debian/Gestion_sondes/Logs')
|
||||
else:
|
||||
log_dir = Path.cwd() / 'Logs'
|
||||
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
load_dotenv()
|
||||
ENVOI_SMS = os.getenv("ENVOI_SMS") == "1"
|
||||
|
||||
# --- Config MySQL ---
|
||||
config = {
|
||||
"host": os.getenv("DB_HOST"),
|
||||
"user": os.getenv("DB_USER"),
|
||||
"password": os.getenv("DB_PASSWORD"),
|
||||
"database": os.getenv("DB_NAME")
|
||||
}
|
||||
|
||||
# --- Suivi des alertes actives pour rappels ---
|
||||
alertes_actives = {}
|
||||
|
||||
# --- Fonction d'envoi de mail ---
|
||||
def envoyer_sms_ovh(message, lieu):
|
||||
try:
|
||||
import requests
|
||||
sms_data = {
|
||||
"account": os.getenv("OVH_SMS_ACCOUNT"),
|
||||
"login": os.getenv("OVH_SERVICE_NAME"),
|
||||
"password": os.getenv("OVH_PASSWORD"),
|
||||
"message": f"{lieu}: {message}",
|
||||
"receivers": os.getenv("SMS_RECEIVER", "").split(","),
|
||||
"sender": os.getenv("OVH_SMS_SENDER")
|
||||
}
|
||||
|
||||
# Exemple d'envoi avec l'API OVH (à adapter selon ton endpoint exact)
|
||||
response = requests.post("https://www.ovh.com/cgi-bin/sms/http2sms.cgi", data=sms_data)
|
||||
print(f"📱 SMS envoyé : {response.text}", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erreur envoi SMS : {e}", flush=True)
|
||||
|
||||
# --- Fonction de surveillance ---
|
||||
def surveiller():
|
||||
global alertes_actives
|
||||
log_entries = []
|
||||
try:
|
||||
conn = mysql.connector.connect(**config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT DISTINCT Lieu FROM Sondes.Chambres_froides")
|
||||
lieux = [row['Lieu'] for row in cursor.fetchall()]
|
||||
|
||||
for lieu in lieux:
|
||||
table_temp = lieu
|
||||
table_alertes = f"Alertes_{lieu}"
|
||||
|
||||
cursor.execute("SELECT Sonde, Temp_Max FROM Sondes.Chambres_froides WHERE Lieu=%s AND Etat='ON'", (lieu,))
|
||||
sondes = cursor.fetchall()
|
||||
|
||||
for sonde in sondes:
|
||||
nom_sonde = sonde['Sonde']
|
||||
seuil = sonde['Temp_Max']
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT Date, Temperature FROM {table_temp}
|
||||
WHERE Sonde = %s
|
||||
ORDER BY Date DESC LIMIT 6
|
||||
""", (nom_sonde,))
|
||||
releves = cursor.fetchall()
|
||||
|
||||
for r in releves:
|
||||
log_entries.append({
|
||||
"Date": r['Date'],
|
||||
"Lieu": lieu,
|
||||
"Sonde": nom_sonde,
|
||||
"Température": r['Temperature'],
|
||||
"Seuil": seuil,
|
||||
"État": "Dépassement" if r['Temperature'] > seuil else "Normal"
|
||||
})
|
||||
|
||||
if len(releves) == 6:
|
||||
toutes_hors_seuil = all(r['Temperature'] > seuil for r in releves)
|
||||
plus_ancien = releves[-1]['Date']
|
||||
maintenant = datetime.now()
|
||||
|
||||
if toutes_hors_seuil and (maintenant - plus_ancien >= timedelta(minutes=30)):
|
||||
cursor.execute(f"""
|
||||
SELECT COUNT(*) as total FROM {table_alertes}
|
||||
WHERE Sonde=%s AND Status='En cours'
|
||||
""", (nom_sonde,))
|
||||
en_cours = cursor.fetchone()
|
||||
if en_cours['total'] == 0:
|
||||
cursor.execute(
|
||||
f"INSERT INTO {table_alertes} (Sonde, Debut_defaut, Status) VALUES (%s, NOW(), 'En cours')",
|
||||
(nom_sonde,)
|
||||
)
|
||||
print(f"🚨 Alerte déclenchée pour {nom_sonde} ({lieu})", flush=True)
|
||||
|
||||
sujet = f"🚨 ALERTE TEMPÉRATURE - {nom_sonde} ({lieu})"
|
||||
message = (
|
||||
f"La sonde '{nom_sonde}' du site '{lieu}' a dépassé le seuil de {seuil}°C "
|
||||
f"depuis plus de 30 minutes.\nHeure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
|
||||
if ENVOI_SMS:
|
||||
envoyer_sms_ovh(message, lieu)
|
||||
|
||||
# Suivi pour rappels
|
||||
alertes_actives[nom_sonde] = maintenant
|
||||
|
||||
else:
|
||||
# Alerte déjà en cours : vérifier s'il faut faire un rappel
|
||||
dernier_envoi = alertes_actives.get(nom_sonde)
|
||||
if dernier_envoi and (maintenant - dernier_envoi >= timedelta(hours=1)):
|
||||
sujet = f"🔔 RAPPEL ALERTE TEMPÉRATURE - {nom_sonde} ({lieu})"
|
||||
message = (
|
||||
f"La sonde '{nom_sonde}' du site '{lieu}' est TOUJOURS en dépassement de seuil (>{seuil}°C).\n"
|
||||
f"Heure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
|
||||
if ENVOI_SMS:
|
||||
envoyer_sms_ovh(message, lieu)
|
||||
alertes_actives[nom_sonde] = maintenant
|
||||
|
||||
# Vérifier retour à la normale (Acquittement)
|
||||
cursor.execute(f"""
|
||||
SELECT Temperature FROM {table_temp}
|
||||
WHERE Sonde = %s
|
||||
ORDER BY Date DESC LIMIT 1
|
||||
""", (nom_sonde,))
|
||||
derniere = cursor.fetchone()
|
||||
if derniere and derniere['Temperature'] <= seuil:
|
||||
cursor.execute(f"""
|
||||
UPDATE {table_alertes}
|
||||
SET Status = 'Acquitté'
|
||||
WHERE Sonde = %s AND Status IN ('En cours', 'Test')
|
||||
""", (nom_sonde,))
|
||||
|
||||
# Nettoyage du suivi si normalisé
|
||||
if nom_sonde in alertes_actives:
|
||||
del alertes_actives[nom_sonde]
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if log_entries:
|
||||
import pandas as pd
|
||||
df_logs = pd.DataFrame(log_entries)
|
||||
|
||||
# Sauvegarde principale
|
||||
df_logs.to_csv(log_dir / "monitor.csv", sep=";", index=False)
|
||||
|
||||
# Sauvegarde secondaire (Linux uniquement)
|
||||
if os.name != 'nt':
|
||||
df_logs.to_csv("/var/log/monitor.csv", sep=";", index=False)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erreur : {e}", flush=True)
|
||||
|
||||
# --- Boucle principale ---
|
||||
while True:
|
||||
print(f"📡 Vérification à {datetime.now()}", flush=True)
|
||||
surveiller()
|
||||
time.sleep(300) # 5 minutes
|
||||
|
||||
82
app/db.py
82
app/db.py
@@ -1,82 +0,0 @@
|
||||
# utils/db.py
|
||||
|
||||
from datetime import datetime
|
||||
from app.utils_db import connect_to_mysql # ✅ Import centralisé
|
||||
|
||||
def get_latest_chaufferie():
|
||||
"""Renvoie les dernières valeurs par sonde dans la table 'Chaufferie'."""
|
||||
db = connect_to_mysql()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT c1.*
|
||||
FROM Sondes.Chaufferie c1
|
||||
INNER JOIN (
|
||||
SELECT Sonde, MAX(Date) AS MaxDate
|
||||
FROM Sondes.Chaufferie
|
||||
GROUP BY Sonde
|
||||
) c2 ON c1.Sonde = c2.Sonde AND c1.Date = c2.MaxDate
|
||||
ORDER BY c1.Sonde;
|
||||
"""
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return result
|
||||
|
||||
|
||||
def get_history_by_sonde(sonde: str, start: datetime, end: datetime):
|
||||
"""Retourne l’historique des températures d’une sonde entre deux dates."""
|
||||
db = connect_to_mysql()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT * FROM Sondes.Chaufferie
|
||||
WHERE Sonde = %s AND Date BETWEEN %s AND %s
|
||||
ORDER BY Date;
|
||||
"""
|
||||
cursor.execute(query, (sonde, start, end))
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return result
|
||||
|
||||
|
||||
def verifier_utilisateur_commun(utilisateur: str, motdepasse: str):
|
||||
"""Vérifie si un utilisateur (non superviseur) existe dans la table MotsDePasse."""
|
||||
db = connect_to_mysql()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT * FROM Sondes.MotsDePasse
|
||||
WHERE utilisateur = %s AND mot_de_passe = %s AND role = 'utilisateur'
|
||||
"""
|
||||
cursor.execute(query, (utilisateur, motdepasse))
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return result
|
||||
|
||||
|
||||
def lire_alertes_sondes():
|
||||
"""Renvoie la liste des alertes non acquittées dans la table Alertes_Chaufferie."""
|
||||
db = connect_to_mysql()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT * FROM Sondes.Alertes_Chaufferie
|
||||
WHERE Etat != 'Acquitté'
|
||||
ORDER BY Debut_defaut DESC
|
||||
"""
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return result
|
||||
|
||||
|
||||
def acquitter_alerte(id_alerte: int):
|
||||
"""Met à jour une alerte comme acquittée dans la base."""
|
||||
db = connect_to_mysql()
|
||||
cursor = db.cursor()
|
||||
query = "UPDATE Sondes.Alertes_Chaufferie SET Etat = 'Acquitté' WHERE Id = %s"
|
||||
cursor.execute(query, (id_alerte,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
@@ -1,35 +0,0 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
def setup_logger(log_filename: str, dossier_logs: str = "/var/log/Cuisine_Saclay") -> None:
|
||||
"""
|
||||
Configure le logger pour écrire à la fois dans un fichier et sur la console.
|
||||
|
||||
:param log_filename: Nom du fichier de log (exemple : 'Cuisine_Saclay.log')
|
||||
:param dossier_logs: Dossier où enregistrer les logs (par défaut : /var/log/Cuisine_Saclay)
|
||||
"""
|
||||
|
||||
# 📁 Créer le dossier s'il n'existe pas
|
||||
os.makedirs(dossier_logs, exist_ok=True)
|
||||
|
||||
# 📄 Chemin complet du fichier de log
|
||||
logfile = os.path.join(dossier_logs, log_filename)
|
||||
|
||||
# 📝 Configuration de base du logger (fichier)
|
||||
logging.basicConfig(
|
||||
filename=logfile,
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
filemode="a" # ajouter au fichier existant
|
||||
)
|
||||
|
||||
# 🔔 Ajout de la sortie console
|
||||
console = logging.StreamHandler()
|
||||
console.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
console.setFormatter(formatter)
|
||||
|
||||
# 👇 Ajouter le handler console au logger racine
|
||||
logging.getLogger('').addHandler(console)
|
||||
|
||||
logging.info(f"Logger initialisé. Fichier de log : {logfile}")
|
||||
@@ -1,48 +0,0 @@
|
||||
import argparse
|
||||
import paho.mqtt.client as mqtt_client
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
from logger_config import setup_logger
|
||||
from utils_db import connect_to_mysql
|
||||
from functools import partial
|
||||
|
||||
def on_message(table_sql, _client, _userdata, msg):
|
||||
try:
|
||||
logging.info(f"Message reçu sur {msg.topic}: {msg.payload.decode()}")
|
||||
cursor = mydb.cursor()
|
||||
sonde_name = '/'.join(msg.topic.split('/')[1:])
|
||||
sql = f"INSERT INTO {table_sql} (Sonde, Temperature) VALUES (%s, %s)"
|
||||
val = (sonde_name, msg.payload.decode())
|
||||
cursor.execute(sql, val)
|
||||
mydb.commit()
|
||||
logging.info(f"Insertion réussie : {val}")
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors de l'insertion du message : {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--log", required=True, help="Nom du fichier de log")
|
||||
parser.add_argument("--table", required=True, help="Nom complet de la table SQL")
|
||||
parser.add_argument("--topic", required=True, help="Topic MQTT à écouter")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 📋 Initialiser le logger
|
||||
setup_logger(args.log)
|
||||
|
||||
# 🔑 Charger les variables d'environnement
|
||||
load_dotenv()
|
||||
|
||||
# 🔌 Connexion MySQL
|
||||
mydb = connect_to_mysql()
|
||||
|
||||
# 📡 Connexion MQTT
|
||||
try:
|
||||
client = mqtt_client.Client()
|
||||
client.username_pw_set("Bwps", "scJ5ACj2keRfI^")
|
||||
client.on_message = partial(on_message, args.table)
|
||||
client.connect("54.36.188.119", 1883, 60)
|
||||
client.subscribe(args.topic)
|
||||
logging.info(f"Connexion MQTT réussie et abonnement au topic '{args.topic}'.")
|
||||
client.loop_forever()
|
||||
except Exception as err:
|
||||
logging.error(f"Erreur MQTT : {err}")
|
||||
@@ -1,68 +0,0 @@
|
||||
import mysql.connector
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def connect_to_mysql():
|
||||
return mysql.connector.connect(
|
||||
host=os.getenv("DB_HOST"),
|
||||
user=os.getenv("DB_USER"),
|
||||
password=os.getenv("DB_PASS"),
|
||||
database=os.getenv("DB_NAME")
|
||||
)
|
||||
|
||||
def get_latest_chaufferie():
|
||||
conn = connect_to_mysql()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT Sonde, Temperature, Date, Topic
|
||||
FROM Sondes.Chaufferie
|
||||
WHERE Date >= NOW() - INTERVAL 5 MINUTE
|
||||
ORDER BY Date DESC
|
||||
"""
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
def get_history_by_sonde(sonde):
|
||||
conn = connect_to_mysql()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT Sonde, Temperature, Date
|
||||
FROM Sondes.Chaufferie
|
||||
WHERE Sonde = %s
|
||||
AND Date >= NOW() - INTERVAL 1 DAY
|
||||
|
||||
"""
|
||||
cursor.execute(query, (sonde,))
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
def lire_alertes_sondes():
|
||||
conn = connect_to_mysql()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT Id, Sonde, Debut_defaut, Etat
|
||||
FROM Sondes.Alertes_Chaufferie
|
||||
WHERE Etat != 'Acquitté'
|
||||
ORDER BY Debut_defaut DESC
|
||||
"""
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
def acquitter_alerte(id_alerte):
|
||||
conn = connect_to_mysql()
|
||||
cursor = conn.cursor()
|
||||
query = "UPDATE Sondes.Alertes_Chaufferie SET Etat = 'Acquitté' WHERE Id = %s"
|
||||
cursor.execute(query, (id_alerte,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
@@ -1,52 +0,0 @@
|
||||
import os
|
||||
import ovh
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def envoyer_sms(message: str, lieu: str = ""):
|
||||
try:
|
||||
client = ovh.Client(
|
||||
endpoint=os.getenv("OVH_ENDPOINT"),
|
||||
application_key=os.getenv("OVH_APP_KEY"),
|
||||
application_secret=os.getenv("OVH_APP_SECRET"),
|
||||
consumer_key=os.getenv("OVH_CONSUMER_KEY"),
|
||||
)
|
||||
|
||||
services = client.get('/sms/')
|
||||
if not services:
|
||||
print("❌ Aucun service SMS OVH trouvé", flush=True)
|
||||
return
|
||||
|
||||
service_name = services[0]
|
||||
numero_dest = os.getenv("SMS_RECEIVER")
|
||||
sender = os.getenv("OVH_SMS_SENDER")
|
||||
|
||||
if numero_dest.startswith('+'):
|
||||
numero_dest = '00' + numero_dest[1:]
|
||||
|
||||
if not numero_dest or not numero_dest.isdigit():
|
||||
print(f"❌ Numéro de téléphone invalide ou manquant : '{numero_dest}'", flush=True)
|
||||
return
|
||||
|
||||
payload = {
|
||||
"sender": sender,
|
||||
"receivers": [numero_dest],
|
||||
"message": message, # Pas d'encodage ni de nettoyage ici
|
||||
"priority": "high",
|
||||
"noStopClause": False
|
||||
|
||||
}
|
||||
|
||||
print("📤 Requête envoyée à OVH :")
|
||||
print(payload)
|
||||
|
||||
result = client.post(f'/sms/{service_name}/jobs', **payload)
|
||||
|
||||
print(f"📱 SMS envoyé à {numero_dest} pour {lieu}. Job ID : {result['ids']}", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur envoi SMS : {e}", flush=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
envoyer_sms("Test SMS OVH", lieu="utils_sms")
|
||||
Reference in New Issue
Block a user