""" Bootstrap module for IoT Bridge. Handles configuration loading, logging setup, and command-line argument parsing. """ import argparse import os import sys from pathlib import Path import structlog import yaml from config import load_config from config.loader import load_devices_from_config from config.schema import MQTTConfig from exceptions import ConfigurationError class BootstrapConfig: """Container for bootstrap configuration.""" def __init__( self, config, logger, config_path: str, config_active_path: Path, bridge_port: int, bridge_token: str | None, ): """ Initialize bootstrap configuration. Args: config: Bridge configuration (or None if no config) logger: Configured logger instance config_path: Path to main config file config_active_path: Path to persisted config file bridge_port: HTTP API port bridge_token: Optional API authentication token """ self.config = config self.logger = logger self.config_path = config_path self.config_active_path = config_active_path self.bridge_port = bridge_port self.bridge_token = bridge_token def parse_arguments() -> argparse.Namespace: """ Parse command-line arguments. Returns: Parsed arguments namespace """ parser = argparse.ArgumentParser(description="IoT MQTT Bridge for Odoo") parser.add_argument( "--config", type=str, default="config.yaml", help="Path to configuration file (default: config.yaml)", ) return parser.parse_args() def load_bridge_config(config_path: str, config_active_path: Path, logger): """ Load bridge configuration with fallback to persisted config. Priority: 1. Load base config from config_path 2. Try to load persisted devices from config_active_path 3. Merge persisted config (devices + MQTT) with base config Args: config_path: Path to main configuration file config_active_path: Path to persisted configuration file logger: Logger instance Returns: Bridge configuration or None if no config available Raises: SystemExit: If critical configuration errors occur """ # Load base configuration print(f"Loading base configuration from {config_path}") try: config = load_config(config_path) except ConfigurationError as e: print(f"ERROR: {e}") print("⚠ No config file found. Bridge will wait for config push from Odoo.") return None except Exception as e: print(f"ERROR: Failed to load config: {e}") sys.exit(1) # Try to load persisted config (devices + MQTT) if config and config_active_path.exists(): print(f"Loading persisted config from {config_active_path}") try: with open(config_active_path) as f: persisted_config = yaml.safe_load(f) # Load devices devices = load_devices_from_config(str(config_active_path)) config.devices = devices print(f"✓ Loaded {len(devices)} devices from persisted config") # Load MQTT config if present if "mqtt" in persisted_config and persisted_config["mqtt"]: mqtt_dict = persisted_config["mqtt"] config.mqtt = MQTTConfig( broker=mqtt_dict.get("broker", "localhost"), port=mqtt_dict.get("port", 1883), username=mqtt_dict.get("username", ""), password=mqtt_dict.get("password", ""), client_id=mqtt_dict.get("client_id", "iot_bridge"), keepalive=mqtt_dict.get("keepalive", 60), use_tls=mqtt_dict.get("use_tls", False), ) print(f"✓ Loaded MQTT config: {config.mqtt.broker}:{config.mqtt.port}") except Exception as e: print(f"⚠ Failed to load persisted config: {e}") print(f" Using config from {config_path}") return config def setup_logger(config): """ Setup structured logging. Args: config: Bridge configuration (or None for basic logging) Returns: Configured logger instance """ if config: from utils.logging import setup_logging return setup_logging(config.logging) else: # Basic logging without config structlog.configure( processors=[ structlog.processors.TimeStamper(fmt="iso"), structlog.processors.add_log_level, structlog.processors.JSONRenderer(), ] ) logger = structlog.get_logger() logger.warning("bridge_started_without_config", message="Waiting for config push from Odoo") return logger def get_mqtt_config(config): """ Get MQTT configuration with fallback to environment variables. Args: config: Bridge configuration (or None) Returns: MQTT configuration (Pydantic MQTTConfig) """ if config: return config.mqtt # Default MQTT config from environment variables return MQTTConfig( broker=os.getenv("MQTT_BROKER", "mosquitto"), port=int(os.getenv("MQTT_PORT", "1883")), username=os.getenv("MQTT_USERNAME"), password=os.getenv("MQTT_PASSWORD"), client_id=os.getenv("MQTT_CLIENT_ID", "iot_bridge"), use_tls=os.getenv("MQTT_USE_TLS", "false").lower() == "true", ) def bootstrap() -> BootstrapConfig: """ Bootstrap the IoT Bridge application. Handles: - Command-line argument parsing - Configuration loading (with persisted config fallback) - Logging setup - Environment variable processing Returns: BootstrapConfig with all initialization data Raises: SystemExit: If critical configuration errors occur """ # Parse arguments args = parse_arguments() # Determine config paths config_active_path = Path("/data/config-active.yaml") config_path = ( args.config if args.config != "config.yaml" else os.getenv("BRIDGE_CONFIG", "config.yaml") ) # Get HTTP server settings from environment bridge_port = int(os.getenv("BRIDGE_PORT", "8080")) bridge_token = os.getenv("BRIDGE_API_TOKEN") # Load configuration (returns None if not available) config = load_bridge_config(config_path, config_active_path, None) # Setup logging logger = setup_logger(config) # Log startup info if config: logger.info("bridge_started", config_file=config_path, devices=len(config.devices)) else: logger.warning("bridge_started_without_config", message="Waiting for config push from Odoo") return BootstrapConfig( config=config, logger=logger, config_path=config_path, config_active_path=config_active_path, bridge_port=bridge_port, bridge_token=bridge_token, )