relancement cuisines Saclay et Meudon
This commit is contained in:
@@ -134,6 +134,48 @@ def lire_seuils_depuis_db(site: str):
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
def lire_cfg_chambres(site: str):
|
||||
"""
|
||||
Retourne un dict {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
||||
depuis Chambres_froides pour le site.
|
||||
"""
|
||||
sql = """
|
||||
SELECT Sonde, Temp_Max, Etat, En_entretien
|
||||
FROM Chambres_froides
|
||||
WHERE Lieu=%s
|
||||
"""
|
||||
cnx = get_db()
|
||||
cfg = {}
|
||||
try:
|
||||
cur = cnx.cursor()
|
||||
cur.execute(sql, (site,))
|
||||
for sonde, temp_max, etat, en_entretien in cur.fetchall():
|
||||
cfg[str(sonde)] = {
|
||||
"temp_max": float(temp_max),
|
||||
"active": str(etat).upper() == "ON",
|
||||
"entretien": bool(int(en_entretien or 0)),
|
||||
}
|
||||
return cfg
|
||||
except MySQLError as err:
|
||||
log.exception("Erreur DB (lire_cfg_chambres): %s", err)
|
||||
return cfg
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
def compute_site_alarm(last_values: list[dict], cfg: dict[str, dict], hysteresis: float = 0.0):
|
||||
"""
|
||||
Retourne (is_on: bool, trigger: (sonde, temp, seuil) | None)
|
||||
"""
|
||||
for row in last_values:
|
||||
sonde = str(row["Sonde"])
|
||||
meta = cfg.get(sonde)
|
||||
if not meta or not meta["active"] or meta["entretien"]:
|
||||
continue
|
||||
temp = float(row["Temperature"])
|
||||
if temp > float(meta["temp_max"]) + 0.0:
|
||||
return True, (sonde, temp, float(meta["temp_max"]))
|
||||
return False, None
|
||||
|
||||
def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool:
|
||||
table = site
|
||||
cnx = get_db()
|
||||
@@ -359,8 +401,12 @@ class MQTTPublisher:
|
||||
def __init__(self, site: str):
|
||||
self.enabled = (_mqtt_ok and (os.getenv("GYRO_MODE", "").lower() == "mqtt"))
|
||||
self.site = site
|
||||
self.topic = (os.getenv(f"GYRO_MQTT_TOPIC_{site}") or
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site.capitalize()}"))
|
||||
self.topic = (
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site}") or
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site.upper()}") or
|
||||
os.getenv("GYRO_MQTT_TOPIC") or
|
||||
f"Sondes/{site}/Gyro/cmd"
|
||||
)
|
||||
self.last_state: bool | None = None
|
||||
|
||||
if not self.enabled:
|
||||
@@ -416,9 +462,9 @@ class MQTTPublisher:
|
||||
if self.last_state is not None and self.last_state == on:
|
||||
return
|
||||
|
||||
payload = "on" if on else "off"
|
||||
payload = "ON" if on else "OFF"
|
||||
try:
|
||||
r = self.client.publish(self.topic, payload=payload, qos=1, retain=True)
|
||||
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
||||
r.wait_for_publish(timeout=3)
|
||||
if r.rc != 0:
|
||||
log.warning("MQTT publish rc=%s (topic=%s)", r.rc, self.topic)
|
||||
@@ -437,21 +483,33 @@ def notifier_sur_depassement(site: str, sonde: str, temp: float, seuil: float):
|
||||
subject, sms_text, email_body = build_alert_text(site, sonde, temp, seuil)
|
||||
notifier.send_sms(sms_text)
|
||||
notifier.send_email(subject, email_body)
|
||||
try: beacon.set(True)
|
||||
except Exception: pass
|
||||
|
||||
def notifier_acquittement(site: str, sonde: str, temp: float, seuil: float):
|
||||
subject, sms_text, _ = build_ok_text(site, sonde, temp, seuil)
|
||||
notifier.send_sms(sms_text)
|
||||
try:
|
||||
if not any_alert_open(site):
|
||||
beacon.set(False)
|
||||
except Exception: pass
|
||||
|
||||
# ========= Cycle & boucle =========
|
||||
def run_monitor_cycle(site: str = SITE):
|
||||
# 1) Lecture mesures + config
|
||||
sondes = lire_sondes_depuis_db(site)
|
||||
seuils = lire_seuils_depuis_db(site)
|
||||
cfg = lire_cfg_chambres(site)
|
||||
|
||||
# 2) Gyro instantané
|
||||
try:
|
||||
gyro_on, trigger = compute_site_alarm(sondes, cfg, hysteresis=float(os.getenv("GYRO_HYSTERESIS", "0.0")))
|
||||
if trigger:
|
||||
s, t, se = trigger
|
||||
log.info("Gyro %s => ON (déclenché par %s: %.2f > %.2f)", site, s, t, se)
|
||||
else:
|
||||
log.info("Gyro %s => OFF (aucun dépassement)", site)
|
||||
beacon.set(gyro_on)
|
||||
except Exception as e:
|
||||
log.exception("Erreur calcul/publish gyrophare: %s", e)
|
||||
|
||||
# 3) Alertes “officielles” (inchangées) avec temporisation 30 min
|
||||
# On reconstitue un dict seuils à partir de la cfg (et on ignore les sondes OFF).
|
||||
seuils = {s: meta["temp_max"] for s, meta in cfg.items() if meta.get("active", False)}
|
||||
|
||||
for r in sondes:
|
||||
nom = str(r["Sonde"])
|
||||
temp = float(r["Temperature"])
|
||||
@@ -460,7 +518,7 @@ def run_monitor_cycle(site: str = SITE):
|
||||
now = now_paris()
|
||||
if temp > seuil:
|
||||
if depassement_depuis_30min(site, nom, seuil):
|
||||
# Ouvrir si pas déjà ouvert → notifier seulement si ouverture réelle
|
||||
# Ouvrir si pas déjà ouverte → notifier seulement si création
|
||||
try:
|
||||
conn = get_db()
|
||||
if open_alert(conn, f"Alertes_{site}", nom, now):
|
||||
@@ -468,7 +526,7 @@ def run_monitor_cycle(site: str = SITE):
|
||||
finally:
|
||||
conn.close()
|
||||
else:
|
||||
# Fermer si ouvert → notifier seulement si fermeture réelle
|
||||
# Fermer si ouverte → notifier seulement si fermeture réelle
|
||||
try:
|
||||
conn = get_db()
|
||||
if close_alert(conn, f"Alertes_{site}", nom):
|
||||
@@ -476,7 +534,6 @@ def run_monitor_cycle(site: str = SITE):
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
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)
|
||||
while True:
|
||||
|
||||
@@ -115,6 +115,49 @@ def lire_sondes_depuis_db(site: str):
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
def lire_cfg_chambres(site: str):
|
||||
"""
|
||||
Retourne un dict {sonde: {"temp_max": float, "active": bool, "entretien": bool}}
|
||||
depuis Chambres_froides pour le site.
|
||||
"""
|
||||
sql = """
|
||||
SELECT Sonde, Temp_Max, Etat, En_entretien
|
||||
FROM Chambres_froides
|
||||
WHERE Lieu=%s
|
||||
"""
|
||||
cnx = get_db()
|
||||
cfg: dict[str, dict] = {}
|
||||
try:
|
||||
cur = cnx.cursor()
|
||||
cur.execute(sql, (site,))
|
||||
for sonde, temp_max, etat, en_entretien in cur.fetchall():
|
||||
cfg[str(sonde)] = {
|
||||
"temp_max": float(temp_max),
|
||||
"active": str(etat).upper() == "ON",
|
||||
"entretien": bool(int(en_entretien or 0)),
|
||||
}
|
||||
return cfg
|
||||
except MySQLError as err:
|
||||
log.exception("Erreur DB (lire_cfg_chambres): %s", err)
|
||||
return cfg
|
||||
finally:
|
||||
cnx.close()
|
||||
|
||||
def compute_site_alarm(last_values: list[dict], cfg: dict[str, dict], hysteresis: float = 0.0):
|
||||
"""
|
||||
Retourne (is_on: bool, trigger: tuple[str,float,float] | None)
|
||||
trigger = (sonde, temp, seuil) si dépassement détecté.
|
||||
"""
|
||||
for row in last_values:
|
||||
sonde = str(row["Sonde"])
|
||||
meta = cfg.get(sonde)
|
||||
if not meta or not meta["active"] or meta["entretien"]:
|
||||
continue
|
||||
temp = float(row["Temperature"])
|
||||
if temp > float(meta["temp_max"]) + 0.0:
|
||||
return True, (sonde, temp, float(meta["temp_max"]))
|
||||
return False, None
|
||||
|
||||
def lire_seuils_depuis_db(site: str):
|
||||
sql = """
|
||||
SELECT Sonde, Temp_Max
|
||||
@@ -360,8 +403,12 @@ class MQTTPublisher:
|
||||
def __init__(self, site: str):
|
||||
self.enabled = (_mqtt_ok and (os.getenv("GYRO_MODE", "").lower() == "mqtt"))
|
||||
self.site = site
|
||||
self.topic = (os.getenv(f"GYRO_MQTT_TOPIC_{site}") or
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site.capitalize()}"))
|
||||
self.topic = (
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site}") or
|
||||
os.getenv(f"GYRO_MQTT_TOPIC_{site.upper()}") or
|
||||
os.getenv("GYRO_MQTT_TOPIC") or
|
||||
f"Sondes/{site}/Gyro/cmd"
|
||||
)
|
||||
self.last_state: bool | None = None
|
||||
|
||||
if not self.enabled:
|
||||
@@ -417,9 +464,9 @@ class MQTTPublisher:
|
||||
if self.last_state is not None and self.last_state == on:
|
||||
return
|
||||
|
||||
payload = "on" if on else "off"
|
||||
payload = "ON" if on else "OFF"
|
||||
try:
|
||||
r = self.client.publish(self.topic, payload=payload, qos=1, retain=True)
|
||||
r = self.client.publish(self.topic, payload=payload, qos=2, retain=True)
|
||||
r.wait_for_publish(timeout=3)
|
||||
if r.rc != 0:
|
||||
log.warning("MQTT publish rc=%s (topic=%s)", r.rc, self.topic)
|
||||
@@ -438,22 +485,34 @@ def notifier_sur_depassement(site: str, sonde: str, temp: float, seuil: float):
|
||||
subject, sms_text, email_body = build_alert_text(site, sonde, temp, seuil)
|
||||
notifier.send_sms(sms_text)
|
||||
notifier.send_email(subject, email_body)
|
||||
try: beacon.set(True)
|
||||
except Exception: pass
|
||||
|
||||
def notifier_acquittement(site: str, sonde: str, temp: float, seuil: float):
|
||||
subject, sms_text, _ = build_ok_text(site, sonde, temp, seuil)
|
||||
notifier.send_sms(sms_text)
|
||||
try:
|
||||
if not any_alert_open(site):
|
||||
beacon.set(False)
|
||||
except Exception: pass
|
||||
|
||||
# ========= Cycle & boucle =========
|
||||
def run_monitor_cycle(site: str = SITE):
|
||||
sondes = lire_sondes_depuis_db(site)
|
||||
seuils = lire_seuils_depuis_db(site)
|
||||
for r in sondes:
|
||||
# 1) Lecture dernières mesures + config chambres
|
||||
last_rows = lire_sondes_depuis_db(site) # [{'Sonde','Temperature','Date'}]
|
||||
cfg = lire_cfg_chambres(site) # {sonde: {temp_max, active, entretien}}
|
||||
|
||||
# 2) Gyro instantané : ON si >=1 sonde active & non en entretien dépasse son seuil
|
||||
try:
|
||||
gyro_on, trigger = compute_site_alarm(last_rows, cfg, hysteresis=float(os.getenv("GYRO_HYSTERESIS", "0.0")))
|
||||
if trigger:
|
||||
s, t, se = trigger
|
||||
log.info("Gyro %s => ON (déclenché par %s: %.2f > %.2f)", site, s, t, se)
|
||||
else:
|
||||
log.info("Gyro %s => OFF (aucun dépassement)", site)
|
||||
beacon.set(gyro_on)
|
||||
except Exception as e:
|
||||
log.exception("Erreur calcul/publish gyrophare: %s", e)
|
||||
|
||||
# 3) Alertes "officielles" (inchangées) avec temporisation 30 min
|
||||
# On reconstitue un dict seuils pour réutiliser ta logique existante en dessous.
|
||||
seuils = {s: meta["temp_max"] for s, meta in cfg.items() if meta.get("active", False)}
|
||||
|
||||
for r in last_rows:
|
||||
nom = str(r["Sonde"])
|
||||
temp = float(r["Temperature"])
|
||||
seuil = float(seuils.get(nom, 6.0))
|
||||
@@ -478,6 +537,7 @@ def run_monitor_cycle(site: str = SITE):
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
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)
|
||||
while True:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import os, logging
|
||||
|
||||
def setup_logger(filename: str):
|
||||
# si l'argument est un chemin absolu, on le respecte
|
||||
if os.path.isabs(filename):
|
||||
log_path = filename
|
||||
else:
|
||||
# ancien comportement (ex: /var/log/Cuisine_Saclay/<filename>)
|
||||
base = "/var/log/Cuisine_Saclay"
|
||||
os.makedirs(base, exist_ok=True)
|
||||
log_path = os.path.join(base, filename)
|
||||
|
||||
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
filename=log_path,
|
||||
format="%(asctime)s %(levelname)s %(message)s"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
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}")
|
||||
|
||||
@@ -27,10 +27,10 @@ logging.basicConfig(
|
||||
load_dotenv('/home/debian/Gestion_sondes/.env')
|
||||
|
||||
# OVH SMS
|
||||
APP_KEY = os.getenv('OVH_APP_KEY')
|
||||
APP_SECRET = os.getenv('OVH_APP_SECRET')
|
||||
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_SERVICE_NAME')
|
||||
SERVICE_NAME = os.getenv('OVH_SMS_SERVICE')
|
||||
SMS_RECEIVER = os.getenv('SMS_RECEIVER')
|
||||
SMS_SENDER = os.getenv('OVH_SMS_SENDER')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
52
app/utils_sms.py
Normal file
52
app/utils_sms.py
Normal file
@@ -0,0 +1,52 @@
|
||||
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