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.
157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
"""
|
|
Modelo de Ubicación para almacenar datos GPS.
|
|
|
|
Utiliza TimescaleDB hypertable para almacenamiento eficiente
|
|
de series temporales de ubicaciones.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
DateTime,
|
|
Float,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
event,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.core.database import Base
|
|
|
|
|
|
class Ubicacion(Base):
|
|
"""
|
|
Modelo de ubicación GPS.
|
|
|
|
Esta tabla está diseñada para ser una hypertable de TimescaleDB,
|
|
optimizada para almacenar millones de registros de ubicación.
|
|
"""
|
|
|
|
__tablename__ = "ubicaciones"
|
|
|
|
# Clave primaria compuesta: tiempo + vehiculo_id
|
|
tiempo: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
primary_key=True,
|
|
nullable=False,
|
|
)
|
|
vehiculo_id: Mapped[int] = mapped_column(
|
|
ForeignKey("vehiculos.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
nullable=False,
|
|
)
|
|
|
|
# Coordenadas
|
|
lat: Mapped[float] = mapped_column(Float, nullable=False)
|
|
lng: Mapped[float] = mapped_column(Float, nullable=False)
|
|
altitud: Mapped[float | None] = mapped_column(Float, nullable=True) # metros sobre nivel del mar
|
|
|
|
# Movimiento
|
|
velocidad: Mapped[float | None] = mapped_column(Float, nullable=True) # km/h
|
|
rumbo: Mapped[float | None] = mapped_column(Float, nullable=True) # grados (0-360)
|
|
|
|
# Precisión
|
|
precision: Mapped[float | None] = mapped_column(Float, nullable=True) # metros
|
|
hdop: Mapped[float | None] = mapped_column(Float, nullable=True) # Horizontal Dilution of Precision
|
|
|
|
# Información GPS
|
|
satelites: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
fuente: Mapped[str] = mapped_column(
|
|
String(20),
|
|
default="gps",
|
|
nullable=False,
|
|
) # gps, network, fused, meshtastic
|
|
|
|
# Estado del dispositivo
|
|
bateria_dispositivo: Mapped[float | None] = mapped_column(Float, nullable=True) # porcentaje
|
|
bateria_vehiculo: Mapped[float | None] = mapped_column(Float, nullable=True) # voltaje
|
|
|
|
# Estado del vehículo
|
|
motor_encendido: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
|
odometro: Mapped[float | None] = mapped_column(Float, nullable=True) # km
|
|
|
|
# Sensores OBD (opcional)
|
|
rpm: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
temperatura_motor: Mapped[float | None] = mapped_column(Float, nullable=True) # Celsius
|
|
nivel_combustible: Mapped[float | None] = mapped_column(Float, nullable=True) # porcentaje
|
|
|
|
# Índices para consultas frecuentes
|
|
__table_args__ = (
|
|
# Índice espacial aproximado para consultas por área
|
|
Index("idx_ubicaciones_coords", "lat", "lng"),
|
|
# Índice para consultas por vehículo en un rango de tiempo
|
|
Index("idx_ubicaciones_vehiculo_tiempo", "vehiculo_id", "tiempo"),
|
|
# Índice para encontrar paradas (velocidad 0)
|
|
Index("idx_ubicaciones_velocidad", "velocidad"),
|
|
# Configuración para TimescaleDB
|
|
{
|
|
"timescaledb_hypertable": {
|
|
"time_column_name": "tiempo",
|
|
"chunk_time_interval": "1 day",
|
|
}
|
|
},
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Ubicacion(vehiculo_id={self.vehiculo_id}, tiempo={self.tiempo}, lat={self.lat}, lng={self.lng})>"
|
|
|
|
def to_geojson(self) -> dict:
|
|
"""Convierte la ubicación a formato GeoJSON Point."""
|
|
return {
|
|
"type": "Feature",
|
|
"geometry": {
|
|
"type": "Point",
|
|
"coordinates": [self.lng, self.lat],
|
|
},
|
|
"properties": {
|
|
"vehiculo_id": self.vehiculo_id,
|
|
"tiempo": self.tiempo.isoformat(),
|
|
"velocidad": self.velocidad,
|
|
"rumbo": self.rumbo,
|
|
"motor_encendido": self.motor_encendido,
|
|
},
|
|
}
|
|
|
|
|
|
# Función para crear la hypertable en TimescaleDB
|
|
# Se ejecuta después de crear la tabla
|
|
def create_hypertable(target, connection, **kw):
|
|
"""Crea la hypertable de TimescaleDB después de crear la tabla."""
|
|
# Esta función se ejecutará solo si TimescaleDB está instalado
|
|
try:
|
|
connection.execute(
|
|
"""
|
|
SELECT create_hypertable(
|
|
'ubicaciones',
|
|
'tiempo',
|
|
if_not_exists => TRUE,
|
|
chunk_time_interval => INTERVAL '1 day'
|
|
);
|
|
"""
|
|
)
|
|
# Habilitar compresión después de 7 días
|
|
connection.execute(
|
|
"""
|
|
ALTER TABLE ubicaciones SET (
|
|
timescaledb.compress,
|
|
timescaledb.compress_segmentby = 'vehiculo_id'
|
|
);
|
|
"""
|
|
)
|
|
# Política de compresión automática
|
|
connection.execute(
|
|
"""
|
|
SELECT add_compression_policy('ubicaciones', INTERVAL '7 days', if_not_exists => TRUE);
|
|
"""
|
|
)
|
|
except Exception:
|
|
# Si TimescaleDB no está instalado, continuar sin hypertable
|
|
pass
|
|
|
|
|
|
# Registrar evento para crear hypertable
|
|
event.listen(Ubicacion.__table__, "after_create", create_hypertable)
|