""" Event system for the USDA Vision Camera System. This module provides a thread-safe event system for communication between different components of the system (MQTT, cameras, recording, etc.). """ import threading import logging from typing import Dict, List, Callable, Any, Optional from dataclasses import dataclass from datetime import datetime from enum import Enum class EventType(Enum): """Event types for the system""" MACHINE_STATE_CHANGED = "machine_state_changed" CAMERA_STATUS_CHANGED = "camera_status_changed" RECORDING_STARTED = "recording_started" RECORDING_STOPPED = "recording_stopped" RECORDING_ERROR = "recording_error" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" SYSTEM_SHUTDOWN = "system_shutdown" @dataclass class Event: """Event data structure""" event_type: EventType source: str data: Dict[str, Any] timestamp: datetime def __post_init__(self): if not isinstance(self.timestamp, datetime): self.timestamp = datetime.now() class EventSystem: """Thread-safe event system for inter-component communication""" def __init__(self): self.logger = logging.getLogger(__name__) self._subscribers: Dict[EventType, List[Callable]] = {} self._lock = threading.RLock() self._event_history: List[Event] = [] self._max_history = 1000 # Keep last 1000 events def subscribe(self, event_type: EventType, callback: Callable[[Event], None]) -> None: """Subscribe to an event type""" with self._lock: if event_type not in self._subscribers: self._subscribers[event_type] = [] if callback not in self._subscribers[event_type]: self._subscribers[event_type].append(callback) self.logger.debug(f"Subscribed to {event_type.value}") def unsubscribe(self, event_type: EventType, callback: Callable[[Event], None]) -> None: """Unsubscribe from an event type""" with self._lock: if event_type in self._subscribers: try: self._subscribers[event_type].remove(callback) self.logger.debug(f"Unsubscribed from {event_type.value}") except ValueError: pass # Callback wasn't subscribed def publish(self, event_type: EventType, source: str, data: Optional[Dict[str, Any]] = None) -> None: """Publish an event""" if data is None: data = {} event = Event( event_type=event_type, source=source, data=data, timestamp=datetime.now() ) # Add to history with self._lock: self._event_history.append(event) if len(self._event_history) > self._max_history: self._event_history.pop(0) # Notify subscribers self._notify_subscribers(event) def _notify_subscribers(self, event: Event) -> None: """Notify all subscribers of an event""" with self._lock: subscribers = self._subscribers.get(event.event_type, []).copy() for callback in subscribers: try: callback(event) except Exception as e: self.logger.error(f"Error in event callback for {event.event_type.value}: {e}") def get_recent_events(self, event_type: Optional[EventType] = None, limit: int = 100) -> List[Event]: """Get recent events, optionally filtered by type""" with self._lock: events = self._event_history.copy() if event_type: events = [e for e in events if e.event_type == event_type] return events[-limit:] if limit else events def clear_history(self) -> None: """Clear event history""" with self._lock: self._event_history.clear() self.logger.info("Event history cleared") def get_subscriber_count(self, event_type: EventType) -> int: """Get number of subscribers for an event type""" with self._lock: return len(self._subscribers.get(event_type, [])) def get_all_event_types(self) -> List[EventType]: """Get all event types that have subscribers""" with self._lock: return list(self._subscribers.keys()) # Global event system instance event_system = EventSystem() # Convenience functions for common events def publish_machine_state_changed(machine_name: str, state: str, source: str = "mqtt") -> None: """Publish machine state change event""" event_system.publish( EventType.MACHINE_STATE_CHANGED, source, { "machine_name": machine_name, "state": state, "previous_state": None # Could be enhanced to track previous state } ) def publish_camera_status_changed(camera_name: str, status: str, details: str = "", source: str = "camera_monitor") -> None: """Publish camera status change event""" event_system.publish( EventType.CAMERA_STATUS_CHANGED, source, { "camera_name": camera_name, "status": status, "details": details } ) def publish_recording_started(camera_name: str, filename: str, source: str = "recorder") -> None: """Publish recording started event""" event_system.publish( EventType.RECORDING_STARTED, source, { "camera_name": camera_name, "filename": filename } ) def publish_recording_stopped(camera_name: str, filename: str, duration_seconds: float, source: str = "recorder") -> None: """Publish recording stopped event""" event_system.publish( EventType.RECORDING_STOPPED, source, { "camera_name": camera_name, "filename": filename, "duration_seconds": duration_seconds } ) def publish_recording_error(camera_name: str, error_message: str, source: str = "recorder") -> None: """Publish recording error event""" event_system.publish( EventType.RECORDING_ERROR, source, { "camera_name": camera_name, "error_message": error_message } )