"""Configuration management for OBD2 client.""" import json import os from dataclasses import dataclass, field from pathlib import Path from typing import List, Optional @dataclass class CANConfig: """CAN bus configuration.""" interface: str = "can0" bitrate: int = 500000 virtual: bool = False @dataclass class OBD2Config: """OBD2 protocol configuration.""" request_id: int = 0x7DF response_id: int = 0x7E8 timeout: float = 0.1 @dataclass class ScanConfig: """PID scan configuration.""" retries: int = 5 retry_delay: float = 1.5 initial_delay: float = 1.0 @dataclass class PollingConfig: """Polling configuration.""" interval_fast: float = 0.1 interval_slow: float = 1.0 fast_pids: List[int] = field(default_factory=lambda: [0x0C, 0x0D, 0x11]) slow_pids: List[int] = field(default_factory=lambda: [0x05, 0x2F, 0x5C]) @dataclass class StorageConfig: """SQLite storage configuration.""" enabled: bool = True db_path: Optional[str] = None # None = default location wal_mode: bool = True retention_days: int = 30 batch_size: int = 50 flush_interval: float = 1.0 @dataclass class PostgreSQLConfig: """PostgreSQL sync configuration.""" enabled: bool = False host: str = "localhost" port: int = 5432 database: str = "carpibord" user: str = "carpibord" password: str = "" sync_interval: float = 30.0 batch_size: int = 500 max_retries: int = 3 retry_delay: float = 5.0 @dataclass class FlipperConfig: """Flipper Zero configuration.""" enabled: bool = False port: str = "/dev/serial0" baudrate: int = 115200 show_ups: bool = True @dataclass class Config: """Main configuration container.""" can: CANConfig = field(default_factory=CANConfig) obd2: OBD2Config = field(default_factory=OBD2Config) scan: ScanConfig = field(default_factory=ScanConfig) polling: PollingConfig = field(default_factory=PollingConfig) storage: StorageConfig = field(default_factory=StorageConfig) postgresql: PostgreSQLConfig = field(default_factory=PostgreSQLConfig) flipper: FlipperConfig = field(default_factory=FlipperConfig) @classmethod def load(cls, config_path: Optional[str] = None) -> "Config": """Load configuration from JSON file and environment variables. Args: config_path: Path to config.json file Returns: Config instance """ config = cls() if config_path is None: config_path = os.environ.get( "OBD2_CONFIG_PATH", str(Path(__file__).parent.parent / "config.json"), ) config_file = Path(config_path) if config_file.exists(): with open(config_file, "r") as f: data = json.load(f) config._load_from_dict(data) config._load_from_env() return config def _load_from_dict(self, data: dict) -> None: """Load configuration from dictionary.""" if "can" in data: can_data = data["can"] self.can.interface = can_data.get("interface", self.can.interface) self.can.bitrate = can_data.get("bitrate", self.can.bitrate) self.can.virtual = can_data.get("virtual", self.can.virtual) if "obd2" in data: obd2_data = data["obd2"] if "request_id" in obd2_data: self.obd2.request_id = self._parse_hex(obd2_data["request_id"]) if "response_id" in obd2_data: self.obd2.response_id = self._parse_hex(obd2_data["response_id"]) self.obd2.timeout = obd2_data.get("timeout", self.obd2.timeout) if "scan" in data: scan_data = data["scan"] self.scan.retries = scan_data.get("retries", self.scan.retries) self.scan.retry_delay = scan_data.get("retry_delay", self.scan.retry_delay) self.scan.initial_delay = scan_data.get("initial_delay", self.scan.initial_delay) if "polling" in data: poll_data = data["polling"] self.polling.interval_fast = poll_data.get( "interval_fast", self.polling.interval_fast ) self.polling.interval_slow = poll_data.get( "interval_slow", self.polling.interval_slow ) if "fast_pids" in poll_data: self.polling.fast_pids = [ self._parse_hex(p) for p in poll_data["fast_pids"] ] if "slow_pids" in poll_data: self.polling.slow_pids = [ self._parse_hex(p) for p in poll_data["slow_pids"] ] if "storage" in data: storage_data = data["storage"] self.storage.enabled = storage_data.get("enabled", self.storage.enabled) self.storage.db_path = storage_data.get("db_path", self.storage.db_path) self.storage.wal_mode = storage_data.get("wal_mode", self.storage.wal_mode) self.storage.retention_days = storage_data.get( "retention_days", self.storage.retention_days ) self.storage.batch_size = storage_data.get( "batch_size", self.storage.batch_size ) self.storage.flush_interval = storage_data.get( "flush_interval", self.storage.flush_interval ) if "postgresql" in data: pg_data = data["postgresql"] self.postgresql.enabled = pg_data.get("enabled", self.postgresql.enabled) self.postgresql.host = pg_data.get("host", self.postgresql.host) self.postgresql.port = pg_data.get("port", self.postgresql.port) self.postgresql.database = pg_data.get("database", self.postgresql.database) self.postgresql.user = pg_data.get("user", self.postgresql.user) self.postgresql.password = pg_data.get("password", self.postgresql.password) self.postgresql.sync_interval = pg_data.get( "sync_interval", self.postgresql.sync_interval ) self.postgresql.batch_size = pg_data.get( "batch_size", self.postgresql.batch_size ) self.postgresql.max_retries = pg_data.get( "max_retries", self.postgresql.max_retries ) self.postgresql.retry_delay = pg_data.get( "retry_delay", self.postgresql.retry_delay ) if "flipper" in data: flipper_data = data["flipper"] self.flipper.enabled = flipper_data.get("enabled", self.flipper.enabled) self.flipper.port = flipper_data.get("port", self.flipper.port) self.flipper.baudrate = flipper_data.get("baudrate", self.flipper.baudrate) self.flipper.show_ups = flipper_data.get("show_ups", self.flipper.show_ups) def _load_from_env(self) -> None: """Load configuration from environment variables.""" # CAN config if "OBD2_CAN_INTERFACE" in os.environ: self.can.interface = os.environ["OBD2_CAN_INTERFACE"] if "OBD2_CAN_BITRATE" in os.environ: self.can.bitrate = int(os.environ["OBD2_CAN_BITRATE"]) if "OBD2_CAN_VIRTUAL" in os.environ: self.can.virtual = os.environ["OBD2_CAN_VIRTUAL"].lower() == "true" if "OBD2_TIMEOUT" in os.environ: self.obd2.timeout = float(os.environ["OBD2_TIMEOUT"]) # Storage config if "OBD2_STORAGE_ENABLED" in os.environ: self.storage.enabled = os.environ["OBD2_STORAGE_ENABLED"].lower() == "true" if "OBD2_STORAGE_PATH" in os.environ: self.storage.db_path = os.environ["OBD2_STORAGE_PATH"] # PostgreSQL config if "OBD2_PG_ENABLED" in os.environ: self.postgresql.enabled = os.environ["OBD2_PG_ENABLED"].lower() == "true" if "OBD2_PG_HOST" in os.environ: self.postgresql.host = os.environ["OBD2_PG_HOST"] if "OBD2_PG_PORT" in os.environ: self.postgresql.port = int(os.environ["OBD2_PG_PORT"]) if "OBD2_PG_DATABASE" in os.environ: self.postgresql.database = os.environ["OBD2_PG_DATABASE"] if "OBD2_PG_USER" in os.environ: self.postgresql.user = os.environ["OBD2_PG_USER"] if "OBD2_PG_PASSWORD" in os.environ: self.postgresql.password = os.environ["OBD2_PG_PASSWORD"] # Flipper config if "OBD2_FLIPPER_ENABLED" in os.environ: self.flipper.enabled = os.environ["OBD2_FLIPPER_ENABLED"].lower() == "true" if "OBD2_FLIPPER_PORT" in os.environ: self.flipper.port = os.environ["OBD2_FLIPPER_PORT"] @staticmethod def _parse_hex(value) -> int: """Parse hex string or int to int.""" if isinstance(value, int): return value if isinstance(value, str): if value.startswith("0x") or value.startswith("0X"): return int(value, 16) return int(value) return value