Modif watchdog relevés

This commit is contained in:
2026-01-08 08:48:33 +01:00
parent 6c3772266c
commit 2fd850e426
4 changed files with 211 additions and 134 deletions

View File

@@ -1,193 +1,255 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Mqtt_meudon.py Mqtt_meudon.py (version nettoyée)
Récupère les mesures MQTT du site Meudon et les insère dans la table Sondes.Meudon. --------------------------------
- S'abonne à Meudon/# sur le broker MQTT
- Parse les messages (topic -> nom de sonde, payload -> température)
- Insère les relevés dans MySQL (table `Meudon` par défaut)
Variables d'environnement attendues (exemples) :
- MQTT_HOST, MQTT_PORT, MQTT_USER, MQTT_PASS
- DB_HOST, DB_USER, DB_PASS, DB_NAME
Optionnelles :
- DB_TABLE (défaut: Meudon)
- LOG_FILE (défaut: Mqtt_meudon.log)
- LOG_LEVEL (défaut: INFO)
- DB_POOL_SIZE (défaut: 5)
""" """
from __future__ import annotations
import os import os
import sys
import time
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import socket from typing import Any, Optional, cast
import mysql.connector
from mysql.connector import Error
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from paho.mqtt.client import CallbackAPIVersion from paho.mqtt.client import CallbackAPIVersion
from dotenv import load_dotenv from mysql.connector import pooling
from mysql.connector.abstracts import MySQLConnectionAbstract
from mysql.connector.cursor import MySQLCursor
# ========================= # =========================
# Chargement du .env # Configuration (ENV)
# ========================= # =========================
load_dotenv()
# --- MySQL (commun) --- MQTT_HOST = os.getenv("MQTT_HOST", "127.0.0.1")
DB_HOST = os.getenv("DB_HOST")
DB_USER = os.getenv("DB_USER")
DB_PASS = os.getenv("DB_PASS")
DB_NAME = os.getenv("DB_NAME")
# --- MQTT ---
MQTT_HOST = os.getenv("MQTT_HOST")
MQTT_USER = os.getenv("MQTT_USER")
MQTT_PASS = os.getenv("MQTT_PASS")
MQTT_PORT = int(os.getenv("MQTT_PORT", "1883")) MQTT_PORT = int(os.getenv("MQTT_PORT", "1883"))
MQTT_USER = os.getenv("MQTT_USER", "")
MQTT_PASS = os.getenv("MQTT_PASS", "")
# Client ID (configurable, sinon suffixé avec le hostname) DB_HOST = os.getenv("DB_HOST", "127.0.0.1")
MQTT_CLIENT_ID = os.getenv( DB_USER = os.getenv("DB_USER", "root")
"MQTT_CLIENT_ID_MEUDON", DB_PASS = os.getenv("DB_PASS", "")
f"Mqtt_meudon_{socket.gethostname()}" DB_NAME = os.getenv("DB_NAME", "Sondes")
) DB_TABLE = os.getenv("DB_TABLE", "Meudon")
GYRO_TOPIC_MEUDON = os.getenv("GYRO_MQTT_TOPIC_MEUDON", "Meudon/gyrophare") LOG_FILE = os.getenv("LOG_FILE", "Mqtt_meudon.log")
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
DB_POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "5"))
# Nom de la table de destination
TABLE_NAME = "Meudon"
# ========================= # =========================
# Logging # Logging
# ========================= # =========================
def setup_logging():
logger = logging.getLogger()
logger.setLevel(logging.INFO)
formatter = logging.Formatter( def setup_logging() -> None:
"%(asctime)s - %(levelname)s - %(message)s", """Configure un log propre (rotation) sans dupliquer les handlers."""
datefmt="%Y-%m-%d %H:%M:%S" logger = logging.getLogger()
logger.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
# Évite la duplication si setup_logging() est rappelé
if any(isinstance(h, RotatingFileHandler) for h in logger.handlers):
return
fmt = logging.Formatter(
"%(asctime)s | %(levelname)s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
) )
# Console
console = logging.StreamHandler()
console.setFormatter(formatter)
logger.addHandler(console)
# Logs fichier (même logique que Saclay)
log_dir = os.getenv("LOG_DIR", "./Logs")
try:
os.makedirs(log_dir, exist_ok=True)
file_handler = RotatingFileHandler( file_handler = RotatingFileHandler(
os.path.join(log_dir, "Mqtt_meudon.log"), LOG_FILE,
maxBytes=1_000_000, maxBytes=2_000_000,
backupCount=5, backupCount=5,
encoding="utf-8", encoding="utf-8",
) )
file_handler.setFormatter(formatter) file_handler.setFormatter(fmt)
file_handler.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(fmt)
console_handler.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
logger.addHandler(file_handler) logger.addHandler(file_handler)
except Exception as e: logger.addHandler(console_handler)
logging.warning("Impossible de créer le fichier de log : %s", e)
# ========================= # =========================
# Accès MySQL # MySQL (pool)
# ========================= # =========================
def insert_temperature(sonde: str, temperature: float) -> None:
""" _db_pool: Optional[pooling.MySQLConnectionPool] = None
Insère une mesure dans la table Sondes.Meudon.
La colonne Date utilise CURRENT_TIMESTAMP par défaut dans MySQL.
""" def init_db_pool() -> None:
try: global _db_pool
conn = mysql.connector.connect( if _db_pool is not None:
return
_db_pool = pooling.MySQLConnectionPool(
pool_name="meudon_pool",
pool_size=DB_POOL_SIZE,
host=DB_HOST, host=DB_HOST,
user=DB_USER, user=DB_USER,
password=DB_PASS, password=DB_PASS,
database=DB_NAME, database=DB_NAME,
autocommit=False,
) )
logging.info("Pool MySQL initialisé (db=%s, size=%s)", DB_NAME, DB_POOL_SIZE)
def insert_temperature(sonde: str, temperature: float) -> None:
"""Insère un relevé dans la table configurée."""
if _db_pool is None:
init_db_pool()
conn: Optional[MySQLConnectionAbstract] = None
cursor: Optional[MySQLCursor] = None
try:
assert _db_pool is not None # pour le typage
conn = cast(MySQLConnectionAbstract, _db_pool.get_connection())
cursor = conn.cursor() cursor = conn.cursor()
sql = f"INSERT INTO {TABLE_NAME} (Sonde, Temperature) VALUES (%s, %s)"
# Colonnes supposées : Sonde, Temperature, Date
sql = f"INSERT INTO `{DB_TABLE}` (Sonde, Temperature, Date) VALUES (%s, %s, NOW())"
cursor.execute(sql, (sonde, temperature)) cursor.execute(sql, (sonde, temperature))
conn.commit() conn.commit()
logging.info("Insertion OK (Meudon) -> %s = %.2f", sonde, temperature) except Exception as exc:
logging.error("Erreur MySQL (insert %s): %s", sonde, exc)
except Error as e: # En cas d'erreur, on rollback pour garder la connexion saine
logging.exception("Erreur MySQL (Meudon) pour la sonde %s : %s", sonde, e)
finally:
try: try:
if cursor: if conn is not None:
cursor.close() conn.rollback()
if conn and conn.is_connected():
conn.close()
except Exception: except Exception:
pass pass
finally:
try:
if cursor is not None:
cursor.close()
finally:
if conn is not None:
conn.close()
# ========================= # =========================
# Callbacks MQTT (API v2) # MQTT callbacks
# ========================= # =========================
def on_connect(client, userdata, flags, reason_code, properties=None):
def _topic_to_sonde(topic: str) -> str:
"""Convertit un topic MQTT en nom de sonde.
Exemple : 'Meudon/Chambre1' -> 'Chambre1'
"""
parts = topic.split("/")
if len(parts) >= 2:
return parts[-1].strip()
return topic.strip()
def _payload_to_float(payload: bytes) -> Optional[float]:
"""Convertit un payload en float (accepte virgule)."""
try:
s = payload.decode("utf-8", errors="replace").strip()
if not s:
return None
s = s.replace(",", ".")
return float(s)
except Exception:
return None
def on_connect(
client: mqtt.Client,
_userdata: Any,
_flags: Any,
reason_code: int,
properties: Any = None,
) -> None:
# `properties` est fourni par l'API v2 ; on le garde pour compatibilité.
_ = properties # évite les avertissements "non utilisé"
if reason_code == 0: if reason_code == 0:
logging.info("Connecté au broker MQTT Meudon (%s)", MQTT_HOST) logging.info("Connecté au broker MQTT Meudon (%s:%s)", MQTT_HOST, MQTT_PORT)
# Abonnement à TOUT ce qui commence par "Meudon/"
result, mid = client.subscribe("Meudon/#") result, mid = client.subscribe("Meudon/#")
logging.info("Abonné au topic : Meudon/# (result=%s, mid=%s)", result, mid) logging.info("Abonné au topic : Meudon/# (result=%s, mid=%s)", result, mid)
else: else:
logging.error("Échec de connexion MQTT (Meudon), code retour = %s", reason_code) logging.error("Échec de connexion MQTT (Meudon), reason_code=%s", reason_code)
def on_message(client, userdata, msg: mqtt.MQTTMessage): def on_disconnect(
topic = msg.topic _client: mqtt.Client,
payload_raw = msg.payload.decode("utf-8", errors="ignore").strip() _userdata: Any,
reason_code: int,
properties: Any = None,
) -> None:
_ = properties
logging.warning("Déconnecté du broker MQTT (reason_code=%s)", reason_code)
logging.debug("Msg reçu (Meudon) : topic=%s payload=%s", topic, payload_raw)
# On ignore le gyrophare def on_message(_client: mqtt.Client, _userdata: Any, msg: mqtt.MQTTMessage) -> None:
if topic == GYRO_TOPIC_MEUDON: topic = (msg.topic or "").strip()
logging.debug("Topic gyrophare Meudon ignoré : %s", topic) sonde = _topic_to_sonde(topic)
temp = _payload_to_float(msg.payload)
if temp is None:
logging.warning("Payload non numérique ignoré (topic=%s, payload=%r)", topic, msg.payload)
return return
# Nom de la sonde = dernier segment du topic logging.info("Reçu: topic=%s -> sonde=%s | température=%s", topic, sonde, temp)
sonde = topic.split("/")[-1] if "/" in topic else topic insert_temperature(sonde, temp)
# Conversion du payload en float
try:
value = float(payload_raw.replace(",", "."))
except ValueError:
logging.warning(
"Payload non numérique (Meudon), mesure ignorée (topic=%s, payload=%s)",
topic,
payload_raw,
)
return
insert_temperature(sonde, value)
# ========================= # =========================
# Programme principal # Main
# ========================= # =========================
def main():
setup_logging()
logging.info("Démarrage du script Mqtt_meudon")
# Vérif minimale des variables d'env MySQL def build_mqtt_client() -> mqtt.Client:
for var in ["DB_HOST", "DB_USER", "DB_PASS", "DB_NAME"]: client = mqtt.Client(callback_api_version=CallbackAPIVersion.VERSION2)
if os.getenv(var) in (None, ""):
logging.error("Variable d'environnement %s manquante !", var)
# Vérif minimale des variables d'env MQTT if MQTT_USER:
if not MQTT_HOST:
logging.error("MQTT_HOST_MEUDON manquant !")
if not MQTT_USER:
logging.warning("MQTT_USER_MEUDON non défini (connexion sans login ?)")
if not MQTT_PORT or MQTT_PORT <= 0:
logging.error("MQTT_PORT_MEUDON invalide : %s", MQTT_PORT)
client = mqtt.Client(
client_id=MQTT_CLIENT_ID,
callback_api_version=CallbackAPIVersion.VERSION2
)
client.username_pw_set(MQTT_USER, MQTT_PASS) client.username_pw_set(MQTT_USER, MQTT_PASS)
client.on_connect = on_connect client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message client.on_message = on_message
# Reconnexion automatique
client.reconnect_delay_set(min_delay=1, max_delay=30)
return client
def main() -> None:
setup_logging()
init_db_pool()
client = build_mqtt_client()
while True:
try: try:
logging.info("Connexion MQTT en cours (%s:%s)...", MQTT_HOST, MQTT_PORT)
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60) client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
except Exception as e: break
logging.exception("Impossible de se connecter au broker MQTT Meudon : %s", e) except Exception as exc:
return logging.error("Impossible de se connecter au broker MQTT: %s", exc)
time.sleep(5)
logging.info("Boucle MQTT Meudon en cours (Ctrl+C pour arrêter)...") logging.info("Boucle MQTT Meudon en cours (Ctrl+C pour arrêter)...")
try: try:
@@ -195,7 +257,10 @@ def main():
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Arrêt demandé par l'utilisateur (Meudon)." ) logging.info("Arrêt demandé par l'utilisateur (Meudon)." )
finally: finally:
try:
client.disconnect() client.disconnect()
except Exception:
pass
logging.info("Client MQTT Meudon déconnecté.") logging.info("Client MQTT Meudon déconnecté.")

View File

@@ -26,9 +26,9 @@ DB_USER = os.getenv("DB_USER")
DB_PASS = os.getenv("DB_PASS") DB_PASS = os.getenv("DB_PASS")
DB_NAME = os.getenv("DB_NAME") DB_NAME = os.getenv("DB_NAME")
MQTT_HOST = "54.36.188.119" MQTT_HOST = "162.19.78.131"
MQTT_USER = "Bwps" MQTT_USER = "sondes"
MQTT_PASS = "scJ5ACj2keRfI^" MQTT_PASS = "3J@bjYP0"
MQTT_PORT = int(os.getenv("MQTT_PORT", 1883)) MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
GYRO_TOPIC_SACLAY = os.getenv("GYRO_MQTT_TOPIC_SACLAY", "Saclay/gyrophare") GYRO_TOPIC_SACLAY = os.getenv("GYRO_MQTT_TOPIC_SACLAY", "Saclay/gyrophare")

View File

@@ -5,12 +5,24 @@ from datetime import datetime, timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
import logging import logging
import sys
import mysql.connector # important pour cibler les exceptions MySQL import mysql.connector # important pour cibler les exceptions MySQL
import utils_db import utils_db
from utils_mail import envoyer_mail from utils_mail import envoyer_mail
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler(sys.stdout)]
)
# Forcer l'encodage UTF-8 du flux si possible (Windows/PyCharm)
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
# -------------------- LOGS -------------------- # -------------------- 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)

View File

@@ -10,8 +10,8 @@ load_dotenv('/home/debian/Gestion_sondes/.env')
SMTP_HOST = os.getenv("SMTP_HOST", "smtp.mail.ovh.net") SMTP_HOST = os.getenv("SMTP_HOST", "smtp.mail.ovh.net")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465")) SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
SMTP_LOGIN = os.getenv("SMTP_LOGIN") # ex: services@domo91.fr SMTP_LOGIN = os.getenv("SMTP_USER") # ex: services@domo91.fr
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD") # mot de passe OVH SMTP_PASSWORD = os.getenv("SMTP_PASS") # mot de passe OVH
MAIL_FROM = os.getenv("MAIL_FROM", SMTP_LOGIN) MAIL_FROM = os.getenv("MAIL_FROM", SMTP_LOGIN)
MAIL_TO = os.getenv("MAIL_TO") # ex: services@domo91.fr MAIL_TO = os.getenv("MAIL_TO") # ex: services@domo91.fr