""" 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