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>
This commit is contained in:
169
alert_watchdog.py
Normal file
169
alert_watchdog.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user