#!/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()