Massive update - API and other modules added

This commit is contained in:
Alireza Vaezi
2025-07-25 21:39:07 -04:00
parent 172f46d44d
commit f6d6ba612e
70 changed files with 7276 additions and 15 deletions

View File

@@ -0,0 +1,15 @@
"""
USDA Vision Camera System - Core Module
This module contains the core functionality for the USDA vision camera system,
including configuration management, state management, and event handling.
"""
__version__ = "1.0.0"
__author__ = "USDA Vision Team"
from .config import Config
from .state_manager import StateManager
from .events import EventSystem
__all__ = ["Config", "StateManager", "EventSystem"]

View File

@@ -0,0 +1,207 @@
"""
Configuration management for the USDA Vision Camera System.
This module handles all configuration settings including MQTT broker settings,
camera configurations, storage paths, and system parameters.
"""
import os
import json
import logging
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class MQTTConfig:
"""MQTT broker configuration"""
broker_host: str = "192.168.1.110"
broker_port: int = 1883
username: Optional[str] = None
password: Optional[str] = None
topics: Dict[str, str] = None
def __post_init__(self):
if self.topics is None:
self.topics = {
"vibratory_conveyor": "vision/vibratory_conveyor/state",
"blower_separator": "vision/blower_separator/state"
}
@dataclass
class CameraConfig:
"""Individual camera configuration"""
name: str
machine_topic: str # Which MQTT topic triggers this camera
storage_path: str
exposure_ms: float = 1.0
gain: float = 3.5
target_fps: float = 3.0
enabled: bool = True
@dataclass
class StorageConfig:
"""Storage configuration"""
base_path: str = "/storage"
max_file_size_mb: int = 1000 # Max size per video file
max_recording_duration_minutes: int = 60 # Max recording duration
cleanup_older_than_days: int = 30 # Auto cleanup old files
@dataclass
class SystemConfig:
"""System-wide configuration"""
camera_check_interval_seconds: int = 2
log_level: str = "INFO"
log_file: str = "usda_vision_system.log"
api_host: str = "0.0.0.0"
api_port: int = 8000
enable_api: bool = True
timezone: str = "America/New_York" # Atlanta, Georgia timezone
class Config:
"""Main configuration manager"""
def __init__(self, config_file: Optional[str] = None):
self.config_file = config_file or "config.json"
self.logger = logging.getLogger(__name__)
# Default configurations
self.mqtt = MQTTConfig()
self.storage = StorageConfig()
self.system = SystemConfig()
# Camera configurations - will be populated from config file or defaults
self.cameras: List[CameraConfig] = []
# Load configuration
self.load_config()
# Ensure storage directories exist
self._ensure_storage_directories()
def load_config(self) -> None:
"""Load configuration from file"""
config_path = Path(self.config_file)
if config_path.exists():
try:
with open(config_path, 'r') as f:
config_data = json.load(f)
# Load MQTT config
if 'mqtt' in config_data:
mqtt_data = config_data['mqtt']
self.mqtt = MQTTConfig(**mqtt_data)
# Load storage config
if 'storage' in config_data:
storage_data = config_data['storage']
self.storage = StorageConfig(**storage_data)
# Load system config
if 'system' in config_data:
system_data = config_data['system']
self.system = SystemConfig(**system_data)
# Load camera configs
if 'cameras' in config_data:
self.cameras = [
CameraConfig(**cam_data)
for cam_data in config_data['cameras']
]
else:
self._create_default_camera_configs()
self.logger.info(f"Configuration loaded from {config_path}")
except Exception as e:
self.logger.error(f"Error loading config from {config_path}: {e}")
self._create_default_camera_configs()
else:
self.logger.info(f"Config file {config_path} not found, using defaults")
self._create_default_camera_configs()
self.save_config() # Save default config
def _create_default_camera_configs(self) -> None:
"""Create default camera configurations"""
self.cameras = [
CameraConfig(
name="camera1",
machine_topic="vibratory_conveyor",
storage_path=os.path.join(self.storage.base_path, "camera1")
),
CameraConfig(
name="camera2",
machine_topic="blower_separator",
storage_path=os.path.join(self.storage.base_path, "camera2")
)
]
def save_config(self) -> None:
"""Save current configuration to file"""
config_data = {
'mqtt': asdict(self.mqtt),
'storage': asdict(self.storage),
'system': asdict(self.system),
'cameras': [asdict(cam) for cam in self.cameras]
}
try:
with open(self.config_file, 'w') as f:
json.dump(config_data, f, indent=2)
self.logger.info(f"Configuration saved to {self.config_file}")
except Exception as e:
self.logger.error(f"Error saving config to {self.config_file}: {e}")
def _ensure_storage_directories(self) -> None:
"""Ensure all storage directories exist"""
try:
# Create base storage directory
Path(self.storage.base_path).mkdir(parents=True, exist_ok=True)
# Create camera-specific directories
for camera in self.cameras:
Path(camera.storage_path).mkdir(parents=True, exist_ok=True)
self.logger.info("Storage directories verified/created")
except Exception as e:
self.logger.error(f"Error creating storage directories: {e}")
def get_camera_by_topic(self, topic: str) -> Optional[CameraConfig]:
"""Get camera configuration by MQTT topic"""
for camera in self.cameras:
if camera.machine_topic == topic:
return camera
return None
def get_camera_by_name(self, name: str) -> Optional[CameraConfig]:
"""Get camera configuration by name"""
for camera in self.cameras:
if camera.name == name:
return camera
return None
def update_camera_config(self, name: str, **kwargs) -> bool:
"""Update camera configuration"""
camera = self.get_camera_by_name(name)
if camera:
for key, value in kwargs.items():
if hasattr(camera, key):
setattr(camera, key, value)
self.save_config()
return True
return False
def to_dict(self) -> Dict[str, Any]:
"""Convert configuration to dictionary"""
return {
'mqtt': asdict(self.mqtt),
'storage': asdict(self.storage),
'system': asdict(self.system),
'cameras': [asdict(cam) for cam in self.cameras]
}

View File

@@ -0,0 +1,195 @@
"""
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
}
)

View File

@@ -0,0 +1,260 @@
"""
Logging configuration for the USDA Vision Camera System.
This module provides comprehensive logging setup with rotation, formatting,
and different log levels for different components.
"""
import logging
import logging.handlers
import os
import sys
from typing import Optional
from datetime import datetime
class ColoredFormatter(logging.Formatter):
"""Colored formatter for console output"""
# ANSI color codes
COLORS = {
'DEBUG': '\033[36m', # Cyan
'INFO': '\033[32m', # Green
'WARNING': '\033[33m', # Yellow
'ERROR': '\033[31m', # Red
'CRITICAL': '\033[35m', # Magenta
'RESET': '\033[0m' # Reset
}
def format(self, record):
# Add color to levelname
if record.levelname in self.COLORS:
record.levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.COLORS['RESET']}"
return super().format(record)
class USDAVisionLogger:
"""Custom logger setup for the USDA Vision Camera System"""
def __init__(self, log_level: str = "INFO", log_file: Optional[str] = None,
enable_console: bool = True, enable_rotation: bool = True):
self.log_level = log_level.upper()
self.log_file = log_file
self.enable_console = enable_console
self.enable_rotation = enable_rotation
# Setup logging
self._setup_logging()
def _setup_logging(self) -> None:
"""Setup comprehensive logging configuration"""
# Get root logger
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, self.log_level))
# Clear existing handlers
root_logger.handlers.clear()
# Create formatters
detailed_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s'
)
simple_formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
)
colored_formatter = ColoredFormatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Console handler
if self.enable_console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(getattr(logging, self.log_level))
console_handler.setFormatter(colored_formatter)
root_logger.addHandler(console_handler)
# File handler
if self.log_file:
try:
# Create log directory if it doesn't exist
log_dir = os.path.dirname(self.log_file)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
if self.enable_rotation:
# Rotating file handler (10MB max, keep 5 backups)
file_handler = logging.handlers.RotatingFileHandler(
self.log_file,
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
else:
file_handler = logging.FileHandler(self.log_file)
file_handler.setLevel(logging.DEBUG) # File gets all messages
file_handler.setFormatter(detailed_formatter)
root_logger.addHandler(file_handler)
except Exception as e:
print(f"Warning: Could not setup file logging: {e}")
# Setup specific logger levels for different components
self._setup_component_loggers()
# Log the logging setup
logger = logging.getLogger(__name__)
logger.info(f"Logging initialized - Level: {self.log_level}, File: {self.log_file}")
def _setup_component_loggers(self) -> None:
"""Setup specific log levels for different components"""
# MQTT client - can be verbose
mqtt_logger = logging.getLogger('usda_vision_system.mqtt')
if self.log_level == 'DEBUG':
mqtt_logger.setLevel(logging.DEBUG)
else:
mqtt_logger.setLevel(logging.INFO)
# Camera components - important for debugging
camera_logger = logging.getLogger('usda_vision_system.camera')
camera_logger.setLevel(logging.INFO)
# API server - can be noisy
api_logger = logging.getLogger('usda_vision_system.api')
if self.log_level == 'DEBUG':
api_logger.setLevel(logging.DEBUG)
else:
api_logger.setLevel(logging.INFO)
# Uvicorn - reduce noise unless debugging
uvicorn_logger = logging.getLogger('uvicorn')
if self.log_level == 'DEBUG':
uvicorn_logger.setLevel(logging.INFO)
else:
uvicorn_logger.setLevel(logging.WARNING)
# FastAPI - reduce noise
fastapi_logger = logging.getLogger('fastapi')
fastapi_logger.setLevel(logging.WARNING)
@staticmethod
def setup_exception_logging():
"""Setup logging for uncaught exceptions"""
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
# Don't log keyboard interrupts
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logger = logging.getLogger("uncaught_exception")
logger.critical(
"Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback)
)
sys.excepthook = handle_exception
class PerformanceLogger:
"""Logger for performance monitoring"""
def __init__(self, name: str):
self.logger = logging.getLogger(f"performance.{name}")
self.start_time: Optional[float] = None
def start_timer(self, operation: str) -> None:
"""Start timing an operation"""
import time
self.start_time = time.time()
self.logger.debug(f"Started: {operation}")
def end_timer(self, operation: str) -> float:
"""End timing an operation and log duration"""
import time
if self.start_time is None:
self.logger.warning(f"Timer not started for: {operation}")
return 0.0
duration = time.time() - self.start_time
self.logger.info(f"Completed: {operation} in {duration:.3f}s")
self.start_time = None
return duration
def log_metric(self, metric_name: str, value: float, unit: str = "") -> None:
"""Log a performance metric"""
self.logger.info(f"Metric: {metric_name} = {value} {unit}")
class ErrorTracker:
"""Track and log errors with context"""
def __init__(self, component_name: str):
self.component_name = component_name
self.logger = logging.getLogger(f"errors.{component_name}")
self.error_count = 0
self.last_error_time: Optional[datetime] = None
def log_error(self, error: Exception, context: str = "",
additional_data: Optional[dict] = None) -> None:
"""Log an error with context and tracking"""
self.error_count += 1
self.last_error_time = datetime.now()
error_msg = f"Error in {self.component_name}"
if context:
error_msg += f" ({context})"
error_msg += f": {str(error)}"
if additional_data:
error_msg += f" | Data: {additional_data}"
self.logger.error(error_msg, exc_info=True)
def log_warning(self, message: str, context: str = "") -> None:
"""Log a warning with context"""
warning_msg = f"Warning in {self.component_name}"
if context:
warning_msg += f" ({context})"
warning_msg += f": {message}"
self.logger.warning(warning_msg)
def get_error_stats(self) -> dict:
"""Get error statistics"""
return {
"component": self.component_name,
"error_count": self.error_count,
"last_error_time": self.last_error_time.isoformat() if self.last_error_time else None
}
def setup_logging(log_level: str = "INFO", log_file: Optional[str] = None) -> USDAVisionLogger:
"""Setup logging for the entire application"""
# Setup main logging
logger_setup = USDAVisionLogger(
log_level=log_level,
log_file=log_file,
enable_console=True,
enable_rotation=True
)
# Setup exception logging
USDAVisionLogger.setup_exception_logging()
return logger_setup
def get_performance_logger(component_name: str) -> PerformanceLogger:
"""Get a performance logger for a component"""
return PerformanceLogger(component_name)
def get_error_tracker(component_name: str) -> ErrorTracker:
"""Get an error tracker for a component"""
return ErrorTracker(component_name)

View File

@@ -0,0 +1,328 @@
"""
State management for the USDA Vision Camera System.
This module manages the current state of machines, cameras, and recordings
in a thread-safe manner.
"""
import threading
import logging
from typing import Dict, Optional, List, Any
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
class MachineState(Enum):
"""Machine states"""
UNKNOWN = "unknown"
ON = "on"
OFF = "off"
ERROR = "error"
class CameraStatus(Enum):
"""Camera status"""
UNKNOWN = "unknown"
AVAILABLE = "available"
BUSY = "busy"
ERROR = "error"
DISCONNECTED = "disconnected"
class RecordingState(Enum):
"""Recording states"""
IDLE = "idle"
RECORDING = "recording"
STOPPING = "stopping"
ERROR = "error"
@dataclass
class MachineInfo:
"""Machine state information"""
name: str
state: MachineState = MachineState.UNKNOWN
last_updated: datetime = field(default_factory=datetime.now)
last_message: Optional[str] = None
mqtt_topic: Optional[str] = None
@dataclass
class CameraInfo:
"""Camera state information"""
name: str
status: CameraStatus = CameraStatus.UNKNOWN
last_checked: datetime = field(default_factory=datetime.now)
last_error: Optional[str] = None
device_info: Optional[Dict[str, Any]] = None
is_recording: bool = False
current_recording_file: Optional[str] = None
recording_start_time: Optional[datetime] = None
@dataclass
class RecordingInfo:
"""Recording session information"""
camera_name: str
filename: str
start_time: datetime
state: RecordingState = RecordingState.RECORDING
end_time: Optional[datetime] = None
file_size_bytes: Optional[int] = None
frame_count: Optional[int] = None
error_message: Optional[str] = None
class StateManager:
"""Thread-safe state manager for the entire system"""
def __init__(self):
self.logger = logging.getLogger(__name__)
self._lock = threading.RLock()
# State dictionaries
self._machines: Dict[str, MachineInfo] = {}
self._cameras: Dict[str, CameraInfo] = {}
self._recordings: Dict[str, RecordingInfo] = {} # Key: recording_id (filename)
# System state
self._mqtt_connected = False
self._system_started = False
self._last_mqtt_message_time: Optional[datetime] = None
# Machine state management
def update_machine_state(self, name: str, state: str, message: Optional[str] = None, topic: Optional[str] = None) -> bool:
"""Update machine state"""
try:
machine_state = MachineState(state.lower())
except ValueError:
self.logger.warning(f"Invalid machine state: {state}")
machine_state = MachineState.UNKNOWN
with self._lock:
if name not in self._machines:
self._machines[name] = MachineInfo(name=name, mqtt_topic=topic)
machine = self._machines[name]
old_state = machine.state
machine.state = machine_state
machine.last_updated = datetime.now()
machine.last_message = message
if topic:
machine.mqtt_topic = topic
self.logger.info(f"Machine {name} state: {old_state.value} -> {machine_state.value}")
return old_state != machine_state
def get_machine_state(self, name: str) -> Optional[MachineInfo]:
"""Get machine state"""
with self._lock:
return self._machines.get(name)
def get_all_machines(self) -> Dict[str, MachineInfo]:
"""Get all machine states"""
with self._lock:
return self._machines.copy()
# Camera state management
def update_camera_status(self, name: str, status: str, error: Optional[str] = None, device_info: Optional[Dict] = None) -> bool:
"""Update camera status"""
try:
camera_status = CameraStatus(status.lower())
except ValueError:
self.logger.warning(f"Invalid camera status: {status}")
camera_status = CameraStatus.UNKNOWN
with self._lock:
if name not in self._cameras:
self._cameras[name] = CameraInfo(name=name)
camera = self._cameras[name]
old_status = camera.status
camera.status = camera_status
camera.last_checked = datetime.now()
camera.last_error = error
if device_info:
camera.device_info = device_info
if old_status != camera_status:
self.logger.info(f"Camera {name} status: {old_status.value} -> {camera_status.value}")
return True
return False
def set_camera_recording(self, name: str, recording: bool, filename: Optional[str] = None) -> None:
"""Set camera recording state"""
with self._lock:
if name not in self._cameras:
self._cameras[name] = CameraInfo(name=name)
camera = self._cameras[name]
camera.is_recording = recording
camera.current_recording_file = filename
if recording and filename:
camera.recording_start_time = datetime.now()
self.logger.info(f"Camera {name} started recording: {filename}")
elif not recording:
camera.recording_start_time = None
self.logger.info(f"Camera {name} stopped recording")
def get_camera_status(self, name: str) -> Optional[CameraInfo]:
"""Get camera status"""
with self._lock:
return self._cameras.get(name)
def get_all_cameras(self) -> Dict[str, CameraInfo]:
"""Get all camera statuses"""
with self._lock:
return self._cameras.copy()
# Recording management
def start_recording(self, camera_name: str, filename: str) -> str:
"""Start a new recording session"""
recording_id = filename # Use filename as recording ID
with self._lock:
recording = RecordingInfo(
camera_name=camera_name,
filename=filename,
start_time=datetime.now()
)
self._recordings[recording_id] = recording
# Update camera state
self.set_camera_recording(camera_name, True, filename)
self.logger.info(f"Started recording session: {recording_id}")
return recording_id
def stop_recording(self, recording_id: str, file_size: Optional[int] = None, frame_count: Optional[int] = None) -> bool:
"""Stop a recording session"""
with self._lock:
if recording_id not in self._recordings:
self.logger.warning(f"Recording session not found: {recording_id}")
return False
recording = self._recordings[recording_id]
recording.state = RecordingState.IDLE
recording.end_time = datetime.now()
recording.file_size_bytes = file_size
recording.frame_count = frame_count
# Update camera state
self.set_camera_recording(recording.camera_name, False)
duration = (recording.end_time - recording.start_time).total_seconds()
self.logger.info(f"Stopped recording session: {recording_id} (duration: {duration:.1f}s)")
return True
def set_recording_error(self, recording_id: str, error_message: str) -> bool:
"""Set recording error state"""
with self._lock:
if recording_id not in self._recordings:
return False
recording = self._recordings[recording_id]
recording.state = RecordingState.ERROR
recording.error_message = error_message
recording.end_time = datetime.now()
# Update camera state
self.set_camera_recording(recording.camera_name, False)
self.logger.error(f"Recording error for {recording_id}: {error_message}")
return True
def get_recording(self, recording_id: str) -> Optional[RecordingInfo]:
"""Get recording information"""
with self._lock:
return self._recordings.get(recording_id)
def get_all_recordings(self) -> Dict[str, RecordingInfo]:
"""Get all recording sessions"""
with self._lock:
return self._recordings.copy()
def get_active_recordings(self) -> Dict[str, RecordingInfo]:
"""Get currently active recordings"""
with self._lock:
return {
rid: recording for rid, recording in self._recordings.items()
if recording.state == RecordingState.RECORDING
}
# System state management
def set_mqtt_connected(self, connected: bool) -> None:
"""Set MQTT connection state"""
with self._lock:
old_state = self._mqtt_connected
self._mqtt_connected = connected
if connected:
self._last_mqtt_message_time = datetime.now()
if old_state != connected:
self.logger.info(f"MQTT connection: {'connected' if connected else 'disconnected'}")
def is_mqtt_connected(self) -> bool:
"""Check if MQTT is connected"""
with self._lock:
return self._mqtt_connected
def update_mqtt_activity(self) -> None:
"""Update last MQTT message time"""
with self._lock:
self._last_mqtt_message_time = datetime.now()
def set_system_started(self, started: bool) -> None:
"""Set system started state"""
with self._lock:
self._system_started = started
self.logger.info(f"System {'started' if started else 'stopped'}")
def is_system_started(self) -> bool:
"""Check if system is started"""
with self._lock:
return self._system_started
# Utility methods
def get_system_summary(self) -> Dict[str, Any]:
"""Get a summary of the entire system state"""
with self._lock:
return {
"system_started": self._system_started,
"mqtt_connected": self._mqtt_connected,
"last_mqtt_message": self._last_mqtt_message_time.isoformat() if self._last_mqtt_message_time else None,
"machines": {name: {
"state": machine.state.value,
"last_updated": machine.last_updated.isoformat()
} for name, machine in self._machines.items()},
"cameras": {name: {
"status": camera.status.value,
"is_recording": camera.is_recording,
"last_checked": camera.last_checked.isoformat()
} for name, camera in self._cameras.items()},
"active_recordings": len(self.get_active_recordings()),
"total_recordings": len(self._recordings)
}
def cleanup_old_recordings(self, max_age_hours: int = 24) -> int:
"""Clean up old recording entries from memory"""
cutoff_time = datetime.now() - datetime.timedelta(hours=max_age_hours)
removed_count = 0
with self._lock:
to_remove = []
for recording_id, recording in self._recordings.items():
if (recording.state != RecordingState.RECORDING and
recording.end_time and recording.end_time < cutoff_time):
to_remove.append(recording_id)
for recording_id in to_remove:
del self._recordings[recording_id]
removed_count += 1
if removed_count > 0:
self.logger.info(f"Cleaned up {removed_count} old recording entries")
return removed_count

View File

@@ -0,0 +1,225 @@
"""
Timezone utilities for the USDA Vision Camera System.
This module provides timezone-aware datetime handling for Atlanta, Georgia.
"""
import datetime
import pytz
import logging
from typing import Optional
class TimezoneManager:
"""Manages timezone-aware datetime operations"""
def __init__(self, timezone_name: str = "America/New_York"):
self.timezone_name = timezone_name
self.timezone = pytz.timezone(timezone_name)
self.logger = logging.getLogger(__name__)
# Log timezone information
self.logger.info(f"Timezone manager initialized for {timezone_name}")
self._log_timezone_info()
def _log_timezone_info(self) -> None:
"""Log current timezone information"""
now = self.now()
self.logger.info(f"Current local time: {now}")
self.logger.info(f"Current UTC time: {self.to_utc(now)}")
self.logger.info(f"Timezone: {now.tzname()} (UTC{now.strftime('%z')})")
def now(self) -> datetime.datetime:
"""Get current time in the configured timezone"""
return datetime.datetime.now(self.timezone)
def utc_now(self) -> datetime.datetime:
"""Get current UTC time"""
return datetime.datetime.now(pytz.UTC)
def to_local(self, dt: datetime.datetime) -> datetime.datetime:
"""Convert datetime to local timezone"""
if dt.tzinfo is None:
# Assume UTC if no timezone info
dt = pytz.UTC.localize(dt)
return dt.astimezone(self.timezone)
def to_utc(self, dt: datetime.datetime) -> datetime.datetime:
"""Convert datetime to UTC"""
if dt.tzinfo is None:
# Assume local timezone if no timezone info
dt = self.timezone.localize(dt)
return dt.astimezone(pytz.UTC)
def localize(self, dt: datetime.datetime) -> datetime.datetime:
"""Add timezone info to naive datetime (assumes local timezone)"""
if dt.tzinfo is not None:
return dt
return self.timezone.localize(dt)
def format_timestamp(self, dt: Optional[datetime.datetime] = None,
include_timezone: bool = True) -> str:
"""Format datetime as timestamp string"""
if dt is None:
dt = self.now()
if dt.tzinfo is None:
dt = self.localize(dt)
if include_timezone:
return dt.strftime("%Y-%m-%d %H:%M:%S %Z")
else:
return dt.strftime("%Y-%m-%d %H:%M:%S")
def format_filename_timestamp(self, dt: Optional[datetime.datetime] = None) -> str:
"""Format datetime for use in filenames (no special characters)"""
if dt is None:
dt = self.now()
if dt.tzinfo is None:
dt = self.localize(dt)
return dt.strftime("%Y%m%d_%H%M%S")
def parse_timestamp(self, timestamp_str: str) -> datetime.datetime:
"""Parse timestamp string to datetime"""
try:
# Try parsing with timezone info
return datetime.datetime.fromisoformat(timestamp_str)
except ValueError:
try:
# Try parsing without timezone (assume local)
dt = datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
return self.localize(dt)
except ValueError:
try:
# Try parsing filename format
dt = datetime.datetime.strptime(timestamp_str, "%Y%m%d_%H%M%S")
return self.localize(dt)
except ValueError:
raise ValueError(f"Unable to parse timestamp: {timestamp_str}")
def is_dst(self, dt: Optional[datetime.datetime] = None) -> bool:
"""Check if datetime is during daylight saving time"""
if dt is None:
dt = self.now()
if dt.tzinfo is None:
dt = self.localize(dt)
return bool(dt.dst())
def get_timezone_offset(self, dt: Optional[datetime.datetime] = None) -> str:
"""Get timezone offset string (e.g., '-0500' or '-0400')"""
if dt is None:
dt = self.now()
if dt.tzinfo is None:
dt = self.localize(dt)
return dt.strftime('%z')
def get_timezone_name(self, dt: Optional[datetime.datetime] = None) -> str:
"""Get timezone name (e.g., 'EST' or 'EDT')"""
if dt is None:
dt = self.now()
if dt.tzinfo is None:
dt = self.localize(dt)
return dt.tzname()
# Global timezone manager instance for Atlanta, Georgia
atlanta_tz = TimezoneManager("America/New_York")
# Convenience functions
def now_atlanta() -> datetime.datetime:
"""Get current Atlanta time"""
return atlanta_tz.now()
def format_atlanta_timestamp(dt: Optional[datetime.datetime] = None) -> str:
"""Format timestamp in Atlanta timezone"""
return atlanta_tz.format_timestamp(dt)
def format_filename_timestamp(dt: Optional[datetime.datetime] = None) -> str:
"""Format timestamp for filenames"""
return atlanta_tz.format_filename_timestamp(dt)
def to_atlanta_time(dt: datetime.datetime) -> datetime.datetime:
"""Convert any datetime to Atlanta time"""
return atlanta_tz.to_local(dt)
def check_time_sync() -> dict:
"""Check if system time appears to be synchronized"""
import requests
result = {
"system_time": now_atlanta(),
"timezone": atlanta_tz.get_timezone_name(),
"offset": atlanta_tz.get_timezone_offset(),
"dst": atlanta_tz.is_dst(),
"sync_status": "unknown",
"time_diff_seconds": None,
"error": None
}
try:
# Check against world time API
response = requests.get(
"http://worldtimeapi.org/api/timezone/America/New_York",
timeout=5
)
if response.status_code == 200:
data = response.json()
api_time = datetime.datetime.fromisoformat(data['datetime'])
# Convert to same timezone for comparison
system_time = atlanta_tz.now()
time_diff = abs((system_time.replace(tzinfo=None) -
api_time.replace(tzinfo=None)).total_seconds())
result["api_time"] = api_time
result["time_diff_seconds"] = time_diff
if time_diff < 5:
result["sync_status"] = "synchronized"
elif time_diff < 30:
result["sync_status"] = "minor_drift"
else:
result["sync_status"] = "out_of_sync"
else:
result["error"] = f"API returned status {response.status_code}"
except Exception as e:
result["error"] = str(e)
return result
def log_time_info(logger: Optional[logging.Logger] = None) -> None:
"""Log comprehensive time information"""
if logger is None:
logger = logging.getLogger(__name__)
sync_info = check_time_sync()
logger.info("=== TIME SYNCHRONIZATION STATUS ===")
logger.info(f"System time: {sync_info['system_time']}")
logger.info(f"Timezone: {sync_info['timezone']} ({sync_info['offset']})")
logger.info(f"Daylight Saving: {'Yes' if sync_info['dst'] else 'No'}")
logger.info(f"Sync status: {sync_info['sync_status']}")
if sync_info.get('time_diff_seconds') is not None:
logger.info(f"Time difference: {sync_info['time_diff_seconds']:.2f} seconds")
if sync_info.get('error'):
logger.warning(f"Time sync check error: {sync_info['error']}")
logger.info("=====================================")