Alle Exception-Klassen haben jetzt sinnvolle __init__-Methoden:
- ConfigurationError: path Parameter für Config-Dateipfad
- ConfigValidationError: field + value für fehlerhafte Felder
- ConnectionError: service Parameter (mqtt/odoo)
- MQTTConnectionError: broker + port Parameter
- DeviceError: device_id Parameter
- ValidationError: field + value für Validierungsfehler
Vorher: Klassen hatten nur 'pass' (technisch korrekt, aber wenig nützlich)
Nachher: Strukturierte Fehlerkontext-Erfassung mit dedizierten Attributen
Beispiel:
# Alt: raise ConfigurationError('File not found', details={'path': ...})
# Neu: raise ConfigurationError('File not found', path='/etc/config.yaml')
Angepasst:
- config/loader.py: Nutzt neuen path-Parameter statt details-Dict
- Alle bestehenden Aufrufe bleiben kompatibel (backward-compatible)
Test: python3 -c 'from exceptions import *; e = MQTTConnectionError(...)'
380 lines
10 KiB
Python
380 lines
10 KiB
Python
"""
|
|
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
|
|
"""
|
|
|
|
|
|
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, 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 = {}
|
|
if topic:
|
|
details["topic"] = topic
|
|
if payload:
|
|
details["payload_keys"] = list(payload.keys()) if isinstance(payload, dict) else None
|
|
super().__init__(message, 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)
|