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