""" Storage Handler - persists OBD2 readings to SQLite. Implements batching for efficient database writes. """ import threading import time from typing import Dict, Any, List, Optional from datetime import datetime from .base import BaseHandler, OBD2Reading from ..storage.storage import get_storage, Storage from ..logger import get_logger logger = get_logger(__name__) class StorageHandler(BaseHandler): """ Handler that saves OBD2 readings to SQLite. Features: - Configurable batch size for efficient writes - Automatic flush on interval - Session management integration - Thread-safe batching """ def __init__( self, enabled: bool = True, batch_size: int = 50, flush_interval: float = 1.0, db_path: Optional[str] = None, auto_session: bool = True, ): """ Initialize storage handler. Args: enabled: Whether handler is active batch_size: Number of readings to batch before write flush_interval: Maximum time before flushing batch (seconds) db_path: Path to SQLite database auto_session: Automatically start session on first reading """ super().__init__(name="storage", enabled=enabled) self._storage: Optional[Storage] = None self._batch_size = batch_size self._flush_interval = flush_interval self._db_path = db_path self._auto_session = auto_session self._batch: List[Dict[str, Any]] = [] self._batch_lock = threading.Lock() self._last_flush_time = time.time() self._saved_count = 0 self._batch_count = 0 self._session_started = False def initialize(self) -> bool: """Initialize the handler.""" try: self._storage = get_storage(db_path=self._db_path) self._initialized = True logger.info( f"Storage handler initialized: batch_size={self._batch_size}, " f"flush_interval={self._flush_interval}s" ) return True except Exception as e: logger.error(f"Failed to initialize storage handler: {e}") return False def _ensure_session(self) -> None: """Ensure a session is started.""" if self._auto_session and not self._session_started: if self._storage and not self._storage.get_current_session_id(): self._storage.start_session() self._session_started = True def handle(self, reading: OBD2Reading) -> bool: """ Add reading to batch. Args: reading: OBD2Reading to save Returns: True if reading was accepted """ if not self._initialized or not self.enabled: return False self._ensure_session() with self._batch_lock: self._batch.append({ "pid": reading.pid, "name": reading.name, "value": reading.value, "unit": reading.unit, "timestamp": reading.timestamp.isoformat(), "session_id": reading.session_id, }) should_flush = ( len(self._batch) >= self._batch_size or (time.time() - self._last_flush_time) >= self._flush_interval ) if should_flush: self._do_flush() return True def handle_batch(self, readings: List[OBD2Reading]) -> int: """ Add multiple readings to batch. Args: readings: List of OBD2Reading objects Returns: Number of readings accepted """ if not self._initialized or not self.enabled: return 0 self._ensure_session() with self._batch_lock: for reading in readings: self._batch.append({ "pid": reading.pid, "name": reading.name, "value": reading.value, "unit": reading.unit, "timestamp": reading.timestamp.isoformat(), "session_id": reading.session_id, }) should_flush = ( len(self._batch) >= self._batch_size or (time.time() - self._last_flush_time) >= self._flush_interval ) if should_flush: self._do_flush() return len(readings) def _do_flush(self) -> None: """Internal flush - must be called with lock held.""" if not self._storage or not self._batch: return batch_to_save = self._batch self._batch = [] self._last_flush_time = time.time() try: saved = self._storage.save_readings_batch(batch_to_save) self._saved_count += saved self._batch_count += 1 logger.debug(f"Flushed {saved} readings (batch #{self._batch_count})") except Exception as e: logger.error(f"Failed to flush readings: {e}") # Put back failed batch self._batch = batch_to_save + self._batch def flush(self) -> None: """Flush pending readings to database.""" with self._batch_lock: self._do_flush() def shutdown(self) -> None: """Shutdown the handler.""" self.flush() if self._storage and self._session_started: self._storage.end_session() logger.info( f"Storage handler shutdown: saved={self._saved_count}, " f"batches={self._batch_count}" ) self._initialized = False def get_stats(self) -> Dict[str, Any]: """Get handler statistics.""" storage_stats = {} if self._storage: storage_stats = self._storage.get_stats() with self._batch_lock: pending = len(self._batch) return { "name": self.name, "enabled": self.enabled, "initialized": self._initialized, "saved_count": self._saved_count, "batch_count": self._batch_count, "pending_in_batch": pending, "batch_size": self._batch_size, "flush_interval": self._flush_interval, **storage_stats, } @property def storage(self) -> Optional[Storage]: """Get the Storage instance.""" return self._storage def start_session(self) -> Optional[str]: """Manually start a new session.""" if self._storage: session_id = self._storage.start_session() self._session_started = True return session_id return None def end_session(self) -> None: """Manually end current session.""" if self._storage: self._storage.end_session() self._session_started = False