"""SQLite database schema and connection management for frigate-notify v2.""" import logging import sqlite3 import threading logger = logging.getLogger(__name__) # Thread-local storage for database connections _local = threading.local() _db_path: str = "/data/frigate-notify.db" def set_db_path(path: str) -> None: """Set the database path. Must be called before any other database operations.""" global _db_path _db_path = path def get_connection() -> sqlite3.Connection: """Get a thread-local database connection with WAL mode and foreign keys enabled.""" if not hasattr(_local, "connection") or _local.connection is None: conn = sqlite3.connect(_db_path) conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") conn.row_factory = sqlite3.Row _local.connection = conn return _local.connection def close_connection() -> None: """Close the thread-local database connection if one exists.""" if hasattr(_local, "connection") and _local.connection is not None: _local.connection.close() _local.connection = None def init_db() -> None: """Create all tables and default settings.""" conn = get_connection() cursor = conn.cursor() cursor.executescript( """ CREATE TABLE IF NOT EXISTS known_objects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN ('person', 'vehicle', 'pet')), action TEXT NOT NULL DEFAULT 'quiet' CHECK(action IN ('quiet', 'suppress', 'alert')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS embeddings ( id INTEGER PRIMARY KEY AUTOINCREMENT, known_object_id INTEGER NOT NULL, embedding BLOB NOT NULL, snapshot_url TEXT, thumbnail BLOB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (known_object_id) REFERENCES known_objects(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, frigate_event_id TEXT UNIQUE NOT NULL, camera TEXT NOT NULL, label TEXT NOT NULL, score REAL NOT NULL, snapshot BLOB, embedding BLOB, match_status TEXT NOT NULL DEFAULT 'unreviewed' CHECK(match_status IN ('known', 'unknown', 'ambiguous', 'unreviewed')), matched_object_id INTEGER, match_confidence REAL, ai_description TEXT, notified INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (matched_object_id) REFERENCES known_objects(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at); CREATE INDEX IF NOT EXISTS idx_events_match_status ON events(match_status); CREATE INDEX IF NOT EXISTS idx_events_camera ON events(camera); CREATE INDEX IF NOT EXISTS idx_embeddings_known_object_id ON embeddings(known_object_id); """ ) # Insert default settings if they don't exist defaults = { "vision_backend": "haiku", "recognition_enabled": "false", "event_retention_days": "90", } for key, value in defaults.items(): cursor.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", (key, value), ) conn.commit() logger.info("Database initialized at %s", _db_path)