2026-03-27 13:04:11 +00:00
|
|
|
from datetime import datetime
|
2026-03-27 10:19:34 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-03-27 13:04:11 +00:00
|
|
|
from app.config import now_cst as _utcnow
|
2026-03-27 10:19:34 +00:00
|
|
|
from app.database import Base
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
2026-03-27 13:04:11 +00:00
|
|
|
imei: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
2026-03-27 10:19:34 +00:00
|
|
|
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
|
|
|
|
|
)
|
2026-03-27 13:04:11 +00:00
|
|
|
imei: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
2026-03-27 10:19:34 +00:00
|
|
|
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"<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
|
|
|
|
|
)
|
2026-03-27 13:04:11 +00:00
|
|
|
imei: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
2026-03-27 10:19:34 +00:00
|
|
|
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
|
|
|
|
|
)
|
2026-03-27 13:04:11 +00:00
|
|
|
imei: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
2026-03-27 10:19:34 +00:00
|
|
|
attendance_type: Mapped[str] = mapped_column(
|
|
|
|
|
String(20), nullable=False
|
|
|
|
|
) # clock_in, clock_out
|
2026-03-30 04:26:29 +00:00
|
|
|
attendance_source: Mapped[str] = mapped_column(
|
|
|
|
|
String(20), nullable=False, default="device",
|
|
|
|
|
server_default="device",
|
|
|
|
|
) # device (0xB0/0xB1), bluetooth (0xB2), fence (auto)
|
2026-03-27 10:19:34 +00:00
|
|
|
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
|
|
|
|
|
)
|
2026-03-27 13:04:11 +00:00
|
|
|
imei: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
2026-03-27 10:19:34 +00:00
|
|
|
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})>"
|
|
|
|
|
|
|
|
|
|
|
2026-03-27 13:04:11 +00:00
|
|
|
class FenceConfig(Base):
|
|
|
|
|
"""Geofence configuration for area monitoring."""
|
|
|
|
|
|
|
|
|
|
__tablename__ = "fence_configs"
|
|
|
|
|
|
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
|
|
|
fence_type: Mapped[str] = mapped_column(String(20), nullable=False) # circle / polygon / rectangle
|
|
|
|
|
# Circle center (WGS-84) or polygon centroid for display
|
|
|
|
|
center_lat: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
|
|
|
center_lng: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
|
|
|
radius: Mapped[float | None] = mapped_column(Float, nullable=True) # meters, for circle
|
|
|
|
|
# Polygon/rectangle vertices as JSON: [[lng,lat], [lng,lat], ...] (WGS-84)
|
|
|
|
|
points: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
color: Mapped[str] = mapped_column(String(20), default="#3b82f6", nullable=False)
|
|
|
|
|
fill_color: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
|
|
|
fill_opacity: Mapped[float] = mapped_column(Float, default=0.2, nullable=False)
|
|
|
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
is_active: Mapped[bool] = mapped_column(Integer, default=1, 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"<FenceConfig(id={self.id}, name={self.name}, type={self.fence_type})>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceFenceBinding(Base):
|
|
|
|
|
"""Many-to-many binding between devices and geofences."""
|
|
|
|
|
|
|
|
|
|
__tablename__ = "device_fence_bindings"
|
|
|
|
|
__table_args__ = (
|
|
|
|
|
Index("ix_dfb_device_fence", "device_id", "fence_id", unique=True),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
fence_id: Mapped[int] = mapped_column(
|
|
|
|
|
Integer, ForeignKey("fence_configs.id", ondelete="CASCADE"), index=True, nullable=False
|
|
|
|
|
)
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return f"<DeviceFenceBinding(device_id={self.device_id}, fence_id={self.fence_id})>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceFenceState(Base):
|
|
|
|
|
"""Runtime state tracking: is a device currently inside a fence?"""
|
|
|
|
|
|
|
|
|
|
__tablename__ = "device_fence_states"
|
|
|
|
|
__table_args__ = (
|
|
|
|
|
Index("ix_dfs_device_fence", "device_id", "fence_id", unique=True),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
fence_id: Mapped[int] = mapped_column(
|
|
|
|
|
Integer, ForeignKey("fence_configs.id", ondelete="CASCADE"), index=True, nullable=False
|
|
|
|
|
)
|
|
|
|
|
is_inside: Mapped[bool] = mapped_column(Integer, default=0, nullable=False)
|
|
|
|
|
last_transition_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
|
last_check_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
|
last_latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
|
|
|
last_longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return f"<DeviceFenceState(device_id={self.device_id}, fence_id={self.fence_id}, inside={self.is_inside})>"
|
|
|
|
|
|
|
|
|
|
|
2026-03-27 10:19:34 +00:00
|
|
|
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})>"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"<ApiKey(id={self.id}, name={self.name}, permissions={self.permissions})>"
|