Files
desungongpai/app/models.py

324 lines
15 KiB
Python

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"<Device(id={self.id}, imei={self.imei}, status={self.status})>"
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"<LocationRecord(id={self.id}, device_id={self.device_id}, "
f"type={self.location_type})>"
)
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(10), 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"<AlarmRecord(id={self.id}, device_id={self.device_id}, "
f"type={self.alarm_type})>"
)
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"<HeartbeatRecord(id={self.id}, device_id={self.device_id})>"
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"<AttendanceRecord(id={self.id}, device_id={self.device_id}, "
f"type={self.attendance_type})>"
)
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"<BluetoothRecord(id={self.id}, device_id={self.device_id}, "
f"type={self.record_type})>"
)
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"<BeaconConfig(id={self.id}, mac={self.beacon_mac}, name={self.name})>"
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"<CommandLog(id={self.id}, device_id={self.device_id}, "
f"type={self.command_type}, status={self.status})>"
)