02-ago: [proyecto] Sistema vigilancia Tapo con deteccion movimiento/persona + notificacion Telegram/Alexa
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,3 +19,6 @@ Thumbs.db
|
|||||||
*.log
|
*.log
|
||||||
.watcher_last_id
|
.watcher_last_id
|
||||||
telegram_mensajes.log
|
telegram_mensajes.log
|
||||||
|
|
||||||
|
# Vigilancia
|
||||||
|
vigilancia/snapshots/
|
||||||
|
|||||||
22
vigilancia/.env.example
Normal file
22
vigilancia/.env.example
Normal 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
35
vigilancia/camara.py
Normal 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
30
vigilancia/config.py
Normal 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")
|
||||||
11
vigilancia/detector_movimiento.py
Normal file
11
vigilancia/detector_movimiento.py
Normal 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
|
||||||
15
vigilancia/detector_persona.py
Normal file
15
vigilancia/detector_persona.py
Normal 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
87
vigilancia/main.py
Normal 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
33
vigilancia/notificador.py
Normal 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}")
|
||||||
4
vigilancia/requirements.txt
Normal file
4
vigilancia/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
opencv-python
|
||||||
|
requests
|
||||||
|
python-dotenv
|
||||||
|
ultralytics
|
||||||
Reference in New Issue
Block a user