Complete Pydantic V2 migration and config modernization: api/models.py: - Migrated @validator → @field_validator (Pydantic V2) - Added @classmethod decorators (required in V2) - Updated validator signatures: (cls, v, values) → (cls, v, info) - Consolidated unique validators into one method - Fixed: 3 PydanticDeprecatedSince20 warnings ✅ config/schema.py: - Migrated from @dataclass → Pydantic BaseModel - Added Field() with validation constraints: * Port: ge=1, le=65535 * Floats: gt=0, ge=0 * Strings: min_length constraints - Added descriptive field descriptions - Better type safety and runtime validation core/bootstrap.py: - Replaced SimpleNamespace → MQTTConfig (Pydantic) - Now returns properly typed Pydantic models - Better IDE autocomplete and type checking Benefits: ✅ No more Pydantic V1 deprecation warnings ✅ Runtime validation for all config fields ✅ Better error messages on invalid config ✅ Type-safe config throughout the application ✅ Automatic validation (port ranges, positive numbers, etc.) ✅ Prepared for Pydantic V3 migration Migration verified: - Config loading works: load_config('config.example.yaml') ✓ - Bootstrap works with Pydantic models ✓ - Field validation works (tested port ranges, thresholds) ✓ - All existing functionality preserved ✓ Test: python3 -c 'from core.bootstrap import bootstrap; bootstrap()' ✅ Works perfectly with new Pydantic V2 models See OPTIMIZATION_PLAN.md Phase 2.3 for details.
235 lines
6.9 KiB
Python
235 lines
6.9 KiB
Python
"""
|
|
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,
|
|
)
|