"""Open IP camera discovery — models and migration helpers. Cameras are stored in their own `cameras` table (TimescaleDB hypertable on last_seen) alongside the existing `events` feed, deduped by URL hash. """ from __future__ import annotations from sqlalchemy import ( Column, Float, Index, Integer, String, Text, DateTime, JSON, func, Table, BigInteger, ) from sqlalchemy.dialects.postgresql import UUID import uuid from database import metadata cameras = Table( "cameras", metadata, Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4), # sha256 hex of the camera's source/snapshot URL — the dedupe key. Column("url_hash", String(64), nullable=False, index=True), Column("source_url", Text, nullable=False), # Snapshot endpoint (may differ from the directory listing URL). Column("snapshot_url", Text), # Which discovery feed produced this record (e.g. "insecam", "camera_list"). Column("discovery_source", String(128), nullable=False, index=True), Column("location_lat", Float), Column("location_lon", Float), Column("location_name", String(512)), # Vendor / device type when detectable from the directory page or URL. Column("vendor", String(128)), Column("device_type", String(64)), # e.g. ip-cam, rtsp, mjpeg Column("first_seen", DateTime(timezone=True), server_default=func.now(), nullable=False), Column("last_seen", DateTime(timezone=True), server_default=func.now(), nullable=False), Column("raw", JSON), ) Index("ix_cameras_location", cameras.c.location_lat, cameras.c.location_lon) Index("ix_cameras_last_seen", cameras.c.last_seen)