""" Custom exceptions for IoT Bridge. This module defines a hierarchy of exceptions for better error handling and debugging. All exceptions inherit from BridgeError base class. Exception Hierarchy: BridgeError (base) ├── ConfigurationError ├── ConnectionError │ ├── MQTTConnectionError │ └── OdooAPIError ├── DeviceError │ ├── DeviceNotFoundError │ └── ParserError └── ValidationError └── ConfigValidationError """ from typing import Any class BridgeError(Exception): """ Base exception for all IoT Bridge errors. All custom exceptions should inherit from this class to enable catching all bridge-specific errors with a single except clause. Attributes: message: Human-readable error description details: Optional dictionary with additional context """ def __init__(self, message: str, details: dict | None = None): """ Initialize bridge error. Args: message: Error description details: Optional context dict (e.g., {"device_id": "xyz"}) """ self.message = message self.details = details or {} super().__init__(message) def __str__(self) -> str: """Return formatted error message with details.""" if self.details: details_str = ", ".join(f"{k}={v}" for k, v in self.details.items()) return f"{self.message} [{details_str}]" return self.message # ============================================================================ # Configuration Errors # ============================================================================ class ConfigurationError(BridgeError): """ Raised when configuration is invalid or missing. Examples: - Missing required configuration file - Invalid YAML syntax - Missing required configuration fields """ def __init__(self, message: str, path: str | None = None, details: dict | None = None): """ Initialize configuration error. Args: message: Error description path: Optional path to configuration file details: Additional context """ self.path = path full_details = details or {} if path: full_details["path"] = path super().__init__(message, full_details) class ConfigValidationError(ConfigurationError): """ Raised when configuration validation fails. Examples: - Invalid port number (e.g., port=-1) - Invalid URL format - Invalid device configuration """ def __init__( self, message: str, field: str | None = None, value: Any = None, path: str | None = None, details: dict | None = None, ): """ Initialize config validation error. Args: message: Error description field: Name of the invalid field value: The invalid value path: Optional path to configuration file details: Additional context """ self.field = field self.value = value full_details = details or {} if field: full_details["field"] = field if value is not None: full_details["value"] = value super().__init__(message, path, full_details) # ============================================================================ # Connection Errors # ============================================================================ class ConnectionError(BridgeError): """ Base class for connection-related errors. Examples: - Network timeouts - Connection refused - Authentication failures """ def __init__(self, message: str, service: str | None = None, details: dict | None = None): """ Initialize connection error. Args: message: Error description service: Service name (e.g., 'mqtt', 'odoo') details: Additional context """ self.service = service full_details = details or {} if service: full_details["service"] = service super().__init__(message, full_details) class MQTTConnectionError(ConnectionError): """ Raised when MQTT broker connection fails. Examples: - Cannot connect to MQTT broker - Authentication failed - Connection lost unexpectedly - Subscribe/publish failures """ def __init__( self, message: str, broker: str | None = None, port: int | None = None, details: dict | None = None, ): """ Initialize MQTT connection error. Args: message: Error description broker: MQTT broker address port: MQTT broker port details: Additional context """ self.broker = broker self.port = port full_details = details or {} if broker: full_details["broker"] = broker if port: full_details["port"] = port super().__init__(message, service="mqtt", details=full_details) class OdooAPIError(ConnectionError): """ Raised when Odoo API communication fails. Examples: - HTTP request failures - Authentication errors - Invalid API responses - Network timeouts """ def __init__( self, message: str, status_code: int | None = None, response: dict | None = None, details: dict | None = None, ): """ Initialize Odoo API error. Args: message: Error description status_code: HTTP status code (if applicable) response: API response body (if available) details: Additional context """ self.status_code = status_code self.response = response full_details = details or {} if status_code: full_details["status_code"] = status_code if response: full_details["response"] = response super().__init__(message, service="odoo", details=full_details) # ============================================================================ # Device Errors # ============================================================================ class DeviceError(BridgeError): """ Base class for device-related errors. Examples: - Device not configured - Parser errors - Invalid device state """ def __init__(self, message: str, device_id: str | None = None, details: dict | None = None): """ Initialize device error. Args: message: Error description device_id: Optional device identifier details: Additional context """ self.device_id = device_id full_details = details or {} if device_id: full_details["device_id"] = device_id super().__init__(message, full_details) class DeviceNotFoundError(DeviceError): """ Raised when a device is not found in configuration. Examples: - Received MQTT message for unconfigured device - Device removed during runtime """ def __init__(self, device_id: str, message: str | None = None): """ Initialize device not found error. Args: device_id: The device identifier that was not found message: Optional custom message """ self.device_id = device_id msg = message or f"Device not found: {device_id}" super().__init__(msg, device_id=device_id) class ParserError(DeviceError): """ Raised when message parsing fails. Examples: - Invalid message format - Unsupported device type - Missing required fields in payload """ def __init__(self, message: str, topic: str | None = None, payload: dict | None = None): """ Initialize parser error. Args: message: Error description topic: MQTT topic (if applicable) payload: Message payload (if applicable) """ self.topic = topic self.payload = payload details: dict[str, Any] = {} if topic: details["topic"] = topic if payload: details["payload_keys"] = list(payload.keys()) if isinstance(payload, dict) else None super().__init__(message, details=details) # ============================================================================ # Validation Errors # ============================================================================ class ValidationError(BridgeError): """ Base class for validation errors. Examples: - Invalid input data - Schema validation failures - Business rule violations """ def __init__( self, message: str, field: str | None = None, value: Any = None, details: dict | None = None, ): """ Initialize validation error. Args: message: Error description field: Name of the field that failed validation value: The invalid value details: Additional context """ self.field = field self.value = value full_details = details or {} if field: full_details["field"] = field if value is not None: full_details["value"] = value super().__init__(message, full_details) # ============================================================================ # Utility Functions # ============================================================================ def wrap_exception(exc: Exception, context: str) -> BridgeError: """ Wrap a generic exception in a BridgeError with context. This is useful for converting standard library exceptions (e.g., OSError, ValueError) into BridgeError instances while preserving the original error message and adding context. Args: exc: The original exception context: Description of what was being done when error occurred Returns: BridgeError instance wrapping the original exception Example: try: with open(config_file) as f: config = yaml.load(f) except (OSError, yaml.YAMLError) as e: raise wrap_exception(e, f"Loading config from {config_file}") """ message = f"{context}: {str(exc)}" details = {"original_exception": type(exc).__name__, "original_message": str(exc)} return BridgeError(message, details)