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:
286
backend/app/services/traccar_service.py
Normal file
286
backend/app/services/traccar_service.py
Normal 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
|
||||
Reference in New Issue
Block a user