from datetime import datetime, timezone from sqlalchemy import ( BigInteger, Boolean, DateTime, Float, ForeignKey, Index, Integer, String, Text, ) from sqlalchemy.dialects.sqlite import JSON from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base def _utcnow() -> datetime: return datetime.now(timezone.utc) class Device(Base): """Registered Bluetooth badge devices.""" __tablename__ = "devices" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) imei: Mapped[str] = mapped_column(String(20), unique=True, index=True, nullable=False) device_type: Mapped[str] = mapped_column(String(10), nullable=False) name: Mapped[str | None] = mapped_column(String(100), nullable=True) status: Mapped[str] = mapped_column(String(20), default="offline", nullable=False) battery_level: Mapped[int | None] = mapped_column(Integer, nullable=True) gsm_signal: Mapped[int | None] = mapped_column(Integer, nullable=True) last_heartbeat: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) last_login: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) iccid: Mapped[str | None] = mapped_column(String(30), nullable=True) imsi: Mapped[str | None] = mapped_column(String(20), nullable=True) timezone: Mapped[str] = mapped_column(String(30), default="+8", nullable=False) language: Mapped[str] = mapped_column(String(10), default="cn", nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) updated_at: Mapped[datetime | None] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow, nullable=True ) # Relationships locations: Mapped[list["LocationRecord"]] = relationship( back_populates="device", cascade="all, delete-orphan", lazy="noload" ) alarms: Mapped[list["AlarmRecord"]] = relationship( back_populates="device", cascade="all, delete-orphan", lazy="noload" ) heartbeats: Mapped[list["HeartbeatRecord"]] = relationship( back_populates="device", cascade="all, delete-orphan", lazy="noload" ) attendance_records: Mapped[list["AttendanceRecord"]] = relationship( back_populates="device", cascade="all, delete-orphan", lazy="noload" ) bluetooth_records: Mapped[list["BluetoothRecord"]] = relationship( back_populates="device", cascade="all, delete-orphan", lazy="noload" ) command_logs: Mapped[list["CommandLog"]] = relationship( back_populates="device", cascade="all, delete-orphan", lazy="noload" ) def __repr__(self) -> str: return f"" class LocationRecord(Base): """GPS / LBS / WIFI location records.""" __tablename__ = "location_records" __table_args__ = ( Index("ix_location_device_time", "device_id", "recorded_at"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) device_id: Mapped[int] = mapped_column( Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False ) location_type: Mapped[str] = mapped_column( String(10), nullable=False ) # gps, lbs, wifi, gps_4g, lbs_4g, wifi_4g latitude: Mapped[float | None] = mapped_column(Float, nullable=True) longitude: Mapped[float | None] = mapped_column(Float, nullable=True) speed: Mapped[float | None] = mapped_column(Float, nullable=True) course: Mapped[float | None] = mapped_column(Float, nullable=True) gps_satellites: Mapped[int | None] = mapped_column(Integer, nullable=True) gps_positioned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) mcc: Mapped[int | None] = mapped_column(Integer, nullable=True) mnc: Mapped[int | None] = mapped_column(Integer, nullable=True) lac: Mapped[int | None] = mapped_column(BigInteger, nullable=True) cell_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True) rssi: Mapped[int | None] = mapped_column(Integer, nullable=True) neighbor_cells: Mapped[dict | None] = mapped_column(JSON, nullable=True) wifi_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) report_mode: Mapped[int | None] = mapped_column(Integer, nullable=True) is_realtime: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) mileage: Mapped[float | None] = mapped_column(Float, nullable=True) address: Mapped[str | None] = mapped_column(Text, nullable=True) raw_data: Mapped[str | None] = mapped_column(Text, nullable=True) recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) device: Mapped["Device"] = relationship(back_populates="locations") def __repr__(self) -> str: return ( f"" ) class AlarmRecord(Base): """Alarm events raised by devices.""" __tablename__ = "alarm_records" __table_args__ = ( Index("ix_alarm_device_time", "device_id", "recorded_at"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) device_id: Mapped[int] = mapped_column( Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False ) alarm_type: Mapped[str] = mapped_column( String(30), nullable=False ) # sos, low_battery, power_on, power_off, enter_fence, exit_fence, ... alarm_source: Mapped[str | None] = mapped_column( String(20), nullable=True ) # single_fence, multi_fence, lbs, wifi protocol_number: Mapped[int] = mapped_column(Integer, nullable=False) latitude: Mapped[float | None] = mapped_column(Float, nullable=True) longitude: Mapped[float | None] = mapped_column(Float, nullable=True) speed: Mapped[float | None] = mapped_column(Float, nullable=True) course: Mapped[float | None] = mapped_column(Float, nullable=True) mcc: Mapped[int | None] = mapped_column(Integer, nullable=True) mnc: Mapped[int | None] = mapped_column(Integer, nullable=True) lac: Mapped[int | None] = mapped_column(BigInteger, nullable=True) cell_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True) battery_level: Mapped[int | None] = mapped_column(Integer, nullable=True) gsm_signal: Mapped[int | None] = mapped_column(Integer, nullable=True) fence_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) wifi_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) address: Mapped[str | None] = mapped_column(Text, nullable=True) acknowledged: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) device: Mapped["Device"] = relationship(back_populates="alarms") def __repr__(self) -> str: return ( f"" ) class HeartbeatRecord(Base): """Heartbeat history from devices.""" __tablename__ = "heartbeat_records" __table_args__ = ( Index("ix_heartbeat_device_time", "device_id", "created_at"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) device_id: Mapped[int] = mapped_column( Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False ) protocol_number: Mapped[int] = mapped_column(Integer, nullable=False) # 0x13 or 0x36 terminal_info: Mapped[int] = mapped_column(Integer, nullable=False) battery_level: Mapped[int] = mapped_column(Integer, nullable=False) gsm_signal: Mapped[int] = mapped_column(Integer, nullable=False) extension_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) device: Mapped["Device"] = relationship(back_populates="heartbeats") def __repr__(self) -> str: return f"" class AttendanceRecord(Base): """Attendance punch records from badges.""" __tablename__ = "attendance_records" __table_args__ = ( Index("ix_attendance_device_time", "device_id", "recorded_at"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) device_id: Mapped[int] = mapped_column( Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False ) attendance_type: Mapped[str] = mapped_column( String(20), nullable=False ) # clock_in, clock_out protocol_number: Mapped[int] = mapped_column(Integer, nullable=False) gps_positioned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) latitude: Mapped[float | None] = mapped_column(Float, nullable=True) longitude: Mapped[float | None] = mapped_column(Float, nullable=True) speed: Mapped[float | None] = mapped_column(Float, nullable=True) course: Mapped[float | None] = mapped_column(Float, nullable=True) gps_satellites: Mapped[int | None] = mapped_column(Integer, nullable=True) battery_level: Mapped[int | None] = mapped_column(Integer, nullable=True) gsm_signal: Mapped[int | None] = mapped_column(Integer, nullable=True) mcc: Mapped[int | None] = mapped_column(Integer, nullable=True) mnc: Mapped[int | None] = mapped_column(Integer, nullable=True) lac: Mapped[int | None] = mapped_column(BigInteger, nullable=True) cell_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True) wifi_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) lbs_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) address: Mapped[str | None] = mapped_column(Text, nullable=True) recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) device: Mapped["Device"] = relationship(back_populates="attendance_records") def __repr__(self) -> str: return ( f"" ) class BluetoothRecord(Base): """Bluetooth punch card and location records.""" __tablename__ = "bluetooth_records" __table_args__ = ( Index("ix_bluetooth_device_time", "device_id", "recorded_at"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) device_id: Mapped[int] = mapped_column( Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False ) record_type: Mapped[str] = mapped_column( String(20), nullable=False ) # punch, location protocol_number: Mapped[int] = mapped_column(Integer, nullable=False) beacon_mac: Mapped[str | None] = mapped_column(String(20), nullable=True) beacon_uuid: Mapped[str | None] = mapped_column(String(36), nullable=True) beacon_major: Mapped[int | None] = mapped_column(Integer, nullable=True) beacon_minor: Mapped[int | None] = mapped_column(Integer, nullable=True) rssi: Mapped[int | None] = mapped_column(Integer, nullable=True) beacon_battery: Mapped[float | None] = mapped_column(Float, nullable=True) beacon_battery_unit: Mapped[str | None] = mapped_column(String(10), nullable=True) attendance_type: Mapped[str | None] = mapped_column(String(20), nullable=True) bluetooth_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) latitude: Mapped[float | None] = mapped_column(Float, nullable=True) longitude: Mapped[float | None] = mapped_column(Float, nullable=True) recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) device: Mapped["Device"] = relationship(back_populates="bluetooth_records") def __repr__(self) -> str: return ( f"" ) class BeaconConfig(Base): """Registered Bluetooth beacon configuration for indoor positioning.""" __tablename__ = "beacon_configs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) beacon_mac: Mapped[str] = mapped_column(String(20), unique=True, index=True, nullable=False) beacon_uuid: Mapped[str | None] = mapped_column(String(36), nullable=True) beacon_major: Mapped[int | None] = mapped_column(Integer, nullable=True) beacon_minor: Mapped[int | None] = mapped_column(Integer, nullable=True) name: Mapped[str] = mapped_column(String(100), nullable=False) floor: Mapped[str | None] = mapped_column(String(20), nullable=True) area: Mapped[str | None] = mapped_column(String(100), nullable=True) latitude: Mapped[float | None] = mapped_column(Float, nullable=True) longitude: Mapped[float | None] = mapped_column(Float, nullable=True) address: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[str] = mapped_column(String(20), default="active", nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) updated_at: Mapped[datetime | None] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow, nullable=True ) def __repr__(self) -> str: return f"" class CommandLog(Base): """Log of commands sent to devices.""" __tablename__ = "command_logs" __table_args__ = ( Index("ix_command_device_status", "device_id", "status"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) device_id: Mapped[int] = mapped_column( Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False ) command_type: Mapped[str] = mapped_column(String(30), nullable=False) command_content: Mapped[str] = mapped_column(Text, nullable=False) server_flag: Mapped[str] = mapped_column(String(20), nullable=False) response_content: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[str] = mapped_column( String(20), default="pending", nullable=False ) # pending, sent, success, failed sent_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) response_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) device: Mapped["Device"] = relationship(back_populates="command_logs") def __repr__(self) -> str: return ( f"" ) class ApiKey(Base): """API keys for external system authentication.""" __tablename__ = "api_keys" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) key_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False) permissions: Mapped[str] = mapped_column( String(20), default="read", nullable=False ) # read, write, admin is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) last_used_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False) def __repr__(self) -> str: return f""