""" GigLez SQLAlchemy ORM Models Database models matching the PostgreSQL + PostGIS schema Based on Wigle wardriving patterns adapted for IoT RF device mapping """ from datetime import datetime from typing import Optional, List from sqlalchemy import ( Column, String, Integer, Float, DateTime, Text, Boolean, DECIMAL, ARRAY, ForeignKey, CheckConstraint, UniqueConstraint, Index, BYTEA ) from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.dialects.postgresql import JSONB from geoalchemy2 import Geometry from geoalchemy2.functions import ST_SetSRID, ST_MakePoint # ============================================================================= # BASE # ============================================================================= Base = declarative_base() # ============================================================================= # CORE MODELS # ============================================================================= class User(Base): """User accounts for community features""" __tablename__ = 'users' # Primary Key id = Column(Integer, primary_key=True) # Authentication username = Column(String(50), unique=True, nullable=False, index=True) email = Column(String(255), unique=True, nullable=False, index=True) password_hash = Column(String(255), nullable=False) # Profile display_name = Column(String(100)) avatar_url = Column(Text) bio = Column(Text) # Statistics total_captures = Column(Integer, default=0) total_identifications = Column(Integer, default=0) reputation_score = Column(Integer, default=0) # Settings api_key = Column(String(64), unique=True, index=True) email_verified = Column(Boolean, default=False) # Timestamps created_at = Column(DateTime, default=datetime.utcnow) last_login = Column(DateTime) # Relationships sessions = relationship("Session", back_populates="user") captures = relationship("Capture", back_populates="user") identifications = relationship("Identification", back_populates="user") votes = relationship("Vote", back_populates="user") def __repr__(self): return f"" def to_dict(self): """Convert to dictionary""" return { 'id': self.id, 'username': self.username, 'display_name': self.display_name, 'total_captures': self.total_captures, 'reputation_score': self.reputation_score, 'created_at': self.created_at.isoformat() if self.created_at else None } class Session(Base): """Wardriving/capture sessions (Wigle pattern)""" __tablename__ = 'sessions' # Primary Key id = Column(Integer, primary_key=True) # Foreign Keys user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), index=True) # Session Metadata session_uuid = Column(String(64), unique=True, nullable=False, index=True) name = Column(String(200)) description = Column(Text) started_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True) ended_at = Column(DateTime) # Privacy Settings is_public = Column(Boolean, default=True, index=True) anonymize_gps = Column(Boolean, default=False) gps_precision_meters = Column(Integer, default=10) # Session Statistics (auto-updated by triggers) total_captures = Column(Integer, default=0) unique_devices = Column(Integer, default=0) distance_km = Column(DECIMAL(10, 2)) # Bounding Box (auto-updated by triggers) min_latitude = Column(DECIMAL(10, 8)) max_latitude = Column(DECIMAL(10, 8)) min_longitude = Column(DECIMAL(11, 8)) max_longitude = Column(DECIMAL(11, 8)) # Relationships user = relationship("User", back_populates="sessions") captures = relationship("Capture", back_populates="session") def __repr__(self): return f"" def to_dict(self): """Convert to dictionary""" return { 'id': self.id, 'session_uuid': self.session_uuid, 'name': self.name, 'started_at': self.started_at.isoformat() if self.started_at else None, 'ended_at': self.ended_at.isoformat() if self.ended_at else None, 'total_captures': self.total_captures, 'unique_devices': self.unique_devices, 'is_public': self.is_public } class Device(Base): """Known IoT device types from signature databases""" __tablename__ = 'devices' # Primary Key id = Column(Integer, primary_key=True) # Device Identification manufacturer = Column(String(200), index=True) model = Column(String(200)) device_type = Column(String(100), index=True) description = Column(Text) # RF Characteristics typical_frequency = Column(Integer, index=True) frequency_range_low = Column(Integer) frequency_range_high = Column(Integer) modulation_types = Column(ARRAY(Text)) # Protocol Information protocol = Column(String(100), index=True) bit_length = Column(Integer) encoding = Column(String(50)) # Metadata fcc_id = Column(String(50)) manufacturer_code = Column(String(50)) # Community Data created_at = Column(DateTime, default=datetime.utcnow) verified = Column(Boolean, default=False, index=True) verification_count = Column(Integer, default=0) # Source source = Column(String(50), index=True) # 'flipper', 'rtl433', 'urh', 'community' source_url = Column(Text) # Full-text search (auto-updated by trigger) search_vector = Column('search_vector', nullable=True) # Relationships signatures = relationship("Signature", back_populates="device") captures = relationship("Capture", back_populates="device") capture_matches = relationship("CaptureMatch", back_populates="device") def __repr__(self): return f"" def to_dict(self): """Convert to dictionary""" return { 'id': self.id, 'manufacturer': self.manufacturer, 'model': self.model, 'device_type': self.device_type, 'description': self.description, 'typical_frequency': self.typical_frequency, 'protocol': self.protocol, 'verified': self.verified, 'source': self.source } class Capture(Base): """RF signal captures with GPS coordinates""" __tablename__ = 'captures' # Primary Key: SHA256 hash of file (Wigle deduplication pattern) file_hash = Column(String(64), primary_key=True) # Foreign Keys session_id = Column(Integer, ForeignKey('sessions.id', ondelete='CASCADE'), index=True) user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), index=True) device_id = Column(Integer, ForeignKey('devices.id', ondelete='SET NULL'), index=True) # GPS Data latitude = Column(DECIMAL(10, 8), nullable=False) longitude = Column(DECIMAL(11, 8), nullable=False) altitude = Column(DECIMAL(8, 2)) gps_accuracy = Column(DECIMAL(6, 2)) geom = Column(Geometry('POINT', srid=4326)) # Auto-populated by trigger # Timestamps captured_at = Column(DateTime, nullable=False, index=True) uploaded_at = Column(DateTime, default=datetime.utcnow, index=True) # RF Signal Data frequency = Column(Integer, nullable=False, index=True) rssi = Column(Integer) modulation = Column(String(50)) preset = Column(String(100)) # Protocol Information (if decoded) protocol = Column(String(100), index=True) bit_length = Column(Integer) key_data = Column(BYTEA) timing_element = Column(Integer) # Raw Signal Data raw_data = Column(Text) raw_format = Column(String(20)) # 'RAW', 'BinRAW', 'KEY' # File Storage file_path = Column(String(500)) file_size = Column(Integer) # Automatic Matching Results (best match) match_confidence = Column(DECIMAL(5, 4)) match_method = Column(String(50)) # Constraints __table_args__ = ( CheckConstraint('latitude >= -90 AND latitude <= 90', name='valid_latitude'), CheckConstraint('longitude >= -180 AND longitude <= 180', name='valid_longitude'), CheckConstraint('match_confidence >= 0 AND match_confidence <= 1', name='valid_confidence'), CheckConstraint('frequency >= 300000000 AND frequency <= 928000000', name='valid_frequency'), Index('idx_captures_geom', 'geom', postgresql_using='gist'), ) # Relationships session = relationship("Session", back_populates="captures") user = relationship("User", back_populates="captures") device = relationship("Device", back_populates="captures") capture_matches = relationship("CaptureMatch", back_populates="capture") identifications = relationship("Identification", back_populates="capture") def __repr__(self): return f"" def to_dict(self): """Convert to dictionary""" return { 'file_hash': self.file_hash, 'latitude': float(self.latitude) if self.latitude else None, 'longitude': float(self.longitude) if self.longitude else None, 'altitude': float(self.altitude) if self.altitude else None, 'gps_accuracy': float(self.gps_accuracy) if self.gps_accuracy else None, 'captured_at': self.captured_at.isoformat() if self.captured_at else None, 'frequency': self.frequency, 'rssi': self.rssi, 'modulation': self.modulation, 'protocol': self.protocol, 'bit_length': self.bit_length, 'device_id': self.device_id, 'match_confidence': float(self.match_confidence) if self.match_confidence else None, 'match_method': self.match_method } class Signature(Base): """Protocol signatures for device matching""" __tablename__ = 'signatures' # Primary Key id = Column(Integer, primary_key=True) # Foreign Keys device_id = Column(Integer, ForeignKey('devices.id', ondelete='CASCADE'), index=True) # Signature Pattern protocol = Column(String(100), nullable=False, index=True) frequency = Column(Integer, index=True) modulation = Column(String(50)) # Matching Criteria bit_pattern = Column(BYTEA) bit_mask = Column(BYTEA) timing_min = Column(Integer) timing_max = Column(Integer) # Raw Pattern pattern_regex = Column(Text) # Confidence Weighting weight = Column(DECIMAL(4, 3), default=1.0) # Metadata created_at = Column(DateTime, default=datetime.utcnow) source = Column(String(50), index=True) source_file = Column(String(500)) # Constraints __table_args__ = ( UniqueConstraint('device_id', 'protocol', 'bit_pattern', name='uq_device_protocol_pattern'), ) # Relationships device = relationship("Device", back_populates="signatures") flipper_signature = relationship("FlipperSignature", back_populates="signature", uselist=False) rtl433_protocol = relationship("RTL433Protocol", back_populates="signature", uselist=False) def __repr__(self): return f"" class CaptureMatch(Base): """Many-to-many mapping of captures to devices with confidence scores""" __tablename__ = 'capture_matches' # Primary Key id = Column(Integer, primary_key=True) # Foreign Keys capture_id = Column(String(64), ForeignKey('captures.file_hash', ondelete='CASCADE'), index=True) device_id = Column(Integer, ForeignKey('devices.id', ondelete='CASCADE'), index=True) signature_id = Column(Integer, ForeignKey('signatures.id', ondelete='SET NULL'), index=True) # Match Details confidence = Column(DECIMAL(5, 4), nullable=False, index=True) match_method = Column(String(50), nullable=False) match_details = Column(JSONB) # Timestamp matched_at = Column(DateTime, default=datetime.utcnow) # Constraints __table_args__ = ( CheckConstraint('confidence >= 0 AND confidence <= 1', name='valid_match_confidence'), ) # Relationships capture = relationship("Capture", back_populates="capture_matches") device = relationship("Device", back_populates="capture_matches") def __repr__(self): return f"" class Identification(Base): """User-submitted device identifications""" __tablename__ = 'identifications' # Primary Key id = Column(Integer, primary_key=True) # Foreign Keys capture_id = Column(String(64), ForeignKey('captures.file_hash', ondelete='CASCADE'), index=True) device_id = Column(Integer, ForeignKey('devices.id'), index=True) user_id = Column(Integer, ForeignKey('users.id'), index=True) # Identification Details confidence = Column(String(20)) # 'certain', 'likely', 'guess' notes = Column(Text) # Visual Evidence photo_urls = Column(ARRAY(Text)) # Community Validation upvotes = Column(Integer, default=0) downvotes = Column(Integer, default=0) verified = Column(Boolean, default=False, index=True) verified_by = Column(Integer, ForeignKey('users.id')) verified_at = Column(DateTime) # Timestamp submitted_at = Column(DateTime, default=datetime.utcnow) # Constraints __table_args__ = ( UniqueConstraint('capture_id', 'user_id', 'device_id', name='uq_capture_user_device'), ) # Relationships capture = relationship("Capture", back_populates="identifications") user = relationship("User", back_populates="identifications", foreign_keys=[user_id]) votes = relationship("Vote", back_populates="identification") def __repr__(self): return f"" class Vote(Base): """Votes on device identifications""" __tablename__ = 'votes' # Primary Key id = Column(Integer, primary_key=True) # Foreign Keys identification_id = Column(Integer, ForeignKey('identifications.id', ondelete='CASCADE'), index=True) user_id = Column(Integer, ForeignKey('users.id'), index=True) # Vote Data vote_type = Column(Integer, nullable=False) # 1 = upvote, -1 = downvote voted_at = Column(DateTime, default=datetime.utcnow) # Constraints __table_args__ = ( CheckConstraint('vote_type IN (1, -1)', name='valid_vote'), UniqueConstraint('identification_id', 'user_id', name='uq_identification_user'), ) # Relationships identification = relationship("Identification", back_populates="votes") user = relationship("User", back_populates="votes") def __repr__(self): return f"" class UploadMarker(Base): """Track last uploaded capture per user/session (Wigle incremental sync pattern)""" __tablename__ = 'upload_markers' # Composite Primary Key user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True) session_id = Column(Integer, ForeignKey('sessions.id', ondelete='CASCADE'), primary_key=True) # Marker Data last_capture_hash = Column(String(64)) last_upload_time = Column(DateTime, nullable=False, default=datetime.utcnow) def __repr__(self): return f"" # ============================================================================= # SIGNATURE DATABASE MODELS # ============================================================================= class FlipperSignature(Base): """Flipper Zero specific signature data""" __tablename__ = 'flipper_signatures' # Primary Key id = Column(Integer, primary_key=True) # Foreign Key signature_id = Column(Integer, ForeignKey('signatures.id', ondelete='CASCADE'), index=True) # Flipper-Specific Fields filetype = Column(String(50)) version = Column(Integer) preset = Column(String(100), index=True) # Custom Preset Data custom_preset_module = Column(String(50)) custom_preset_data = Column(BYTEA) # Protocol Data protocol = Column(String(100), index=True) bit = Column(Integer) key = Column(BYTEA) te = Column(Integer) # RAW Data raw_data = Column(Text) bin_raw_bit = Column(Integer) bin_raw_te = Column(Integer) bin_raw_data = Column(BYTEA) # Source source_file = Column(String(500)) imported_at = Column(DateTime, default=datetime.utcnow) # Relationships signature = relationship("Signature", back_populates="flipper_signature") def __repr__(self): return f"" class RTL433Protocol(Base): """RTL_433 specific protocol data""" __tablename__ = 'rtl433_protocols' # Primary Key id = Column(Integer, primary_key=True) # Foreign Key signature_id = Column(Integer, ForeignKey('signatures.id', ondelete='CASCADE'), index=True) # Protocol Identification protocol_number = Column(Integer, index=True) protocol_name = Column(String(200)) model = Column(String(200), index=True) # RF Characteristics frequency = Column(Integer) modulation = Column(String(50), index=True) # Timing Information short_width = Column(Integer) long_width = Column(Integer) reset_limit = Column(Integer) gap_limit = Column(Integer) # Decoding decoder_type = Column(String(50)) bit_count = Column(Integer) # JSON Fields Mapping json_fields = Column(JSONB) # Source source_file = Column(String(500)) imported_at = Column(DateTime, default=datetime.utcnow) # Relationships signature = relationship("Signature", back_populates="rtl433_protocol") def __repr__(self): return f"" # ============================================================================= # HELPER FUNCTIONS # ============================================================================= def create_all_tables(engine): """Create all tables in the database""" Base.metadata.create_all(engine) def drop_all_tables(engine): """Drop all tables from the database (use with caution!)""" Base.metadata.drop_all(engine)