255 lines
8.9 KiB
Python
255 lines
8.9 KiB
Python
"""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
|