FlotillasGPS - Sistema completo de monitoreo de flotillas GPS

Sistema completo para monitoreo y gestion de flotas de vehiculos con:
- Backend FastAPI con PostgreSQL/TimescaleDB
- Frontend React con TypeScript y TailwindCSS
- App movil React Native con Expo
- Soporte para dispositivos GPS, Meshtastic y celulares
- Video streaming en vivo con MediaMTX
- Geocercas, alertas, viajes y reportes
- Autenticacion JWT y WebSockets en tiempo real

Documentacion completa y guias de usuario incluidas.
This commit is contained in:
FlotillasGPS Developer
2026-01-21 08:18:00 +00:00
commit 51d78bacf4
248 changed files with 50171 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
"""
Módulo WebSocket - Endpoints para comunicación en tiempo real.
"""
from app.api.websocket.manager import manager, ConnectionManager
from app.api.websocket.ubicaciones import router as ubicaciones_router
from app.api.websocket.alertas import router as alertas_router
__all__ = [
"manager",
"ConnectionManager",
"ubicaciones_router",
"alertas_router",
]

View File

@@ -0,0 +1,125 @@
"""
WebSocket endpoint para alertas en tiempo real.
"""
import json
from typing import Optional
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
from app.api.websocket.manager import manager
from app.core.security import decode_token
router = APIRouter()
async def get_user_from_token(token: Optional[str]) -> Optional[int]:
"""
Obtiene el ID de usuario desde un token JWT.
Args:
token: Token JWT.
Returns:
ID del usuario o None.
"""
if not token:
return None
try:
payload = decode_token(token)
return int(payload.get("sub"))
except Exception:
return None
@router.websocket("/ws/alertas")
async def websocket_alertas(
websocket: WebSocket,
token: Optional[str] = Query(None),
):
"""
WebSocket para recibir alertas en tiempo real.
Recibe todas las alertas generadas en el sistema.
Args:
websocket: Conexión WebSocket.
token: Token JWT para autenticación (opcional).
"""
user_id = await get_user_from_token(token)
await manager.connect(websocket, "alertas", user_id)
# Enviar confirmación
await websocket.send_json({
"type": "connected",
"channel": "alerts",
})
try:
while True:
data = await websocket.receive_text()
try:
message = json.loads(data)
if message.get("action") == "ping":
await websocket.send_json({"type": "pong"})
elif message.get("action") == "acknowledge":
# Cliente confirma recepción de alerta
alert_id = message.get("alert_id")
await websocket.send_json({
"type": "acknowledged",
"alert_id": alert_id,
})
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
await manager.disconnect(websocket, "alertas", user_id)
@router.websocket("/ws/alertas/vehiculo/{vehiculo_id}")
async def websocket_alertas_vehiculo(
websocket: WebSocket,
vehiculo_id: int,
token: Optional[str] = Query(None),
):
"""
WebSocket para alertas de un vehículo específico.
Args:
websocket: Conexión WebSocket.
vehiculo_id: ID del vehículo.
token: Token JWT para autenticación (opcional).
"""
user_id = await get_user_from_token(token)
await manager.connect(websocket, "alertas", user_id)
await manager.subscribe_vehicle(websocket, vehiculo_id)
# Enviar confirmación
await websocket.send_json({
"type": "connected",
"channel": "vehicle_alerts",
"vehicle_id": vehiculo_id,
})
try:
while True:
data = await websocket.receive_text()
try:
message = json.loads(data)
if message.get("action") == "ping":
await websocket.send_json({"type": "pong"})
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
await manager.unsubscribe_vehicle(websocket, vehiculo_id)
await manager.disconnect(websocket, "alertas", user_id)

View File

@@ -0,0 +1,266 @@
"""
Gestor de conexiones WebSocket.
Maneja las conexiones de clientes WebSocket para
actualizaciones en tiempo real.
"""
import asyncio
import json
from datetime import datetime, timezone
from typing import Dict, List, Optional, Set
from fastapi import WebSocket
class ConnectionManager:
"""
Gestor de conexiones WebSocket.
Mantiene un registro de conexiones activas y permite
enviar mensajes a clientes específicos o a todos.
"""
def __init__(self):
"""Inicializa el gestor de conexiones."""
# Conexiones activas por tipo de suscripción
self._connections: Dict[str, Set[WebSocket]] = {
"ubicaciones": set(),
"alertas": set(),
"vehiculos": set(),
}
# Conexiones por usuario
self._user_connections: Dict[int, Set[WebSocket]] = {}
# Suscripciones a vehículos específicos
self._vehicle_subscriptions: Dict[int, Set[WebSocket]] = {}
# Lock para operaciones thread-safe
self._lock = asyncio.Lock()
async def connect(
self,
websocket: WebSocket,
channel: str = "ubicaciones",
user_id: Optional[int] = None,
) -> None:
"""
Acepta una nueva conexión WebSocket.
Args:
websocket: Conexión WebSocket.
channel: Canal de suscripción.
user_id: ID del usuario (opcional).
"""
await websocket.accept()
async with self._lock:
# Agregar a conexiones del canal
if channel in self._connections:
self._connections[channel].add(websocket)
# Agregar a conexiones del usuario
if user_id:
if user_id not in self._user_connections:
self._user_connections[user_id] = set()
self._user_connections[user_id].add(websocket)
async def disconnect(
self,
websocket: WebSocket,
channel: str = "ubicaciones",
user_id: Optional[int] = None,
) -> None:
"""
Desconecta un WebSocket.
Args:
websocket: Conexión WebSocket.
channel: Canal de suscripción.
user_id: ID del usuario (opcional).
"""
async with self._lock:
# Remover de conexiones del canal
if channel in self._connections:
self._connections[channel].discard(websocket)
# Remover de conexiones del usuario
if user_id and user_id in self._user_connections:
self._user_connections[user_id].discard(websocket)
if not self._user_connections[user_id]:
del self._user_connections[user_id]
# Remover de suscripciones de vehículos
for vehicle_id in list(self._vehicle_subscriptions.keys()):
self._vehicle_subscriptions[vehicle_id].discard(websocket)
if not self._vehicle_subscriptions[vehicle_id]:
del self._vehicle_subscriptions[vehicle_id]
async def subscribe_vehicle(
self,
websocket: WebSocket,
vehicle_id: int,
) -> None:
"""
Suscribe un WebSocket a actualizaciones de un vehículo específico.
Args:
websocket: Conexión WebSocket.
vehicle_id: ID del vehículo.
"""
async with self._lock:
if vehicle_id not in self._vehicle_subscriptions:
self._vehicle_subscriptions[vehicle_id] = set()
self._vehicle_subscriptions[vehicle_id].add(websocket)
async def unsubscribe_vehicle(
self,
websocket: WebSocket,
vehicle_id: int,
) -> None:
"""
Desuscribe un WebSocket de un vehículo.
Args:
websocket: Conexión WebSocket.
vehicle_id: ID del vehículo.
"""
async with self._lock:
if vehicle_id in self._vehicle_subscriptions:
self._vehicle_subscriptions[vehicle_id].discard(websocket)
async def broadcast(
self,
message: dict,
channel: str = "ubicaciones",
) -> None:
"""
Envía un mensaje a todos los clientes de un canal.
Args:
message: Mensaje a enviar.
channel: Canal de destino.
"""
if channel not in self._connections:
return
message_json = json.dumps(message, default=str)
disconnected = []
for websocket in self._connections[channel]:
try:
await websocket.send_text(message_json)
except Exception:
disconnected.append(websocket)
# Limpiar conexiones desconectadas
for ws in disconnected:
await self.disconnect(ws, channel)
async def broadcast_vehicle_update(
self,
vehicle_id: int,
data: dict,
) -> None:
"""
Envía actualización a suscriptores de un vehículo específico.
Args:
vehicle_id: ID del vehículo.
data: Datos a enviar.
"""
message = {
"type": "vehicle_update",
"vehicle_id": vehicle_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"data": data,
}
message_json = json.dumps(message, default=str)
# Enviar a suscriptores del vehículo
if vehicle_id in self._vehicle_subscriptions:
disconnected = []
for websocket in self._vehicle_subscriptions[vehicle_id]:
try:
await websocket.send_text(message_json)
except Exception:
disconnected.append(websocket)
for ws in disconnected:
await self.unsubscribe_vehicle(ws, vehicle_id)
# También enviar al canal general de ubicaciones
await self.broadcast(message, "ubicaciones")
async def send_to_user(
self,
user_id: int,
message: dict,
) -> None:
"""
Envía un mensaje a todas las conexiones de un usuario.
Args:
user_id: ID del usuario.
message: Mensaje a enviar.
"""
if user_id not in self._user_connections:
return
message_json = json.dumps(message, default=str)
disconnected = []
for websocket in self._user_connections[user_id]:
try:
await websocket.send_text(message_json)
except Exception:
disconnected.append(websocket)
# Limpiar conexiones desconectadas
for ws in disconnected:
await self.disconnect(ws, user_id=user_id)
async def send_alert(
self,
alert_data: dict,
) -> None:
"""
Envía una alerta a todos los clientes suscritos.
Args:
alert_data: Datos de la alerta.
"""
message = {
"type": "alert",
"timestamp": datetime.now(timezone.utc).isoformat(),
"data": alert_data,
}
await self.broadcast(message, "alertas")
def get_connection_count(self) -> dict:
"""
Obtiene el conteo de conexiones activas.
Returns:
Dict con conteo por canal.
"""
return {
channel: len(connections)
for channel, connections in self._connections.items()
}
def get_vehicle_subscribers(self, vehicle_id: int) -> int:
"""
Obtiene el número de suscriptores de un vehículo.
Args:
vehicle_id: ID del vehículo.
Returns:
Número de suscriptores.
"""
if vehicle_id in self._vehicle_subscriptions:
return len(self._vehicle_subscriptions[vehicle_id])
return 0
# Instancia global del gestor de conexiones
manager = ConnectionManager()

View File

@@ -0,0 +1,187 @@
"""
WebSocket endpoint para ubicaciones en tiempo real.
"""
import json
from typing import Optional
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
from jose import JWTError
from app.api.websocket.manager import manager
from app.core.security import decode_token
router = APIRouter()
async def get_user_from_token(token: Optional[str]) -> Optional[int]:
"""
Obtiene el ID de usuario desde un token JWT.
Args:
token: Token JWT.
Returns:
ID del usuario o None.
"""
if not token:
return None
try:
payload = decode_token(token)
return int(payload.get("sub"))
except (JWTError, ValueError):
return None
@router.websocket("/ws/ubicaciones")
async def websocket_ubicaciones(
websocket: WebSocket,
token: Optional[str] = Query(None),
):
"""
WebSocket para recibir actualizaciones de ubicaciones.
Permite suscribirse a:
- Todas las ubicaciones de la flota
- Vehículos específicos
Args:
websocket: Conexión WebSocket.
token: Token JWT para autenticación (opcional).
"""
user_id = await get_user_from_token(token)
await manager.connect(websocket, "ubicaciones", user_id)
try:
while True:
# Recibir mensajes del cliente
data = await websocket.receive_text()
try:
message = json.loads(data)
action = message.get("action")
if action == "subscribe_vehicle":
# Suscribirse a un vehículo específico
vehicle_id = message.get("vehicle_id")
if vehicle_id:
await manager.subscribe_vehicle(websocket, vehicle_id)
await websocket.send_json({
"type": "subscribed",
"vehicle_id": vehicle_id,
})
elif action == "unsubscribe_vehicle":
# Desuscribirse de un vehículo
vehicle_id = message.get("vehicle_id")
if vehicle_id:
await manager.unsubscribe_vehicle(websocket, vehicle_id)
await websocket.send_json({
"type": "unsubscribed",
"vehicle_id": vehicle_id,
})
elif action == "ping":
# Responder ping para keepalive
await websocket.send_json({"type": "pong"})
except json.JSONDecodeError:
await websocket.send_json({
"type": "error",
"message": "Invalid JSON",
})
except WebSocketDisconnect:
await manager.disconnect(websocket, "ubicaciones", user_id)
@router.websocket("/ws/vehiculo/{vehiculo_id}")
async def websocket_vehiculo(
websocket: WebSocket,
vehiculo_id: int,
token: Optional[str] = Query(None),
):
"""
WebSocket para seguir un vehículo específico.
Args:
websocket: Conexión WebSocket.
vehiculo_id: ID del vehículo a seguir.
token: Token JWT para autenticación (opcional).
"""
user_id = await get_user_from_token(token)
await manager.connect(websocket, "ubicaciones", user_id)
await manager.subscribe_vehicle(websocket, vehiculo_id)
# Enviar confirmación de suscripción
await websocket.send_json({
"type": "connected",
"vehicle_id": vehiculo_id,
})
try:
while True:
data = await websocket.receive_text()
try:
message = json.loads(data)
if message.get("action") == "ping":
await websocket.send_json({"type": "pong"})
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
await manager.unsubscribe_vehicle(websocket, vehiculo_id)
await manager.disconnect(websocket, "ubicaciones", user_id)
@router.websocket("/ws/flota")
async def websocket_flota(
websocket: WebSocket,
token: Optional[str] = Query(None),
):
"""
WebSocket para monitoreo de toda la flota.
Recibe actualizaciones de todos los vehículos activos.
Args:
websocket: Conexión WebSocket.
token: Token JWT para autenticación (opcional).
"""
user_id = await get_user_from_token(token)
await manager.connect(websocket, "ubicaciones", user_id)
# Enviar confirmación
await websocket.send_json({
"type": "connected",
"channel": "fleet",
})
try:
while True:
data = await websocket.receive_text()
try:
message = json.loads(data)
if message.get("action") == "ping":
await websocket.send_json({"type": "pong"})
elif message.get("action") == "request_status":
# Enviar estado actual de conexiones
await websocket.send_json({
"type": "status",
"connections": manager.get_connection_count(),
})
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
await manager.disconnect(websocket, "ubicaciones", user_id)