arbitration_config.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. """
  2. Arbitration Configuration Management for Trixy
  3. This module provides comprehensive configuration management for the arbitration system,
  4. including algorithm selection, timing parameters, satellite priorities, and custom
  5. plugin-based arbitration settings.
  6. The configuration system supports:
  7. - Multiple arbitration algorithms (volume-based, distance-based, priority-based, round-robin, custom)
  8. - Configurable timing parameters (collection window, timeouts)
  9. - Room and satellite priority settings
  10. - Custom algorithm plugin integration
  11. - Runtime configuration updates
  12. - Validation and error handling
  13. Usage:
  14. from trixy_core.arbitration import ArbitrationConfig, ArbitrationAlgorithm
  15. # Create configuration with defaults
  16. config = ArbitrationConfig()
  17. # Configure algorithm
  18. config.primary_algorithm = ArbitrationAlgorithm.VOLUME_BASED
  19. config.fallback_algorithm = ArbitrationAlgorithm.PRIORITY_BASED
  20. # Set room priorities
  21. config.set_room_priority("kitchen", 1.0)
  22. config.set_room_priority("living_room", 0.8)
  23. # Configure timing
  24. config.collection_window_seconds = 1.0
  25. config.arbitration_timeout_seconds = 2.0
  26. """
  27. from dataclasses import dataclass, field
  28. from enum import Enum
  29. from typing import Dict, List, Optional, Any, Union
  30. import json
  31. import os
  32. from pathlib import Path
  33. def pprint(message: str) -> None:
  34. """Arbitration config logging function."""
  35. print(f"[ARBITRATION_CONFIG] {message}")
  36. class ArbitrationAlgorithm(Enum):
  37. """Supported arbitration algorithms."""
  38. VOLUME_BASED = "volume_based"
  39. DISTANCE_BASED = "distance_based"
  40. PRIORITY_BASED = "priority_based"
  41. ROUND_ROBIN = "round_robin"
  42. CUSTOM = "custom"
  43. HYBRID = "hybrid" # Combination of multiple algorithms
  44. class ArbitrationMode(Enum):
  45. """Arbitration operation modes."""
  46. STRICT = "strict" # Strict algorithm adherence
  47. ADAPTIVE = "adaptive" # Adapt based on conditions
  48. LEARNING = "learning" # Learn from user preferences
  49. EMERGENCY = "emergency" # Emergency mode (fastest response)
  50. class SatellitePriority(Enum):
  51. """Satellite priority levels."""
  52. CRITICAL = 1.0
  53. HIGH = 0.8
  54. NORMAL = 0.6
  55. LOW = 0.4
  56. DISABLED = 0.0
  57. @dataclass
  58. class RoomSettings:
  59. """Configuration settings for a specific room."""
  60. priority: float = 0.6
  61. distance_weight: float = 1.0
  62. volume_threshold: float = 0.1
  63. preferred_satellite: Optional[str] = None
  64. disabled: bool = False
  65. custom_settings: Dict[str, Any] = field(default_factory=dict)
  66. def to_dict(self) -> Dict[str, Any]:
  67. """Convert to dictionary representation."""
  68. return {
  69. 'priority': self.priority,
  70. 'distance_weight': self.distance_weight,
  71. 'volume_threshold': self.volume_threshold,
  72. 'preferred_satellite': self.preferred_satellite,
  73. 'disabled': self.disabled,
  74. 'custom_settings': self.custom_settings
  75. }
  76. @classmethod
  77. def from_dict(cls, data: Dict[str, Any]) -> 'RoomSettings':
  78. """Create from dictionary representation."""
  79. return cls(
  80. priority=data.get('priority', 0.6),
  81. distance_weight=data.get('distance_weight', 1.0),
  82. volume_threshold=data.get('volume_threshold', 0.1),
  83. preferred_satellite=data.get('preferred_satellite'),
  84. disabled=data.get('disabled', False),
  85. custom_settings=data.get('custom_settings', {})
  86. )
  87. @dataclass
  88. class AlgorithmWeights:
  89. """Weights for hybrid arbitration algorithm."""
  90. volume_weight: float = 0.5
  91. distance_weight: float = 0.3
  92. priority_weight: float = 0.2
  93. confidence_weight: float = 0.1
  94. history_weight: float = 0.05
  95. def normalize(self) -> 'AlgorithmWeights':
  96. """Normalize weights to sum to 1.0."""
  97. total = (self.volume_weight + self.distance_weight +
  98. self.priority_weight + self.confidence_weight + self.history_weight)
  99. if total > 0:
  100. return AlgorithmWeights(
  101. volume_weight=self.volume_weight / total,
  102. distance_weight=self.distance_weight / total,
  103. priority_weight=self.priority_weight / total,
  104. confidence_weight=self.confidence_weight / total,
  105. history_weight=self.history_weight / total
  106. )
  107. return self
  108. def to_dict(self) -> Dict[str, float]:
  109. """Convert to dictionary representation."""
  110. return {
  111. 'volume_weight': self.volume_weight,
  112. 'distance_weight': self.distance_weight,
  113. 'priority_weight': self.priority_weight,
  114. 'confidence_weight': self.confidence_weight,
  115. 'history_weight': self.history_weight
  116. }
  117. @classmethod
  118. def from_dict(cls, data: Dict[str, float]) -> 'AlgorithmWeights':
  119. """Create from dictionary representation."""
  120. return cls(
  121. volume_weight=data.get('volume_weight', 0.5),
  122. distance_weight=data.get('distance_weight', 0.3),
  123. priority_weight=data.get('priority_weight', 0.2),
  124. confidence_weight=data.get('confidence_weight', 0.1),
  125. history_weight=data.get('history_weight', 0.05)
  126. )
  127. class ArbitrationConfigError(Exception):
  128. """Base exception for arbitration configuration errors."""
  129. pass
  130. class InvalidConfigurationError(ArbitrationConfigError):
  131. """Raised when configuration values are invalid."""
  132. pass
  133. class ConfigurationFileError(ArbitrationConfigError):
  134. """Raised when configuration file operations fail."""
  135. pass
  136. @dataclass
  137. class ArbitrationConfig:
  138. """
  139. Comprehensive configuration for the arbitration system.
  140. This class manages all configuration aspects of the arbitration system,
  141. including algorithm selection, timing parameters, satellite priorities,
  142. and custom settings.
  143. """
  144. # Algorithm configuration
  145. primary_algorithm: ArbitrationAlgorithm = ArbitrationAlgorithm.VOLUME_BASED
  146. fallback_algorithm: ArbitrationAlgorithm = ArbitrationAlgorithm.PRIORITY_BASED
  147. mode: ArbitrationMode = ArbitrationMode.ADAPTIVE
  148. # Timing configuration (in seconds)
  149. collection_window_seconds: float = 1.0
  150. arbitration_timeout_seconds: float = 2.0
  151. min_collection_time: float = 0.1
  152. max_wait_time: float = 5.0
  153. # Volume-based algorithm settings
  154. volume_threshold: float = 0.1
  155. volume_hysteresis: float = 0.05
  156. volume_smoothing_factor: float = 0.3
  157. # Distance-based algorithm settings
  158. distance_threshold: float = 1.0 # meters
  159. distance_falloff: float = 0.5
  160. use_estimated_distance: bool = True
  161. # Priority-based algorithm settings
  162. default_satellite_priority: float = 0.6
  163. room_priorities: Dict[str, RoomSettings] = field(default_factory=dict)
  164. # Round-robin algorithm settings
  165. round_robin_reset_interval: int = 100 # Reset after N arbitrations
  166. round_robin_fairness_mode: bool = True
  167. # Hybrid algorithm settings
  168. algorithm_weights: AlgorithmWeights = field(default_factory=AlgorithmWeights)
  169. # Advanced settings
  170. enable_learning: bool = True
  171. enable_analytics: bool = True
  172. enable_history_tracking: bool = True
  173. max_history_entries: int = 1000
  174. # Custom algorithm settings
  175. custom_algorithm_plugin: Optional[str] = None
  176. custom_algorithm_config: Dict[str, Any] = field(default_factory=dict)
  177. # Performance settings
  178. max_concurrent_arbitrations: int = 10
  179. thread_pool_size: int = 4
  180. enable_caching: bool = True
  181. cache_size: int = 100
  182. # Debugging and logging
  183. enable_debug_logging: bool = False
  184. log_all_decisions: bool = True
  185. log_performance_metrics: bool = True
  186. def __post_init__(self):
  187. """Post-initialization validation and setup."""
  188. self.validate()
  189. if not self.room_priorities:
  190. self._setup_default_room_priorities()
  191. def validate(self) -> None:
  192. """
  193. Validate configuration values.
  194. Raises:
  195. InvalidConfigurationError: If any configuration values are invalid
  196. """
  197. # Validate timing parameters
  198. if self.collection_window_seconds <= 0:
  199. raise InvalidConfigurationError("Collection window must be positive")
  200. if self.arbitration_timeout_seconds <= self.collection_window_seconds:
  201. raise InvalidConfigurationError(
  202. "Arbitration timeout must be greater than collection window"
  203. )
  204. if self.min_collection_time < 0 or self.min_collection_time > self.collection_window_seconds:
  205. raise InvalidConfigurationError(
  206. "Min collection time must be between 0 and collection window"
  207. )
  208. # Validate thresholds
  209. if not 0 <= self.volume_threshold <= 1:
  210. raise InvalidConfigurationError("Volume threshold must be between 0 and 1")
  211. if not 0 <= self.default_satellite_priority <= 1:
  212. raise InvalidConfigurationError("Default satellite priority must be between 0 and 1")
  213. # Validate performance settings
  214. if self.max_concurrent_arbitrations <= 0:
  215. raise InvalidConfigurationError("Max concurrent arbitrations must be positive")
  216. if self.thread_pool_size <= 0:
  217. raise InvalidConfigurationError("Thread pool size must be positive")
  218. # Validate hybrid algorithm weights
  219. if self.primary_algorithm == ArbitrationAlgorithm.HYBRID:
  220. self.algorithm_weights = self.algorithm_weights.normalize()
  221. pprint("Configuration validation completed successfully")
  222. def _setup_default_room_priorities(self) -> None:
  223. """Set up default room priorities."""
  224. default_rooms = {
  225. "kitchen": RoomSettings(priority=1.0),
  226. "living_room": RoomSettings(priority=0.8),
  227. "bedroom": RoomSettings(priority=0.6),
  228. "office": RoomSettings(priority=0.7),
  229. "bathroom": RoomSettings(priority=0.4),
  230. "garage": RoomSettings(priority=0.3),
  231. }
  232. for room_id, settings in default_rooms.items():
  233. self.room_priorities[room_id] = settings
  234. pprint(f"Set up default priorities for {len(default_rooms)} rooms")
  235. def set_room_priority(self, room_id: str, priority: float, **kwargs) -> None:
  236. """
  237. Set priority for a specific room.
  238. Args:
  239. room_id: Room identifier
  240. priority: Priority value (0.0 to 1.0)
  241. **kwargs: Additional room settings
  242. """
  243. if not 0 <= priority <= 1:
  244. raise InvalidConfigurationError(f"Priority must be between 0 and 1, got {priority}")
  245. if room_id not in self.room_priorities:
  246. self.room_priorities[room_id] = RoomSettings()
  247. self.room_priorities[room_id].priority = priority
  248. # Update additional settings
  249. for key, value in kwargs.items():
  250. if hasattr(self.room_priorities[room_id], key):
  251. setattr(self.room_priorities[room_id], key, value)
  252. pprint(f"Set priority for room '{room_id}' to {priority}")
  253. def get_room_priority(self, room_id: str) -> float:
  254. """
  255. Get priority for a specific room.
  256. Args:
  257. room_id: Room identifier
  258. Returns:
  259. float: Room priority (defaults to default_satellite_priority)
  260. """
  261. if room_id in self.room_priorities:
  262. return self.room_priorities[room_id].priority
  263. return self.default_satellite_priority
  264. def get_room_settings(self, room_id: str) -> RoomSettings:
  265. """
  266. Get complete settings for a specific room.
  267. Args:
  268. room_id: Room identifier
  269. Returns:
  270. RoomSettings: Room configuration settings
  271. """
  272. if room_id not in self.room_priorities:
  273. self.room_priorities[room_id] = RoomSettings(priority=self.default_satellite_priority)
  274. return self.room_priorities[room_id]
  275. def enable_room(self, room_id: str) -> None:
  276. """Enable arbitration for a specific room."""
  277. settings = self.get_room_settings(room_id)
  278. settings.disabled = False
  279. pprint(f"Enabled arbitration for room '{room_id}'")
  280. def disable_room(self, room_id: str) -> None:
  281. """Disable arbitration for a specific room."""
  282. settings = self.get_room_settings(room_id)
  283. settings.disabled = True
  284. pprint(f"Disabled arbitration for room '{room_id}'")
  285. def is_room_enabled(self, room_id: str) -> bool:
  286. """Check if arbitration is enabled for a specific room."""
  287. settings = self.get_room_settings(room_id)
  288. return not settings.disabled
  289. def set_algorithm_weights(self, **weights) -> None:
  290. """
  291. Set weights for hybrid algorithm.
  292. Args:
  293. **weights: Weight values (volume_weight, distance_weight, etc.)
  294. """
  295. for key, value in weights.items():
  296. if hasattr(self.algorithm_weights, key):
  297. setattr(self.algorithm_weights, key, value)
  298. self.algorithm_weights = self.algorithm_weights.normalize()
  299. pprint(f"Updated algorithm weights: {self.algorithm_weights.to_dict()}")
  300. def to_dict(self) -> Dict[str, Any]:
  301. """
  302. Convert configuration to dictionary representation.
  303. Returns:
  304. Dict[str, Any]: Configuration as dictionary
  305. """
  306. return {
  307. 'primary_algorithm': self.primary_algorithm.value,
  308. 'fallback_algorithm': self.fallback_algorithm.value,
  309. 'mode': self.mode.value,
  310. 'collection_window_seconds': self.collection_window_seconds,
  311. 'arbitration_timeout_seconds': self.arbitration_timeout_seconds,
  312. 'min_collection_time': self.min_collection_time,
  313. 'max_wait_time': self.max_wait_time,
  314. 'volume_threshold': self.volume_threshold,
  315. 'volume_hysteresis': self.volume_hysteresis,
  316. 'volume_smoothing_factor': self.volume_smoothing_factor,
  317. 'distance_threshold': self.distance_threshold,
  318. 'distance_falloff': self.distance_falloff,
  319. 'use_estimated_distance': self.use_estimated_distance,
  320. 'default_satellite_priority': self.default_satellite_priority,
  321. 'room_priorities': {
  322. room_id: settings.to_dict()
  323. for room_id, settings in self.room_priorities.items()
  324. },
  325. 'round_robin_reset_interval': self.round_robin_reset_interval,
  326. 'round_robin_fairness_mode': self.round_robin_fairness_mode,
  327. 'algorithm_weights': self.algorithm_weights.to_dict(),
  328. 'enable_learning': self.enable_learning,
  329. 'enable_analytics': self.enable_analytics,
  330. 'enable_history_tracking': self.enable_history_tracking,
  331. 'max_history_entries': self.max_history_entries,
  332. 'custom_algorithm_plugin': self.custom_algorithm_plugin,
  333. 'custom_algorithm_config': self.custom_algorithm_config,
  334. 'max_concurrent_arbitrations': self.max_concurrent_arbitrations,
  335. 'thread_pool_size': self.thread_pool_size,
  336. 'enable_caching': self.enable_caching,
  337. 'cache_size': self.cache_size,
  338. 'enable_debug_logging': self.enable_debug_logging,
  339. 'log_all_decisions': self.log_all_decisions,
  340. 'log_performance_metrics': self.log_performance_metrics,
  341. }
  342. @classmethod
  343. def from_dict(cls, data: Dict[str, Any]) -> 'ArbitrationConfig':
  344. """
  345. Create configuration from dictionary representation.
  346. Args:
  347. data: Configuration dictionary
  348. Returns:
  349. ArbitrationConfig: New configuration instance
  350. """
  351. config = cls()
  352. # Update basic fields
  353. for key, value in data.items():
  354. if key == 'primary_algorithm':
  355. config.primary_algorithm = ArbitrationAlgorithm(value)
  356. elif key == 'fallback_algorithm':
  357. config.fallback_algorithm = ArbitrationAlgorithm(value)
  358. elif key == 'mode':
  359. config.mode = ArbitrationMode(value)
  360. elif key == 'room_priorities':
  361. config.room_priorities = {
  362. room_id: RoomSettings.from_dict(settings_data)
  363. for room_id, settings_data in value.items()
  364. }
  365. elif key == 'algorithm_weights':
  366. config.algorithm_weights = AlgorithmWeights.from_dict(value)
  367. elif hasattr(config, key):
  368. setattr(config, key, value)
  369. config.validate()
  370. return config
  371. def save_to_file(self, file_path: Union[str, Path]) -> None:
  372. """
  373. Save configuration to JSON file.
  374. Args:
  375. file_path: Path to save configuration file
  376. Raises:
  377. ConfigurationFileError: If file cannot be saved
  378. """
  379. try:
  380. file_path = Path(file_path)
  381. file_path.parent.mkdir(parents=True, exist_ok=True)
  382. with open(file_path, 'w') as f:
  383. json.dump(self.to_dict(), f, indent=2, sort_keys=True)
  384. pprint(f"Configuration saved to {file_path}")
  385. except Exception as e:
  386. raise ConfigurationFileError(f"Failed to save configuration: {e}")
  387. @classmethod
  388. def load_from_file(cls, file_path: Union[str, Path]) -> 'ArbitrationConfig':
  389. """
  390. Load configuration from JSON file.
  391. Args:
  392. file_path: Path to configuration file
  393. Returns:
  394. ArbitrationConfig: Loaded configuration
  395. Raises:
  396. ConfigurationFileError: If file cannot be loaded
  397. """
  398. try:
  399. with open(file_path, 'r') as f:
  400. data = json.load(f)
  401. config = cls.from_dict(data)
  402. pprint(f"Configuration loaded from {file_path}")
  403. return config
  404. except FileNotFoundError:
  405. raise ConfigurationFileError(f"Configuration file not found: {file_path}")
  406. except json.JSONDecodeError as e:
  407. raise ConfigurationFileError(f"Invalid JSON in configuration file: {e}")
  408. except Exception as e:
  409. raise ConfigurationFileError(f"Failed to load configuration: {e}")
  410. def update_from_dict(self, updates: Dict[str, Any]) -> None:
  411. """
  412. Update configuration with new values.
  413. Args:
  414. updates: Dictionary of configuration updates
  415. """
  416. current_dict = self.to_dict()
  417. current_dict.update(updates)
  418. new_config = self.from_dict(current_dict)
  419. # Copy all attributes from new config
  420. for key, value in new_config.__dict__.items():
  421. setattr(self, key, value)
  422. pprint(f"Configuration updated with {len(updates)} changes")
  423. def reset_to_defaults(self) -> None:
  424. """Reset configuration to default values."""
  425. default_config = ArbitrationConfig()
  426. for key, value in default_config.__dict__.items():
  427. setattr(self, key, value)
  428. pprint("Configuration reset to defaults")
  429. def get_algorithm_config(self, algorithm: ArbitrationAlgorithm) -> Dict[str, Any]:
  430. """
  431. Get configuration specific to an algorithm.
  432. Args:
  433. algorithm: Algorithm to get configuration for
  434. Returns:
  435. Dict[str, Any]: Algorithm-specific configuration
  436. """
  437. if algorithm == ArbitrationAlgorithm.VOLUME_BASED:
  438. return {
  439. 'threshold': self.volume_threshold,
  440. 'hysteresis': self.volume_hysteresis,
  441. 'smoothing_factor': self.volume_smoothing_factor,
  442. }
  443. elif algorithm == ArbitrationAlgorithm.DISTANCE_BASED:
  444. return {
  445. 'threshold': self.distance_threshold,
  446. 'falloff': self.distance_falloff,
  447. 'use_estimated': self.use_estimated_distance,
  448. }
  449. elif algorithm == ArbitrationAlgorithm.PRIORITY_BASED:
  450. return {
  451. 'default_priority': self.default_satellite_priority,
  452. 'room_priorities': {
  453. room_id: settings.priority
  454. for room_id, settings in self.room_priorities.items()
  455. },
  456. }
  457. elif algorithm == ArbitrationAlgorithm.ROUND_ROBIN:
  458. return {
  459. 'reset_interval': self.round_robin_reset_interval,
  460. 'fairness_mode': self.round_robin_fairness_mode,
  461. }
  462. elif algorithm == ArbitrationAlgorithm.HYBRID:
  463. return {
  464. 'weights': self.algorithm_weights.to_dict(),
  465. }
  466. elif algorithm == ArbitrationAlgorithm.CUSTOM:
  467. return {
  468. 'plugin': self.custom_algorithm_plugin,
  469. 'config': self.custom_algorithm_config,
  470. }
  471. else:
  472. return {}
  473. def create_default_config() -> ArbitrationConfig:
  474. """
  475. Create a default arbitration configuration.
  476. Returns:
  477. ArbitrationConfig: Default configuration instance
  478. """
  479. config = ArbitrationConfig()
  480. pprint("Created default arbitration configuration")
  481. return config
  482. def load_config_from_file(file_path: Union[str, Path]) -> ArbitrationConfig:
  483. """
  484. Load arbitration configuration from file.
  485. Args:
  486. file_path: Path to configuration file
  487. Returns:
  488. ArbitrationConfig: Loaded configuration
  489. """
  490. return ArbitrationConfig.load_from_file(file_path)
  491. def create_config_with_overrides(**overrides) -> ArbitrationConfig:
  492. """
  493. Create configuration with specific overrides.
  494. Args:
  495. **overrides: Configuration overrides
  496. Returns:
  497. ArbitrationConfig: Configuration with overrides applied
  498. """
  499. config = ArbitrationConfig()
  500. config.update_from_dict(overrides)
  501. return config