Modif watchdog relevé uniquement par mail
This commit is contained in:
4
.env
4
.env
@@ -5,7 +5,6 @@ DB_PASS=TX.)-U1!zq5Axdk4
|
|||||||
DB_NAME=Sondes
|
DB_NAME=Sondes
|
||||||
AUTH_USERS=[{"user":"Michel","pass":"210462"}]
|
AUTH_USERS=[{"user":"Michel","pass":"210462"}]
|
||||||
|
|
||||||
|
|
||||||
# MQTT
|
# MQTT
|
||||||
MQTT_HOST=162.19.78.131
|
MQTT_HOST=162.19.78.131
|
||||||
MQTT_USER=sondes
|
MQTT_USER=sondes
|
||||||
@@ -39,9 +38,10 @@ SMTP_SECURITY=STARTTLS
|
|||||||
SMTP_USER=services@domo91.fr
|
SMTP_USER=services@domo91.fr
|
||||||
SMTP_PASS='VHq3278YA#sGV*bh#mR'
|
SMTP_PASS='VHq3278YA#sGV*bh#mR'
|
||||||
MAIL_FROM=services@domo91.fr
|
MAIL_FROM=services@domo91.fr
|
||||||
|
MAIL_TO=services@domo91.fr
|
||||||
MAIL_TO_SACLAY=robots@domo91.fr,nicolas.thibaut@bw-paris-saclay.com
|
MAIL_TO_SACLAY=robots@domo91.fr,nicolas.thibaut@bw-paris-saclay.com
|
||||||
MAIL_FROM_SACLAY="DOMO91 Saclay <services@domo91.fr>"
|
MAIL_FROM_SACLAY="DOMO91 Saclay <services@domo91.fr>"
|
||||||
MAIL_TO_MEUDON=robots@domo91.fr,superviseur.restauration@parismeudonermitage.com,chef@parismeudonermitage.com
|
MAIL_TO_MEUDON=robots@domo91.fr,chef@parismeudonermitage.com
|
||||||
MAIL_FROM_MEUDON="DOMO91 Meudon <services@domo91.fr>"
|
MAIL_FROM_MEUDON="DOMO91 Meudon <services@domo91.fr>"
|
||||||
|
|
||||||
# --- Paramètres SMS ----
|
# --- Paramètres SMS ----
|
||||||
|
|||||||
@@ -1,39 +1,31 @@
|
|||||||
#!/home/debian/Gestion_sondes/myenv/bin/python3
|
#!/home/debian/Gestion_sondes/myenv/bin/python3
|
||||||
|
# Surveillance de l'arrivée des relevés (par table/site) + SMS d'alerte / retour à la normale
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
import utils_db
|
|
||||||
import logging
|
import logging
|
||||||
from utils_sms import envoyer_sms
|
|
||||||
|
|
||||||
# Dossier Logs
|
import mysql.connector # important pour cibler les exceptions MySQL
|
||||||
|
|
||||||
|
import utils_db
|
||||||
|
from utils_mail import envoyer_mail
|
||||||
|
|
||||||
|
# -------------------- LOGS --------------------
|
||||||
LOG_DIR = '/home/debian/Gestion_sondes/Logs'
|
LOG_DIR = '/home/debian/Gestion_sondes/Logs'
|
||||||
os.makedirs(LOG_DIR, exist_ok=True)
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
# Fichier de log (nommé par date)
|
|
||||||
log_filename = os.path.join(LOG_DIR, datetime.now().strftime("surveillance_%Y-%m-%d.log"))
|
log_filename = os.path.join(LOG_DIR, datetime.now().strftime("surveillance_%Y-%m-%d.log"))
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
handlers=[
|
handlers=[logging.FileHandler(log_filename), logging.StreamHandler()]
|
||||||
logging.FileHandler(log_filename),
|
|
||||||
logging.StreamHandler() # Affiche aussi les logs dans la console
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Charger .env
|
# -------------------- ENV --------------------
|
||||||
load_dotenv('/home/debian/Gestion_sondes/.env')
|
load_dotenv('/home/debian/Gestion_sondes/.env')
|
||||||
|
|
||||||
# OVH SMS
|
# -------------------- PARAMETRES --------------------
|
||||||
APP_KEY = os.getenv('OVH_APPLICATION_KEY')
|
|
||||||
APP_SECRET = os.getenv('OVH_APPLICATION_SECRET')
|
|
||||||
CONSUMER_KEY = os.getenv('OVH_CONSUMER_KEY')
|
|
||||||
SERVICE_NAME = os.getenv('OVH_SMS_SERVICE')
|
|
||||||
SMS_RECEIVER = os.getenv('SMS_RECEIVER')
|
|
||||||
SMS_SENDER = os.getenv('OVH_SMS_SENDER')
|
|
||||||
|
|
||||||
tables = ['Saclay', 'Meudon']
|
tables = ['Saclay', 'Meudon']
|
||||||
DELAI_MINUTES = 15
|
DELAI_MINUTES = 15
|
||||||
RAPPEL_HEURES = 6
|
RAPPEL_HEURES = 6
|
||||||
@@ -41,61 +33,128 @@ RAPPEL_HEURES = 6
|
|||||||
STATE_DIR = '/tmp/surveillance_states'
|
STATE_DIR = '/tmp/surveillance_states'
|
||||||
os.makedirs(STATE_DIR, exist_ok=True)
|
os.makedirs(STATE_DIR, exist_ok=True)
|
||||||
|
|
||||||
def should_send_alert(site):
|
TABLES_SET = set(tables) # whitelist simple
|
||||||
state_file = os.path.join(STATE_DIR, f'{site}.state')
|
|
||||||
|
def _state_file(site: str) -> str:
|
||||||
|
return os.path.join(STATE_DIR, f'{site}.state')
|
||||||
|
|
||||||
|
def should_send_alert(site: str) -> bool:
|
||||||
|
"""
|
||||||
|
Retourne True si on doit envoyer une alerte maintenant (première fois ou rappel).
|
||||||
|
Met à jour le fichier d'état en cas d'envoi.
|
||||||
|
"""
|
||||||
|
sf = _state_file(site)
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
if not os.path.exists(state_file):
|
|
||||||
with open(state_file, 'w') as f:
|
if not os.path.exists(sf):
|
||||||
|
# première alerte
|
||||||
|
try:
|
||||||
|
with open(sf, 'w') as f:
|
||||||
f.write(now.isoformat())
|
f.write(now.isoformat())
|
||||||
|
except OSError as e:
|
||||||
|
logging.warning(f"Impossible d'écrire l'état {sf} : {e} (on alerte quand même).")
|
||||||
return True
|
return True
|
||||||
with open(state_file, 'r') as f:
|
|
||||||
last_alert = datetime.fromisoformat(f.read().strip())
|
try:
|
||||||
|
with open(sf, 'r') as f:
|
||||||
|
raw = f.read().strip()
|
||||||
|
last_alert = datetime.fromisoformat(raw)
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
# état illisible/corrompu -> on réinitialise et on alerte
|
||||||
|
logging.warning(f"Etat corrompu pour {site} ({sf}) : {e}. Réinitialisation.")
|
||||||
|
try:
|
||||||
|
with open(sf, 'w') as f:
|
||||||
|
f.write(now.isoformat())
|
||||||
|
except OSError as e2:
|
||||||
|
logging.warning(f"Impossible de réécrire l'état {sf} : {e2}")
|
||||||
|
return True
|
||||||
|
|
||||||
if now - last_alert >= timedelta(hours=RAPPEL_HEURES):
|
if now - last_alert >= timedelta(hours=RAPPEL_HEURES):
|
||||||
with open(state_file, 'w') as f:
|
try:
|
||||||
|
with open(sf, 'w') as f:
|
||||||
f.write(now.isoformat())
|
f.write(now.isoformat())
|
||||||
|
except OSError as e:
|
||||||
|
logging.warning(f"Impossible de mettre à jour l'état {sf} : {e} (on alerte quand même).")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def clear_state(site):
|
def clear_state(site: str) -> None:
|
||||||
state_file = os.path.join(STATE_DIR, f'{site}.state')
|
sf = _state_file(site)
|
||||||
if os.path.exists(state_file):
|
try:
|
||||||
os.remove(state_file)
|
if os.path.exists(sf):
|
||||||
|
os.remove(sf)
|
||||||
|
except OSError as e:
|
||||||
|
logging.warning(f"Impossible de supprimer l'état {sf} : {e}")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
cnx = utils_db.connect_to_mysql() # ← appel via db_utils
|
|
||||||
cursor = cnx.cursor()
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
limite = now - timedelta(minutes=DELAI_MINUTES)
|
|
||||||
problemes = []
|
problemes = []
|
||||||
|
limite = datetime.now() - timedelta(minutes=DELAI_MINUTES)
|
||||||
|
|
||||||
|
# 1) Connexion MySQL (une seule fois)
|
||||||
|
try:
|
||||||
|
cnx = utils_db.connect_to_mysql()
|
||||||
|
cursor = cnx.cursor()
|
||||||
|
except mysql.connector.Error as e:
|
||||||
|
logging.error(f"MySQL KO : {e}")
|
||||||
|
envoyer_mail("⚠️ ALERTE : Base MySQL inaccessible (surveillance impossible).")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2) Surveillance par table (try SQL à l'intérieur de la boucle)
|
||||||
|
try:
|
||||||
for table in tables:
|
for table in tables:
|
||||||
cursor.execute(f"SELECT MAX(Date) FROM {table}")
|
if table not in TABLES_SET:
|
||||||
result = cursor.fetchone()
|
logging.warning(f"Table ignorée (non whitelistée) : {table}")
|
||||||
last_update = result[0]
|
continue
|
||||||
|
|
||||||
if not last_update or last_update < limite:
|
# 2a) Lecture de la dernière date (erreurs SQL gérées finement)
|
||||||
|
try:
|
||||||
|
cursor.execute(f"SELECT MAX(Date) FROM `{table}`")
|
||||||
|
(last_update,) = cursor.fetchone()
|
||||||
|
except mysql.connector.Error as e:
|
||||||
|
logging.error(f"Erreur SQL sur {table} : {e}")
|
||||||
|
# Vous pouvez décider ici si vous voulez un SMS ou seulement un log.
|
||||||
|
if should_send_alert(table):
|
||||||
|
envoyer_mail(f"⚠️ ALERTE : erreur SQL sur {table} (voir logs).")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2b) Logique métier (hors try SQL)
|
||||||
|
if (last_update is None) or (last_update < limite):
|
||||||
if should_send_alert(table):
|
if should_send_alert(table):
|
||||||
problemes.append(f"{table} (dernier relevé : {last_update})")
|
problemes.append(f"{table} (dernier relevé : {last_update})")
|
||||||
|
logging.warning(f"⚠️ {table} en défaut (dernier relevé : {last_update})")
|
||||||
else:
|
else:
|
||||||
logging.info(f"⏳ Problème déjà signalé pour {table}, attente du délai de rappel.")
|
logging.info(f"⏳ {table} déjà signalé, rappel dans {RAPPEL_HEURES}h.")
|
||||||
else:
|
else:
|
||||||
if os.path.exists(os.path.join(STATE_DIR, f'{table}.state')):
|
# Retour à la normale uniquement si on était en défaut (state présent)
|
||||||
message = f"✅ {table} : relevés à nouveau reçus. Situation normale."
|
if os.path.exists(_state_file(table)):
|
||||||
envoyer_sms(message)
|
message = f"✅ {table} : relevés à nouveau reçus (dernier : {last_update}). Situation normale."
|
||||||
|
envoyer_mail(
|
||||||
|
f"✅ OK : {table} relevés reçus",
|
||||||
|
message
|
||||||
|
)
|
||||||
clear_state(table)
|
clear_state(table)
|
||||||
logging.info(f"📩 SMS de retour à la normale envoyé pour {table}.")
|
logging.info(f"📩 Retour à la normale envoyé pour {table}.")
|
||||||
else:
|
else:
|
||||||
logging.info(f"✅ {table} OK (dernier relevé : {last_update})")
|
logging.info(f"✅ {table} OK (dernier relevé : {last_update})")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 3) Nettoyage MySQL
|
||||||
|
try:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
cnx.close()
|
cnx.close()
|
||||||
|
except mysql.connector.Error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4) Alerte groupée si besoin
|
||||||
if problemes:
|
if problemes:
|
||||||
message = "⚠️ ALERTE : pas de relevés depuis >15min :\n" + "\n".join(problemes)
|
message = f"⚠️ ALERTE : pas de relevés depuis >{DELAI_MINUTES}min :\n" + "\n".join(problemes)
|
||||||
envoyer_sms(message)
|
envoyer_mail(
|
||||||
|
f"⚠️ ALERTE : absence de relevés > {DELAI_MINUTES} min",
|
||||||
|
message
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.info("👍 Tout est OK, aucun SMS envoyé.")
|
logging.info("👍 Tout est OK, aucun Mail envoyé.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
73
app/utils_db.py
Normal file
73
app/utils_db.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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()
|
||||||
45
app/utils_mail.py
Normal file
45
app/utils_mail.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# /home/debian/Gestion_sondes/utils_mail.py
|
||||||
|
import os
|
||||||
|
import smtplib
|
||||||
|
import logging
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv('/home/debian/Gestion_sondes/.env')
|
||||||
|
|
||||||
|
SMTP_HOST = os.getenv("SMTP_HOST", "smtp.mail.ovh.net")
|
||||||
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
||||||
|
|
||||||
|
SMTP_LOGIN = os.getenv("SMTP_LOGIN") # ex: services@domo91.fr
|
||||||
|
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD") # mot de passe OVH
|
||||||
|
MAIL_FROM = os.getenv("MAIL_FROM", SMTP_LOGIN)
|
||||||
|
MAIL_TO = os.getenv("MAIL_TO") # ex: services@domo91.fr
|
||||||
|
|
||||||
|
def envoyer_mail(sujet: str, contenu: str, destinataires=None) -> None:
|
||||||
|
"""
|
||||||
|
Envoi email via OVH SMTP SSL 465 (process identique à supervisor_watchdog.py).
|
||||||
|
destinataires: str unique ou liste; si None => MAIL_TO depuis .env
|
||||||
|
"""
|
||||||
|
if destinataires is None:
|
||||||
|
if not MAIL_TO:
|
||||||
|
raise ValueError("MAIL_TO manquant dans le .env")
|
||||||
|
destinataires = [a.strip() for a in MAIL_TO.split(",") if a.strip()]
|
||||||
|
elif isinstance(destinataires, str):
|
||||||
|
destinataires = [destinataires]
|
||||||
|
|
||||||
|
if not SMTP_LOGIN or not SMTP_PASSWORD:
|
||||||
|
raise ValueError("SMTP_LOGIN / SMTP_PASSWORD manquants dans le .env")
|
||||||
|
|
||||||
|
msg = MIMEText(contenu)
|
||||||
|
msg["Subject"] = sujet
|
||||||
|
msg["From"] = MAIL_FROM
|
||||||
|
msg["To"] = ", ".join(destinataires)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server:
|
||||||
|
server.login(SMTP_LOGIN, SMTP_PASSWORD)
|
||||||
|
server.sendmail(MAIL_FROM, destinataires, msg.as_string())
|
||||||
|
logging.info("📧 Mail envoyé: %s -> %s", sujet, destinataires)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Erreur envoi mail: %s", e)
|
||||||
|
raise
|
||||||
Reference in New Issue
Block a user