02-ago: [proyecto] Sistema vigilancia Tapo con deteccion movimiento/persona + notificacion Telegram/Alexa

This commit is contained in:
minguezsanzjuanjose
2026-08-02 11:02:05 +02:00
parent 3a38e889db
commit 496c2bb9bc
9 changed files with 240 additions and 0 deletions

22
vigilancia/.env.example Normal file
View File

@@ -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

35
vigilancia/camara.py Normal file
View File

@@ -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

30
vigilancia/config.py Normal file
View File

@@ -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")

View File

@@ -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

View File

@@ -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

87
vigilancia/main.py Normal file
View File

@@ -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()

33
vigilancia/notificador.py Normal file
View File

@@ -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}")

View File

@@ -0,0 +1,4 @@
opencv-python
requests
python-dotenv
ultralytics