Files
Lucas/alert_watchdog.py
Debian 24dbc7cd6a Initial commit — Serveur Lucas SmartEye
API réception alertes chute (SmartEye/YOLO), analyse IA (Gemini 2.5 Flash),
gestion alertes avec escalade (watchdog), notifications Firebase,
dashboard web, documentation MkDocs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:26:06 +01:00

169 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
SmartEye SENTINEL - Watchdog d'Alertes
========================================
Script cron qui tourne toutes les minutes.
Vérifie les alertes non acquittées et les renvoie avec escalade.
Crontab :
* * * * * /usr/bin/python3 /var/www/lucas/alert_watchdog.py >> /var/log/smarteye_watchdog.log 2>&1
Auteur : Unigest Solutions / SmartEye V30
"""
import json
import sys
import os
import uuid
from datetime import datetime, timedelta
# Ajouter le chemin du projet
sys.path.insert(0, '/var/www/lucas')
from alert_manager import (
get_alertes_pending,
enregistrer_envoi,
mettre_a_jour_statut,
nettoyer_alertes_obsoletes,
AlertStatus
)
# Firebase
import firebase_admin
from firebase_admin import credentials, messaging
# --- CONFIGURATION ---
TOKEN_TELEPHONE = "fheHxyPRQlS7T0hDnKIpeR:APA91bFPd7v3zO0GqU7pW3X-IfODgnfIUOFUoM4M1_CsCNNE5BSLVSIrCpXqL_xNNNBWdUFE4A4SCNs9_LWUQn8s7mVaw2gnQxhveC1SOc9E0gRiQW1Ct84"
DOMAIN = "https://lucas.unigest.fr"
# --- INITIALISATION FIREBASE ---
if not firebase_admin._apps:
try:
cred = credentials.Certificate("/var/www/lucas/admin_key.json")
firebase_admin.initialize_app(cred)
except Exception as e:
print(f"[WATCHDOG] Erreur Firebase init: {e}")
sys.exit(1)
def envoyer_rappel(alerte_info):
"""
Envoie une notification de rappel avec le niveau d'escalade approprié.
Le message change selon l'urgence croissante.
"""
alert_id = alerte_info["alert_id"]
alerte = alerte_info["alerte"]
level = alerte_info["escalation_level"]
priority = alerte_info["priority"]
count = alerte_info["notification_count"]
age = alerte_info["age_seconds"]
image_path = alerte.get("image_path", "")
url_image = f"{DOMAIN}/{image_path}" if image_path else ""
# Messages d'escalade progressifs
messages_escalade = {
0: {"title": "⚠️ RAPPEL ALERTE CHUTE",
"body": f"Alerte non vue depuis {age // 60} min — Appuyez pour voir"},
1: {"title": "🔴 ALERTE CHUTE NON TRAITÉE",
"body": f"URGENT : Personne potentiellement au sol depuis {age // 60} min !"},
2: {"title": "🚨 ALERTE CRITIQUE NON TRAITÉE",
"body": f"Aucune réponse depuis {age // 60} min — Intervention nécessaire !"},
3: {"title": "🆘 ALERTE MAXIMALE",
"body": f"SANS RÉPONSE DEPUIS {age // 60} MIN — Vérifiez immédiatement !"},
4: {"title": "🆘 DERNIÈRE TENTATIVE",
"body": f"Alerte ignorée depuis {age // 60} min — Escalade en cours"},
}
msg_data = messages_escalade.get(level, messages_escalade[4])
now = datetime.now()
try:
message = messaging.Message(
notification=messaging.Notification(
title=msg_data["title"],
body=msg_data["body"],
),
android=messaging.AndroidConfig(
priority='high',
notification=messaging.AndroidNotification(
channel_id='fall_alert_channel',
priority='max' if level >= 2 else 'high',
default_sound=True,
default_vibrate_timings=True,
visibility='public',
),
# TTL court pour forcer la livraison rapide
ttl=timedelta(seconds=60),
),
data={
"urgence": "true",
"type": "chute_rappel",
"alert_id": alert_id,
"escalation_level": str(level),
"escalation_label": alerte_info["escalation_label"],
"notification_count": str(count + 1),
"original_time": alerte.get("created_at", ""),
"age_minutes": str(age // 60),
"senior_name": "Mamie Lucas",
"senior_photo_url": f"{DOMAIN}/photos/mamie.jpg",
"image_url": url_image,
"camera_id": alerte.get("camera_id", ""),
"audio_relay_ws": alerte.get("audio_relay_ws", "ws://57.128.74.87:8800"),
"camera_ip_map": alerte.get("camera_ip_map", "{}"),
"is_reminder": "true",
"click_action": "FLUTTER_NOTIFICATION_CLICK",
},
token=TOKEN_TELEPHONE,
)
response = messaging.send(message)
# Enregistrer l'envoi
enregistrer_envoi(alert_id, level)
print(f"[WATCHDOG] ✅ Rappel envoyé — Alerte {alert_id[:8]}... "
f"| Niveau {level} | Envoi #{count + 1} | Âge {age // 60}min "
f"| Firebase: {response}")
return True
except messaging.UnregisteredError:
# Token invalide — le téléphone a été réinitialisé ou l'app désinstallée
print(f"[WATCHDOG] ❌ Token Firebase invalide ! L'app est peut-être désinstallée.")
# TODO: Escalader vers SMS ou contact secondaire
return False
except Exception as e:
print(f"[WATCHDOG] ❌ Erreur envoi rappel {alert_id[:8]}...: {e}")
return False
def run_watchdog():
"""Boucle principale du watchdog"""
now = datetime.now()
# Nettoyage des alertes obsolètes (SEEN > 4h, ACKNOWLEDGED > 2h)
cleaned = nettoyer_alertes_obsoletes()
if cleaned > 0:
print(f"[WATCHDOG] 🧹 {cleaned} alerte(s) obsolète(s) nettoyée(s)")
# Récupérer les alertes qui nécessitent un renvoi
alertes_a_renvoyer = get_alertes_pending()
if not alertes_a_renvoyer:
# Silencieux si rien à faire (le cron tourne chaque minute)
return
print(f"\n[WATCHDOG] === {now.strftime('%Y-%m-%d %H:%M:%S')} === "
f"{len(alertes_a_renvoyer)} alerte(s) à renvoyer")
for alerte_info in alertes_a_renvoyer:
envoyer_rappel(alerte_info)
print(f"[WATCHDOG] --- Fin du cycle ---\n")
if __name__ == "__main__":
run_watchdog()