From 496c2bb9bc7d30d38a086634c13720a4fc7d6950 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Sun, 2 Aug 2026 11:02:05 +0200 Subject: [PATCH] 02-ago: [proyecto] Sistema vigilancia Tapo con deteccion movimiento/persona + notificacion Telegram/Alexa --- .gitignore | 3 ++ vigilancia/.env.example | 22 ++++++++ vigilancia/camara.py | 35 +++++++++++++ vigilancia/config.py | 30 +++++++++++ vigilancia/detector_movimiento.py | 11 ++++ vigilancia/detector_persona.py | 15 ++++++ vigilancia/main.py | 87 +++++++++++++++++++++++++++++++ vigilancia/notificador.py | 33 ++++++++++++ vigilancia/requirements.txt | 4 ++ 9 files changed, 240 insertions(+) create mode 100644 vigilancia/.env.example create mode 100644 vigilancia/camara.py create mode 100644 vigilancia/config.py create mode 100644 vigilancia/detector_movimiento.py create mode 100644 vigilancia/detector_persona.py create mode 100644 vigilancia/main.py create mode 100644 vigilancia/notificador.py create mode 100644 vigilancia/requirements.txt diff --git a/.gitignore b/.gitignore index 9b1bec9..554fa1e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ Thumbs.db *.log .watcher_last_id telegram_mensajes.log + +# Vigilancia +vigilancia/snapshots/ diff --git a/vigilancia/.env.example b/vigilancia/.env.example new file mode 100644 index 0000000..61460ec --- /dev/null +++ b/vigilancia/.env.example @@ -0,0 +1,22 @@ +# Cámara +CAM_1_NOMBRE=Salon +CAM_1_RTSP=rtsp://usuario:password@192.168.1.50:554/stream2 +CAM_2_NOMBRE=Entrada +CAM_2_RTSP=rtsp://usuario:password@192.168.1.51:554/stream2 + +# Telegram +TELEGRAM_TOKEN=123456789:AAExampleTokenAqui +TELEGRAM_CHAT_ID=987654321 + +# Franja horaria activa (formato 24h) +HORA_INICIO=08 +HORA_FIN=17 + +# Cooldown entre alertas (segundos) +COOLDOWN=45 + +# Webhook Alexa/Node-RED (opcional, dejar vacío si no se usa) +WEBHOOK_ALEXA= + +# Directorio de snapshots +SNAPSHOT_DIR=snapshots diff --git a/vigilancia/camara.py b/vigilancia/camara.py new file mode 100644 index 0000000..996b655 --- /dev/null +++ b/vigilancia/camara.py @@ -0,0 +1,35 @@ +import cv2 +import time + + +class CamaraRTSP: + def __init__(self, url, nombre): + self.url = url + self.nombre = nombre + self.cap = None + self._conectar() + + def _conectar(self): + self.cap = cv2.VideoCapture(self.url) + self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) + self.cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 5000) + + def leer_frame(self): + if self.cap is None or not self.cap.isOpened(): + print(f"[{self.nombre}] Reconectando...") + try: + self._conectar() + except Exception as e: + print(f"[{self.nombre}] Error al reconectar: {e}") + self.cap = None + time.sleep(2) + return None + + ok, frame = self.cap.read() + if not ok: + print(f"[{self.nombre}] Frame perdido, reintentando...") + self.cap.release() + self.cap = None + return None + + return frame diff --git a/vigilancia/config.py b/vigilancia/config.py new file mode 100644 index 0000000..d629eb6 --- /dev/null +++ b/vigilancia/config.py @@ -0,0 +1,30 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +load_dotenv(Path(__file__).resolve().parent / ".env") + + +def _cams(): + cams = [] + i = 1 + while True: + nombre = os.getenv(f"CAM_{i}_NOMBRE") + rtsp = os.getenv(f"CAM_{i}_RTSP") + if not nombre or not rtsp: + break + cams.append({"nombre": nombre, "rtsp": rtsp}) + i += 1 + return cams + + +CAMARAS = _cams() + +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID") +HORA_INICIO = int(os.getenv("HORA_INICIO", 0)) +HORA_FIN = int(os.getenv("HORA_FIN", 23)) +COOLDOWN = int(os.getenv("COOLDOWN", 30)) +WEBHOOK_ALEXA = os.getenv("WEBHOOK_ALEXA", "").strip() +SNAPSHOT_DIR = os.getenv("SNAPSHOT_DIR", "snapshots") diff --git a/vigilancia/detector_movimiento.py b/vigilancia/detector_movimiento.py new file mode 100644 index 0000000..235af5a --- /dev/null +++ b/vigilancia/detector_movimiento.py @@ -0,0 +1,11 @@ +import cv2 + + +class DetectorMovimiento: + def __init__(self, umbral_pixeles=5000): + self.bg = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=50) + self.umbral = umbral_pixeles + + def hay_movimiento(self, frame) -> bool: + mask = self.bg.apply(frame) + return cv2.countNonZero(mask) > self.umbral diff --git a/vigilancia/detector_persona.py b/vigilancia/detector_persona.py new file mode 100644 index 0000000..14799f7 --- /dev/null +++ b/vigilancia/detector_persona.py @@ -0,0 +1,15 @@ +from ultralytics import YOLO + + +class DetectorPersona: + def __init__(self, modelo="yolov8n.pt", confianza=0.5): + self.model = YOLO(modelo) + self.confianza = confianza + + def hay_persona(self, frame) -> bool: + resultados = self.model.predict(frame, verbose=False, conf=self.confianza) + for r in resultados: + for c in r.boxes.cls: + if int(c) == 0: + return True + return False diff --git a/vigilancia/main.py b/vigilancia/main.py new file mode 100644 index 0000000..ca15932 --- /dev/null +++ b/vigilancia/main.py @@ -0,0 +1,87 @@ +import os +import time +from datetime import datetime + +import cv2 + +import config +from camara import CamaraRTSP +from detector_movimiento import DetectorMovimiento +from notificador import disparar_webhook_alexa, enviar_telegram + +USAR_YOLO = False + +if USAR_YOLO: + from detector_persona import DetectorPersona + + +def dentro_de_franja_horaria() -> bool: + hora_actual = datetime.now().hour + inicio = config.HORA_INICIO + fin = config.HORA_FIN + if inicio < fin: + return inicio <= hora_actual < fin + return hora_actual >= inicio or hora_actual < fin + + +def main(): + if not config.CAMARAS: + print("No hay cámaras configuradas en .env") + return + + os.makedirs(config.SNAPSHOT_DIR, exist_ok=True) + + camaras = [CamaraRTSP(c["rtsp"], c["nombre"]) for c in config.CAMARAS] + detectores = [] + ultima_alerta = {} + + for i, c in enumerate(config.CAMARAS): + detectores.append(DetectorPersona() if USAR_YOLO else DetectorMovimiento()) + ultima_alerta[i] = 0 + + print("Sistema de vigilancia iniciado. Franja activa: " + f"{config.HORA_INICIO}:00 - {config.HORA_FIN}:00") + + while True: + if not dentro_de_franja_horaria(): + time.sleep(30) + continue + + for i, cam in enumerate(camaras): + frame = cam.leer_frame() + if frame is None: + continue + + detector = detectores[i] + detectado = ( + detector.hay_persona(frame) if USAR_YOLO + else detector.hay_movimiento(frame) + ) + + ahora = time.time() + if detectado and (ahora - ultima_alerta[i] > config.COOLDOWN): + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + ruta = os.path.join( + config.SNAPSHOT_DIR, f"snapshot_{cam.nombre}_{ts}.jpg" + ) + cv2_ok = True + try: + cv2.imwrite(ruta, frame) + except Exception as e: + cv2_ok = False + print(f"[{cam.nombre}] Error guardando snapshot: {e}") + + tipo = "persona" if USAR_YOLO else "movimiento" + texto = f"⚠️ Detección de {tipo} en {cam.nombre} - {ts}" + + if cv2_ok: + enviar_telegram(ruta, texto) + disparar_webhook_alexa(texto) + ultima_alerta[i] = ahora + print(texto) + + time.sleep(0.5) + + +if __name__ == "__main__": + main() diff --git a/vigilancia/notificador.py b/vigilancia/notificador.py new file mode 100644 index 0000000..8de5d04 --- /dev/null +++ b/vigilancia/notificador.py @@ -0,0 +1,33 @@ +import requests + +import config + + +def enviar_telegram(ruta_imagen: str, texto: str): + if not config.TELEGRAM_TOKEN or not config.TELEGRAM_CHAT_ID: + print("[Telegram] Falta TELEGRAM_TOKEN o TELEGRAM_CHAT_ID") + return + + url = f"https://api.telegram.org/bot{config.TELEGRAM_TOKEN}/sendPhoto" + try: + with open(ruta_imagen, "rb") as f: + r = requests.post( + url, + data={"chat_id": config.TELEGRAM_CHAT_ID, "caption": texto}, + files={"photo": f}, + timeout=10, + ) + if not r.ok: + print(f"[Telegram] Error {r.status_code}: {r.text}") + except Exception as e: + print(f"[Telegram] Excepción al enviar: {e}") + + +def disparar_webhook_alexa(texto: str): + if not config.WEBHOOK_ALEXA: + return + + try: + requests.post(config.WEBHOOK_ALEXA, json={"mensaje": texto}, timeout=5) + except Exception as e: + print(f"[Webhook Alexa] Excepción: {e}") diff --git a/vigilancia/requirements.txt b/vigilancia/requirements.txt new file mode 100644 index 0000000..f19b758 --- /dev/null +++ b/vigilancia/requirements.txt @@ -0,0 +1,4 @@ +opencv-python +requests +python-dotenv +ultralytics