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,286 @@
"""
Servicio para integración con Traccar.
Recibe datos de ubicación desde Traccar via forward
y los procesa en el sistema.
"""
from datetime import datetime, timezone
from typing import Optional
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.models.dispositivo import Dispositivo
from app.schemas.ubicacion import TraccarLocationCreate, UbicacionCreate
from app.services.ubicacion_service import UbicacionService
class TraccarService:
"""Servicio para integración con Traccar GPS Server."""
def __init__(self, db: AsyncSession):
"""
Inicializa el servicio.
Args:
db: Sesión de base de datos async.
"""
self.db = db
self.ubicacion_service = UbicacionService(db)
self.api_url = settings.TRACCAR_API_URL
self.username = settings.TRACCAR_USERNAME
self.password = settings.TRACCAR_PASSWORD
async def procesar_posicion_traccar(
self,
posicion: TraccarLocationCreate,
) -> Optional[dict]:
"""
Procesa una posición recibida desde Traccar.
Args:
posicion: Datos de posición de Traccar.
Returns:
Resultado del procesamiento o None.
"""
# Buscar dispositivo por ID de Traccar
result = await self.db.execute(
select(Dispositivo)
.where(Dispositivo.identificador == str(posicion.deviceId))
.where(Dispositivo.protocolo == "traccar")
)
dispositivo = result.scalar_one_or_none()
if not dispositivo:
# Intentar buscar por IMEI en attributes
if posicion.attributes and "imei" in posicion.attributes:
result = await self.db.execute(
select(Dispositivo)
.where(Dispositivo.imei == posicion.attributes["imei"])
)
dispositivo = result.scalar_one_or_none()
if not dispositivo:
return None
# Convertir velocidad de nudos a km/h
velocidad = None
if posicion.speed is not None:
velocidad = posicion.speed * 1.852 # nudos a km/h
# Extraer datos adicionales de attributes
bateria = None
motor_encendido = None
odometro = None
if posicion.attributes:
bateria = posicion.attributes.get("batteryLevel")
motor_encendido = posicion.attributes.get("ignition")
# Odómetro puede venir en metros
odometro_metros = posicion.attributes.get("totalDistance")
if odometro_metros:
odometro = odometro_metros / 1000 # a km
# Crear schema de ubicación
ubicacion_data = UbicacionCreate(
vehiculo_id=dispositivo.vehiculo_id,
dispositivo_id=dispositivo.identificador,
lat=posicion.latitude,
lng=posicion.longitude,
velocidad=velocidad,
rumbo=posicion.course,
altitud=posicion.altitude,
precision=posicion.accuracy,
tiempo=posicion.fixTime,
fuente="traccar",
bateria_dispositivo=bateria,
motor_encendido=motor_encendido,
odometro=odometro,
)
# Procesar ubicación
resultado = await self.ubicacion_service.procesar_ubicacion(ubicacion_data)
if resultado:
return {
"status": "processed",
"vehiculo_id": dispositivo.vehiculo_id,
"dispositivo_id": dispositivo.identificador,
}
return None
async def sincronizar_dispositivos(self) -> dict:
"""
Sincroniza dispositivos desde Traccar.
Obtiene la lista de dispositivos de Traccar y los sincroniza
con la base de datos local.
Returns:
Resultado de la sincronización.
"""
if not self.username or not self.password:
return {"error": "Credenciales de Traccar no configuradas"}
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.api_url}/devices",
auth=(self.username, self.password),
timeout=30.0,
)
response.raise_for_status()
dispositivos_traccar = response.json()
except httpx.HTTPError as e:
return {"error": f"Error conectando a Traccar: {str(e)}"}
sincronizados = 0
for d in dispositivos_traccar:
# Verificar si ya existe
result = await self.db.execute(
select(Dispositivo)
.where(Dispositivo.identificador == str(d["id"]))
.where(Dispositivo.protocolo == "traccar")
)
dispositivo = result.scalar_one_or_none()
if not dispositivo:
# Solo registrar, no crear vehículo automáticamente
continue
# Actualizar información
dispositivo.nombre = d.get("name", dispositivo.nombre)
if d.get("lastUpdate"):
dispositivo.ultimo_contacto = datetime.fromisoformat(
d["lastUpdate"].replace("Z", "+00:00")
)
dispositivo.conectado = d.get("status", "") == "online"
sincronizados += 1
await self.db.commit()
return {
"total_traccar": len(dispositivos_traccar),
"sincronizados": sincronizados,
}
async def obtener_posicion_actual(
self,
dispositivo_id: str,
) -> Optional[dict]:
"""
Obtiene la posición actual de un dispositivo desde Traccar.
Args:
dispositivo_id: ID del dispositivo en Traccar.
Returns:
Posición actual o None.
"""
if not self.username or not self.password:
return None
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.api_url}/positions",
params={"deviceId": dispositivo_id},
auth=(self.username, self.password),
timeout=10.0,
)
response.raise_for_status()
posiciones = response.json()
if posiciones:
return posiciones[0]
except httpx.HTTPError:
pass
return None
async def enviar_comando(
self,
dispositivo_id: str,
tipo_comando: str,
data: dict = None,
) -> Optional[dict]:
"""
Envía un comando a un dispositivo via Traccar.
Args:
dispositivo_id: ID del dispositivo en Traccar.
tipo_comando: Tipo de comando (ej: "engineStop", "engineResume").
data: Datos adicionales del comando.
Returns:
Respuesta de Traccar o None.
"""
if not self.username or not self.password:
return None
comando = {
"deviceId": int(dispositivo_id),
"type": tipo_comando,
"attributes": data or {},
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.api_url}/commands/send",
json=comando,
auth=(self.username, self.password),
timeout=30.0,
)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
return {"error": str(e)}
async def obtener_reportes_traccar(
self,
dispositivo_id: str,
desde: datetime,
hasta: datetime,
tipo: str = "route",
) -> Optional[list]:
"""
Obtiene reportes desde Traccar.
Args:
dispositivo_id: ID del dispositivo.
desde: Fecha inicio.
hasta: Fecha fin.
tipo: Tipo de reporte (route, events, trips, stops).
Returns:
Lista de datos del reporte.
"""
if not self.username or not self.password:
return None
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.api_url}/reports/{tipo}",
params={
"deviceId": dispositivo_id,
"from": desde.isoformat(),
"to": hasta.isoformat(),
},
auth=(self.username, self.password),
timeout=60.0,
)
response.raise_for_status()
return response.json()
except httpx.HTTPError:
return None