""" Arbitration Configuration Management for Trixy This module provides comprehensive configuration management for the arbitration system, including algorithm selection, timing parameters, satellite priorities, and custom plugin-based arbitration settings. The configuration system supports: - Multiple arbitration algorithms (volume-based, distance-based, priority-based, round-robin, custom) - Configurable timing parameters (collection window, timeouts) - Room and satellite priority settings - Custom algorithm plugin integration - Runtime configuration updates - Validation and error handling Usage: from trixy_core.arbitration import ArbitrationConfig, ArbitrationAlgorithm # Create configuration with defaults config = ArbitrationConfig() # Configure algorithm config.primary_algorithm = ArbitrationAlgorithm.VOLUME_BASED config.fallback_algorithm = ArbitrationAlgorithm.PRIORITY_BASED # Set room priorities config.set_room_priority("kitchen", 1.0) config.set_room_priority("living_room", 0.8) # Configure timing config.collection_window_seconds = 1.0 config.arbitration_timeout_seconds = 2.0 """ from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Optional, Any, Union import json import os from pathlib import Path def pprint(message: str) -> None: """Arbitration config logging function.""" print(f"[ARBITRATION_CONFIG] {message}") class ArbitrationAlgorithm(Enum): """Supported arbitration algorithms.""" VOLUME_BASED = "volume_based" DISTANCE_BASED = "distance_based" PRIORITY_BASED = "priority_based" ROUND_ROBIN = "round_robin" CUSTOM = "custom" HYBRID = "hybrid" # Combination of multiple algorithms class ArbitrationMode(Enum): """Arbitration operation modes.""" STRICT = "strict" # Strict algorithm adherence ADAPTIVE = "adaptive" # Adapt based on conditions LEARNING = "learning" # Learn from user preferences EMERGENCY = "emergency" # Emergency mode (fastest response) class SatellitePriority(Enum): """Satellite priority levels.""" CRITICAL = 1.0 HIGH = 0.8 NORMAL = 0.6 LOW = 0.4 DISABLED = 0.0 @dataclass class RoomSettings: """Configuration settings for a specific room.""" priority: float = 0.6 distance_weight: float = 1.0 volume_threshold: float = 0.1 preferred_satellite: Optional[str] = None disabled: bool = False custom_settings: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary representation.""" return { 'priority': self.priority, 'distance_weight': self.distance_weight, 'volume_threshold': self.volume_threshold, 'preferred_satellite': self.preferred_satellite, 'disabled': self.disabled, 'custom_settings': self.custom_settings } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'RoomSettings': """Create from dictionary representation.""" return cls( priority=data.get('priority', 0.6), distance_weight=data.get('distance_weight', 1.0), volume_threshold=data.get('volume_threshold', 0.1), preferred_satellite=data.get('preferred_satellite'), disabled=data.get('disabled', False), custom_settings=data.get('custom_settings', {}) ) @dataclass class AlgorithmWeights: """Weights for hybrid arbitration algorithm.""" volume_weight: float = 0.5 distance_weight: float = 0.3 priority_weight: float = 0.2 confidence_weight: float = 0.1 history_weight: float = 0.05 def normalize(self) -> 'AlgorithmWeights': """Normalize weights to sum to 1.0.""" total = (self.volume_weight + self.distance_weight + self.priority_weight + self.confidence_weight + self.history_weight) if total > 0: return AlgorithmWeights( volume_weight=self.volume_weight / total, distance_weight=self.distance_weight / total, priority_weight=self.priority_weight / total, confidence_weight=self.confidence_weight / total, history_weight=self.history_weight / total ) return self def to_dict(self) -> Dict[str, float]: """Convert to dictionary representation.""" return { 'volume_weight': self.volume_weight, 'distance_weight': self.distance_weight, 'priority_weight': self.priority_weight, 'confidence_weight': self.confidence_weight, 'history_weight': self.history_weight } @classmethod def from_dict(cls, data: Dict[str, float]) -> 'AlgorithmWeights': """Create from dictionary representation.""" return cls( volume_weight=data.get('volume_weight', 0.5), distance_weight=data.get('distance_weight', 0.3), priority_weight=data.get('priority_weight', 0.2), confidence_weight=data.get('confidence_weight', 0.1), history_weight=data.get('history_weight', 0.05) ) class ArbitrationConfigError(Exception): """Base exception for arbitration configuration errors.""" pass class InvalidConfigurationError(ArbitrationConfigError): """Raised when configuration values are invalid.""" pass class ConfigurationFileError(ArbitrationConfigError): """Raised when configuration file operations fail.""" pass @dataclass class ArbitrationConfig: """ Comprehensive configuration for the arbitration system. This class manages all configuration aspects of the arbitration system, including algorithm selection, timing parameters, satellite priorities, and custom settings. """ # Algorithm configuration primary_algorithm: ArbitrationAlgorithm = ArbitrationAlgorithm.VOLUME_BASED fallback_algorithm: ArbitrationAlgorithm = ArbitrationAlgorithm.PRIORITY_BASED mode: ArbitrationMode = ArbitrationMode.ADAPTIVE # Timing configuration (in seconds) collection_window_seconds: float = 1.0 arbitration_timeout_seconds: float = 2.0 min_collection_time: float = 0.1 max_wait_time: float = 5.0 # Volume-based algorithm settings volume_threshold: float = 0.1 volume_hysteresis: float = 0.05 volume_smoothing_factor: float = 0.3 # Distance-based algorithm settings distance_threshold: float = 1.0 # meters distance_falloff: float = 0.5 use_estimated_distance: bool = True # Priority-based algorithm settings default_satellite_priority: float = 0.6 room_priorities: Dict[str, RoomSettings] = field(default_factory=dict) # Round-robin algorithm settings round_robin_reset_interval: int = 100 # Reset after N arbitrations round_robin_fairness_mode: bool = True # Hybrid algorithm settings algorithm_weights: AlgorithmWeights = field(default_factory=AlgorithmWeights) # Advanced settings enable_learning: bool = True enable_analytics: bool = True enable_history_tracking: bool = True max_history_entries: int = 1000 # Custom algorithm settings custom_algorithm_plugin: Optional[str] = None custom_algorithm_config: Dict[str, Any] = field(default_factory=dict) # Performance settings max_concurrent_arbitrations: int = 10 thread_pool_size: int = 4 enable_caching: bool = True cache_size: int = 100 # Debugging and logging enable_debug_logging: bool = False log_all_decisions: bool = True log_performance_metrics: bool = True def __post_init__(self): """Post-initialization validation and setup.""" self.validate() if not self.room_priorities: self._setup_default_room_priorities() def validate(self) -> None: """ Validate configuration values. Raises: InvalidConfigurationError: If any configuration values are invalid """ # Validate timing parameters if self.collection_window_seconds <= 0: raise InvalidConfigurationError("Collection window must be positive") if self.arbitration_timeout_seconds <= self.collection_window_seconds: raise InvalidConfigurationError( "Arbitration timeout must be greater than collection window" ) if self.min_collection_time < 0 or self.min_collection_time > self.collection_window_seconds: raise InvalidConfigurationError( "Min collection time must be between 0 and collection window" ) # Validate thresholds if not 0 <= self.volume_threshold <= 1: raise InvalidConfigurationError("Volume threshold must be between 0 and 1") if not 0 <= self.default_satellite_priority <= 1: raise InvalidConfigurationError("Default satellite priority must be between 0 and 1") # Validate performance settings if self.max_concurrent_arbitrations <= 0: raise InvalidConfigurationError("Max concurrent arbitrations must be positive") if self.thread_pool_size <= 0: raise InvalidConfigurationError("Thread pool size must be positive") # Validate hybrid algorithm weights if self.primary_algorithm == ArbitrationAlgorithm.HYBRID: self.algorithm_weights = self.algorithm_weights.normalize() pprint("Configuration validation completed successfully") def _setup_default_room_priorities(self) -> None: """Set up default room priorities.""" default_rooms = { "kitchen": RoomSettings(priority=1.0), "living_room": RoomSettings(priority=0.8), "bedroom": RoomSettings(priority=0.6), "office": RoomSettings(priority=0.7), "bathroom": RoomSettings(priority=0.4), "garage": RoomSettings(priority=0.3), } for room_id, settings in default_rooms.items(): self.room_priorities[room_id] = settings pprint(f"Set up default priorities for {len(default_rooms)} rooms") def set_room_priority(self, room_id: str, priority: float, **kwargs) -> None: """ Set priority for a specific room. Args: room_id: Room identifier priority: Priority value (0.0 to 1.0) **kwargs: Additional room settings """ if not 0 <= priority <= 1: raise InvalidConfigurationError(f"Priority must be between 0 and 1, got {priority}") if room_id not in self.room_priorities: self.room_priorities[room_id] = RoomSettings() self.room_priorities[room_id].priority = priority # Update additional settings for key, value in kwargs.items(): if hasattr(self.room_priorities[room_id], key): setattr(self.room_priorities[room_id], key, value) pprint(f"Set priority for room '{room_id}' to {priority}") def get_room_priority(self, room_id: str) -> float: """ Get priority for a specific room. Args: room_id: Room identifier Returns: float: Room priority (defaults to default_satellite_priority) """ if room_id in self.room_priorities: return self.room_priorities[room_id].priority return self.default_satellite_priority def get_room_settings(self, room_id: str) -> RoomSettings: """ Get complete settings for a specific room. Args: room_id: Room identifier Returns: RoomSettings: Room configuration settings """ if room_id not in self.room_priorities: self.room_priorities[room_id] = RoomSettings(priority=self.default_satellite_priority) return self.room_priorities[room_id] def enable_room(self, room_id: str) -> None: """Enable arbitration for a specific room.""" settings = self.get_room_settings(room_id) settings.disabled = False pprint(f"Enabled arbitration for room '{room_id}'") def disable_room(self, room_id: str) -> None: """Disable arbitration for a specific room.""" settings = self.get_room_settings(room_id) settings.disabled = True pprint(f"Disabled arbitration for room '{room_id}'") def is_room_enabled(self, room_id: str) -> bool: """Check if arbitration is enabled for a specific room.""" settings = self.get_room_settings(room_id) return not settings.disabled def set_algorithm_weights(self, **weights) -> None: """ Set weights for hybrid algorithm. Args: **weights: Weight values (volume_weight, distance_weight, etc.) """ for key, value in weights.items(): if hasattr(self.algorithm_weights, key): setattr(self.algorithm_weights, key, value) self.algorithm_weights = self.algorithm_weights.normalize() pprint(f"Updated algorithm weights: {self.algorithm_weights.to_dict()}") def to_dict(self) -> Dict[str, Any]: """ Convert configuration to dictionary representation. Returns: Dict[str, Any]: Configuration as dictionary """ return { 'primary_algorithm': self.primary_algorithm.value, 'fallback_algorithm': self.fallback_algorithm.value, 'mode': self.mode.value, 'collection_window_seconds': self.collection_window_seconds, 'arbitration_timeout_seconds': self.arbitration_timeout_seconds, 'min_collection_time': self.min_collection_time, 'max_wait_time': self.max_wait_time, 'volume_threshold': self.volume_threshold, 'volume_hysteresis': self.volume_hysteresis, 'volume_smoothing_factor': self.volume_smoothing_factor, 'distance_threshold': self.distance_threshold, 'distance_falloff': self.distance_falloff, 'use_estimated_distance': self.use_estimated_distance, 'default_satellite_priority': self.default_satellite_priority, 'room_priorities': { room_id: settings.to_dict() for room_id, settings in self.room_priorities.items() }, 'round_robin_reset_interval': self.round_robin_reset_interval, 'round_robin_fairness_mode': self.round_robin_fairness_mode, 'algorithm_weights': self.algorithm_weights.to_dict(), 'enable_learning': self.enable_learning, 'enable_analytics': self.enable_analytics, 'enable_history_tracking': self.enable_history_tracking, 'max_history_entries': self.max_history_entries, 'custom_algorithm_plugin': self.custom_algorithm_plugin, 'custom_algorithm_config': self.custom_algorithm_config, 'max_concurrent_arbitrations': self.max_concurrent_arbitrations, 'thread_pool_size': self.thread_pool_size, 'enable_caching': self.enable_caching, 'cache_size': self.cache_size, 'enable_debug_logging': self.enable_debug_logging, 'log_all_decisions': self.log_all_decisions, 'log_performance_metrics': self.log_performance_metrics, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ArbitrationConfig': """ Create configuration from dictionary representation. Args: data: Configuration dictionary Returns: ArbitrationConfig: New configuration instance """ config = cls() # Update basic fields for key, value in data.items(): if key == 'primary_algorithm': config.primary_algorithm = ArbitrationAlgorithm(value) elif key == 'fallback_algorithm': config.fallback_algorithm = ArbitrationAlgorithm(value) elif key == 'mode': config.mode = ArbitrationMode(value) elif key == 'room_priorities': config.room_priorities = { room_id: RoomSettings.from_dict(settings_data) for room_id, settings_data in value.items() } elif key == 'algorithm_weights': config.algorithm_weights = AlgorithmWeights.from_dict(value) elif hasattr(config, key): setattr(config, key, value) config.validate() return config def save_to_file(self, file_path: Union[str, Path]) -> None: """ Save configuration to JSON file. Args: file_path: Path to save configuration file Raises: ConfigurationFileError: If file cannot be saved """ try: file_path = Path(file_path) file_path.parent.mkdir(parents=True, exist_ok=True) with open(file_path, 'w') as f: json.dump(self.to_dict(), f, indent=2, sort_keys=True) pprint(f"Configuration saved to {file_path}") except Exception as e: raise ConfigurationFileError(f"Failed to save configuration: {e}") @classmethod def load_from_file(cls, file_path: Union[str, Path]) -> 'ArbitrationConfig': """ Load configuration from JSON file. Args: file_path: Path to configuration file Returns: ArbitrationConfig: Loaded configuration Raises: ConfigurationFileError: If file cannot be loaded """ try: with open(file_path, 'r') as f: data = json.load(f) config = cls.from_dict(data) pprint(f"Configuration loaded from {file_path}") return config except FileNotFoundError: raise ConfigurationFileError(f"Configuration file not found: {file_path}") except json.JSONDecodeError as e: raise ConfigurationFileError(f"Invalid JSON in configuration file: {e}") except Exception as e: raise ConfigurationFileError(f"Failed to load configuration: {e}") def update_from_dict(self, updates: Dict[str, Any]) -> None: """ Update configuration with new values. Args: updates: Dictionary of configuration updates """ current_dict = self.to_dict() current_dict.update(updates) new_config = self.from_dict(current_dict) # Copy all attributes from new config for key, value in new_config.__dict__.items(): setattr(self, key, value) pprint(f"Configuration updated with {len(updates)} changes") def reset_to_defaults(self) -> None: """Reset configuration to default values.""" default_config = ArbitrationConfig() for key, value in default_config.__dict__.items(): setattr(self, key, value) pprint("Configuration reset to defaults") def get_algorithm_config(self, algorithm: ArbitrationAlgorithm) -> Dict[str, Any]: """ Get configuration specific to an algorithm. Args: algorithm: Algorithm to get configuration for Returns: Dict[str, Any]: Algorithm-specific configuration """ if algorithm == ArbitrationAlgorithm.VOLUME_BASED: return { 'threshold': self.volume_threshold, 'hysteresis': self.volume_hysteresis, 'smoothing_factor': self.volume_smoothing_factor, } elif algorithm == ArbitrationAlgorithm.DISTANCE_BASED: return { 'threshold': self.distance_threshold, 'falloff': self.distance_falloff, 'use_estimated': self.use_estimated_distance, } elif algorithm == ArbitrationAlgorithm.PRIORITY_BASED: return { 'default_priority': self.default_satellite_priority, 'room_priorities': { room_id: settings.priority for room_id, settings in self.room_priorities.items() }, } elif algorithm == ArbitrationAlgorithm.ROUND_ROBIN: return { 'reset_interval': self.round_robin_reset_interval, 'fairness_mode': self.round_robin_fairness_mode, } elif algorithm == ArbitrationAlgorithm.HYBRID: return { 'weights': self.algorithm_weights.to_dict(), } elif algorithm == ArbitrationAlgorithm.CUSTOM: return { 'plugin': self.custom_algorithm_plugin, 'config': self.custom_algorithm_config, } else: return {} def create_default_config() -> ArbitrationConfig: """ Create a default arbitration configuration. Returns: ArbitrationConfig: Default configuration instance """ config = ArbitrationConfig() pprint("Created default arbitration configuration") return config def load_config_from_file(file_path: Union[str, Path]) -> ArbitrationConfig: """ Load arbitration configuration from file. Args: file_path: Path to configuration file Returns: ArbitrationConfig: Loaded configuration """ return ArbitrationConfig.load_from_file(file_path) def create_config_with_overrides(**overrides) -> ArbitrationConfig: """ Create configuration with specific overrides. Args: **overrides: Configuration overrides Returns: ArbitrationConfig: Configuration with overrides applied """ config = ArbitrationConfig() config.update_from_dict(overrides) return config