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.
287 lines
8.8 KiB
Python
287 lines
8.8 KiB
Python
"""
|
|
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
|