feat: Add MQTT publisher and tester scripts for USDA Vision Camera System
- Implemented mqtt_publisher_test.py for manual MQTT message publishing - Created mqtt_test.py to test MQTT message reception and display statistics - Developed test_api_changes.py to verify API changes for camera settings and filename handling - Added test_camera_recovery_api.py for testing camera recovery API endpoints - Introduced test_max_fps.py to demonstrate maximum FPS capture functionality - Implemented test_mqtt_events_api.py to test MQTT events API endpoint - Created test_mqtt_logging.py for enhanced MQTT logging and API endpoint testing - Added sdk_config.py for SDK initialization and configuration with error suppression
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
class SystemStatusResponse(BaseModel):
|
||||
"""System status response model"""
|
||||
|
||||
system_started: bool
|
||||
mqtt_connected: bool
|
||||
last_mqtt_message: Optional[str] = None
|
||||
@@ -23,6 +24,7 @@ class SystemStatusResponse(BaseModel):
|
||||
|
||||
class MachineStatusResponse(BaseModel):
|
||||
"""Machine status response model"""
|
||||
|
||||
name: str
|
||||
state: str
|
||||
last_updated: str
|
||||
@@ -30,8 +32,22 @@ class MachineStatusResponse(BaseModel):
|
||||
mqtt_topic: Optional[str] = None
|
||||
|
||||
|
||||
class MQTTStatusResponse(BaseModel):
|
||||
"""MQTT status response model"""
|
||||
|
||||
connected: bool
|
||||
broker_host: str
|
||||
broker_port: int
|
||||
subscribed_topics: List[str]
|
||||
last_message_time: Optional[str] = None
|
||||
message_count: int
|
||||
error_count: int
|
||||
uptime_seconds: Optional[float] = None
|
||||
|
||||
|
||||
class CameraStatusResponse(BaseModel):
|
||||
"""Camera status response model"""
|
||||
|
||||
name: str
|
||||
status: str
|
||||
is_recording: bool
|
||||
@@ -44,6 +60,7 @@ class CameraStatusResponse(BaseModel):
|
||||
|
||||
class RecordingInfoResponse(BaseModel):
|
||||
"""Recording information response model"""
|
||||
|
||||
camera_name: str
|
||||
filename: str
|
||||
start_time: str
|
||||
@@ -57,12 +74,16 @@ class RecordingInfoResponse(BaseModel):
|
||||
|
||||
class StartRecordingRequest(BaseModel):
|
||||
"""Start recording request model"""
|
||||
camera_name: str
|
||||
|
||||
filename: Optional[str] = None
|
||||
exposure_ms: Optional[float] = Field(default=None, description="Exposure time in milliseconds")
|
||||
gain: Optional[float] = Field(default=None, description="Camera gain value")
|
||||
fps: Optional[float] = Field(default=None, description="Target frames per second")
|
||||
|
||||
|
||||
class StartRecordingResponse(BaseModel):
|
||||
"""Start recording response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
filename: Optional[str] = None
|
||||
@@ -70,11 +91,15 @@ class StartRecordingResponse(BaseModel):
|
||||
|
||||
class StopRecordingRequest(BaseModel):
|
||||
"""Stop recording request model"""
|
||||
camera_name: str
|
||||
|
||||
# Note: This model is currently unused as the stop recording endpoint
|
||||
# only requires the camera_name from the URL path parameter
|
||||
pass
|
||||
|
||||
|
||||
class StopRecordingResponse(BaseModel):
|
||||
"""Stop recording response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
duration_seconds: Optional[float] = None
|
||||
@@ -82,6 +107,7 @@ class StopRecordingResponse(BaseModel):
|
||||
|
||||
class StorageStatsResponse(BaseModel):
|
||||
"""Storage statistics response model"""
|
||||
|
||||
base_path: str
|
||||
total_files: int
|
||||
total_size_bytes: int
|
||||
@@ -91,6 +117,7 @@ class StorageStatsResponse(BaseModel):
|
||||
|
||||
class FileListRequest(BaseModel):
|
||||
"""File list request model"""
|
||||
|
||||
camera_name: Optional[str] = None
|
||||
start_date: Optional[str] = None
|
||||
end_date: Optional[str] = None
|
||||
@@ -99,17 +126,20 @@ class FileListRequest(BaseModel):
|
||||
|
||||
class FileListResponse(BaseModel):
|
||||
"""File list response model"""
|
||||
|
||||
files: List[Dict[str, Any]]
|
||||
total_count: int
|
||||
|
||||
|
||||
class CleanupRequest(BaseModel):
|
||||
"""Cleanup request model"""
|
||||
|
||||
max_age_days: Optional[int] = None
|
||||
|
||||
|
||||
class CleanupResponse(BaseModel):
|
||||
"""Cleanup response model"""
|
||||
|
||||
files_removed: int
|
||||
bytes_freed: int
|
||||
errors: List[str]
|
||||
@@ -117,6 +147,7 @@ class CleanupResponse(BaseModel):
|
||||
|
||||
class EventResponse(BaseModel):
|
||||
"""Event response model"""
|
||||
|
||||
event_type: str
|
||||
source: str
|
||||
data: Dict[str, Any]
|
||||
@@ -125,6 +156,7 @@ class EventResponse(BaseModel):
|
||||
|
||||
class WebSocketMessage(BaseModel):
|
||||
"""WebSocket message model"""
|
||||
|
||||
type: str
|
||||
data: Dict[str, Any]
|
||||
timestamp: Optional[str] = None
|
||||
@@ -132,13 +164,53 @@ class WebSocketMessage(BaseModel):
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Error response model"""
|
||||
|
||||
error: str
|
||||
details: Optional[str] = None
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
class CameraRecoveryResponse(BaseModel):
|
||||
"""Camera recovery response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
camera_name: str
|
||||
operation: str
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
class CameraTestResponse(BaseModel):
|
||||
"""Camera connection test response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
camera_name: str
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
class MQTTEventResponse(BaseModel):
|
||||
"""MQTT event response model"""
|
||||
|
||||
machine_name: str
|
||||
topic: str
|
||||
payload: str
|
||||
normalized_state: str
|
||||
timestamp: str
|
||||
message_number: int
|
||||
|
||||
|
||||
class MQTTEventsHistoryResponse(BaseModel):
|
||||
"""MQTT events history response model"""
|
||||
|
||||
events: List[MQTTEventResponse]
|
||||
total_events: int
|
||||
last_updated: Optional[str] = None
|
||||
|
||||
|
||||
class SuccessResponse(BaseModel):
|
||||
"""Success response model"""
|
||||
|
||||
success: bool = True
|
||||
message: str
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
|
||||
@@ -25,31 +25,31 @@ from .models import *
|
||||
|
||||
class WebSocketManager:
|
||||
"""Manages WebSocket connections for real-time updates"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
self.logger = logging.getLogger(f"{__name__}.WebSocketManager")
|
||||
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
self.logger.info(f"WebSocket connected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
if websocket in self.active_connections:
|
||||
self.active_connections.remove(websocket)
|
||||
self.logger.info(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
|
||||
async def send_personal_message(self, message: dict, websocket: WebSocket):
|
||||
try:
|
||||
await websocket.send_text(json.dumps(message))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error sending personal message: {e}")
|
||||
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
if not self.active_connections:
|
||||
return
|
||||
|
||||
|
||||
disconnected = []
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
@@ -57,7 +57,7 @@ class WebSocketManager:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error broadcasting to connection: {e}")
|
||||
disconnected.append(connection)
|
||||
|
||||
|
||||
# Remove disconnected connections
|
||||
for connection in disconnected:
|
||||
self.disconnect(connection)
|
||||
@@ -65,9 +65,8 @@ class WebSocketManager:
|
||||
|
||||
class APIServer:
|
||||
"""FastAPI server for the USDA Vision Camera System"""
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem,
|
||||
camera_manager, mqtt_client, storage_manager: StorageManager):
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager, mqtt_client, storage_manager: StorageManager):
|
||||
self.config = config
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
@@ -75,111 +74,101 @@ class APIServer:
|
||||
self.mqtt_client = mqtt_client
|
||||
self.storage_manager = storage_manager
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# FastAPI app
|
||||
self.app = FastAPI(
|
||||
title="USDA Vision Camera System API",
|
||||
description="API for monitoring and controlling the USDA vision camera system",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
self.app = FastAPI(title="USDA Vision Camera System API", description="API for monitoring and controlling the USDA vision camera system", version="1.0.0")
|
||||
|
||||
# WebSocket manager
|
||||
self.websocket_manager = WebSocketManager()
|
||||
|
||||
|
||||
# Server state
|
||||
self.server_start_time = datetime.now()
|
||||
self.running = False
|
||||
self._server_thread: Optional[threading.Thread] = None
|
||||
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
|
||||
# Setup CORS
|
||||
self.app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Configure appropriately for production
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
self.app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) # Configure appropriately for production
|
||||
|
||||
# Setup routes
|
||||
self._setup_routes()
|
||||
|
||||
|
||||
# Subscribe to events for WebSocket broadcasting
|
||||
self._setup_event_subscriptions()
|
||||
|
||||
|
||||
def _setup_routes(self):
|
||||
"""Setup API routes"""
|
||||
|
||||
|
||||
@self.app.get("/", response_model=SuccessResponse)
|
||||
async def root():
|
||||
return SuccessResponse(message="USDA Vision Camera System API")
|
||||
|
||||
|
||||
@self.app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
|
||||
@self.app.get("/system/status", response_model=SystemStatusResponse)
|
||||
async def get_system_status():
|
||||
"""Get overall system status"""
|
||||
try:
|
||||
summary = self.state_manager.get_system_summary()
|
||||
uptime = (datetime.now() - self.server_start_time).total_seconds()
|
||||
|
||||
return SystemStatusResponse(
|
||||
system_started=summary["system_started"],
|
||||
mqtt_connected=summary["mqtt_connected"],
|
||||
last_mqtt_message=summary["last_mqtt_message"],
|
||||
machines=summary["machines"],
|
||||
cameras=summary["cameras"],
|
||||
active_recordings=summary["active_recordings"],
|
||||
total_recordings=summary["total_recordings"],
|
||||
uptime_seconds=uptime
|
||||
)
|
||||
|
||||
return SystemStatusResponse(system_started=summary["system_started"], mqtt_connected=summary["mqtt_connected"], last_mqtt_message=summary["last_mqtt_message"], machines=summary["machines"], cameras=summary["cameras"], active_recordings=summary["active_recordings"], total_recordings=summary["total_recordings"], uptime_seconds=uptime)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting system status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/machines", response_model=Dict[str, MachineStatusResponse])
|
||||
async def get_machines():
|
||||
"""Get all machine statuses"""
|
||||
try:
|
||||
machines = self.state_manager.get_all_machines()
|
||||
return {
|
||||
name: MachineStatusResponse(
|
||||
name=machine.name,
|
||||
state=machine.state.value,
|
||||
last_updated=machine.last_updated.isoformat(),
|
||||
last_message=machine.last_message,
|
||||
mqtt_topic=machine.mqtt_topic
|
||||
)
|
||||
for name, machine in machines.items()
|
||||
}
|
||||
return {name: MachineStatusResponse(name=machine.name, state=machine.state.value, last_updated=machine.last_updated.isoformat(), last_message=machine.last_message, mqtt_topic=machine.mqtt_topic) for name, machine in machines.items()}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting machines: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/mqtt/status", response_model=MQTTStatusResponse)
|
||||
async def get_mqtt_status():
|
||||
"""Get MQTT client status and statistics"""
|
||||
try:
|
||||
status = self.mqtt_client.get_status()
|
||||
return MQTTStatusResponse(connected=status["connected"], broker_host=status["broker_host"], broker_port=status["broker_port"], subscribed_topics=status["subscribed_topics"], last_message_time=status["last_message_time"], message_count=status["message_count"], error_count=status["error_count"], uptime_seconds=status["uptime_seconds"])
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting MQTT status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/mqtt/events", response_model=MQTTEventsHistoryResponse)
|
||||
async def get_mqtt_events(limit: int = Query(default=5, ge=1, le=50, description="Number of recent events to retrieve")):
|
||||
"""Get recent MQTT events history"""
|
||||
try:
|
||||
events = self.state_manager.get_recent_mqtt_events(limit)
|
||||
total_events = self.state_manager.get_mqtt_event_count()
|
||||
|
||||
# Convert events to response format
|
||||
event_responses = [MQTTEventResponse(machine_name=event.machine_name, topic=event.topic, payload=event.payload, normalized_state=event.normalized_state, timestamp=event.timestamp.isoformat(), message_number=event.message_number) for event in events]
|
||||
|
||||
last_updated = events[0].timestamp.isoformat() if events else None
|
||||
|
||||
return MQTTEventsHistoryResponse(events=event_responses, total_events=total_events, last_updated=last_updated)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting MQTT events: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras", response_model=Dict[str, CameraStatusResponse])
|
||||
async def get_cameras():
|
||||
"""Get all camera statuses"""
|
||||
try:
|
||||
cameras = self.state_manager.get_all_cameras()
|
||||
return {
|
||||
name: CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
name: CameraStatusResponse(name=camera.name, status=camera.status.value, is_recording=camera.is_recording, last_checked=camera.last_checked.isoformat(), last_error=camera.last_error, device_info=camera.device_info, current_recording_file=camera.current_recording_file, recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None)
|
||||
for name, camera in cameras.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting cameras: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/cameras/{camera_name}/status", response_model=CameraStatusResponse)
|
||||
async def get_camera_status(camera_name: str):
|
||||
"""Get specific camera status"""
|
||||
@@ -187,70 +176,158 @@ class APIServer:
|
||||
camera = self.state_manager.get_camera_status(camera_name)
|
||||
if not camera:
|
||||
raise HTTPException(status_code=404, detail=f"Camera not found: {camera_name}")
|
||||
|
||||
return CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
|
||||
return CameraStatusResponse(name=camera.name, status=camera.status.value, is_recording=camera.is_recording, last_checked=camera.last_checked.isoformat(), last_error=camera.last_error, device_info=camera.device_info, current_recording_file=camera.current_recording_file, recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting camera status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/start-recording", response_model=StartRecordingResponse)
|
||||
async def start_recording(camera_name: str, request: StartRecordingRequest):
|
||||
"""Manually start recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.manual_start_recording(camera_name, request.filename)
|
||||
|
||||
|
||||
success = self.camera_manager.manual_start_recording(camera_name=camera_name, filename=request.filename, exposure_ms=request.exposure_ms, gain=request.gain, fps=request.fps)
|
||||
|
||||
if success:
|
||||
return StartRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording started for {camera_name}",
|
||||
filename=request.filename
|
||||
)
|
||||
# Get the actual filename that was used (with datetime prefix)
|
||||
actual_filename = request.filename
|
||||
if request.filename:
|
||||
from ..core.timezone_utils import format_filename_timestamp
|
||||
|
||||
timestamp = format_filename_timestamp()
|
||||
actual_filename = f"{timestamp}_{request.filename}"
|
||||
|
||||
return StartRecordingResponse(success=True, message=f"Recording started for {camera_name}", filename=actual_filename)
|
||||
else:
|
||||
return StartRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to start recording for {camera_name}"
|
||||
)
|
||||
return StartRecordingResponse(success=False, message=f"Failed to start recording for {camera_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/stop-recording", response_model=StopRecordingResponse)
|
||||
async def stop_recording(camera_name: str):
|
||||
"""Manually stop recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
|
||||
success = self.camera_manager.manual_stop_recording(camera_name)
|
||||
|
||||
|
||||
if success:
|
||||
return StopRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording stopped for {camera_name}"
|
||||
)
|
||||
return StopRecordingResponse(success=True, message=f"Recording stopped for {camera_name}")
|
||||
else:
|
||||
return StopRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to stop recording for {camera_name}"
|
||||
)
|
||||
return StopRecordingResponse(success=False, message=f"Failed to stop recording for {camera_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/test-connection", response_model=CameraTestResponse)
|
||||
async def test_camera_connection(camera_name: str):
|
||||
"""Test camera connection"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.test_camera_connection(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraTestResponse(success=True, message=f"Camera {camera_name} connection test passed", camera_name=camera_name)
|
||||
else:
|
||||
return CameraTestResponse(success=False, message=f"Camera {camera_name} connection test failed", camera_name=camera_name)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error testing camera connection: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reconnect", response_model=CameraRecoveryResponse)
|
||||
async def reconnect_camera(camera_name: str):
|
||||
"""Reconnect to a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reconnect_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} reconnected successfully", camera_name=camera_name, operation="reconnect")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reconnect camera {camera_name}", camera_name=camera_name, operation="reconnect")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reconnecting camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/restart-grab", response_model=CameraRecoveryResponse)
|
||||
async def restart_camera_grab(camera_name: str):
|
||||
"""Restart camera grab process"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.restart_camera_grab(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} grab process restarted successfully", camera_name=camera_name, operation="restart-grab")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to restart grab process for camera {camera_name}", camera_name=camera_name, operation="restart-grab")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error restarting camera grab: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reset-timestamp", response_model=CameraRecoveryResponse)
|
||||
async def reset_camera_timestamp(camera_name: str):
|
||||
"""Reset camera timestamp"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reset_camera_timestamp(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} timestamp reset successfully", camera_name=camera_name, operation="reset-timestamp")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reset timestamp for camera {camera_name}", camera_name=camera_name, operation="reset-timestamp")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error resetting camera timestamp: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/full-reset", response_model=CameraRecoveryResponse)
|
||||
async def full_reset_camera(camera_name: str):
|
||||
"""Perform full camera reset (uninitialize and reinitialize)"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.full_reset_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} full reset completed successfully", camera_name=camera_name, operation="full-reset")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to perform full reset for camera {camera_name}", camera_name=camera_name, operation="full-reset")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error performing full camera reset: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reinitialize", response_model=CameraRecoveryResponse)
|
||||
async def reinitialize_camera(camera_name: str):
|
||||
"""Reinitialize a failed camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reinitialize_failed_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} reinitialized successfully", camera_name=camera_name, operation="reinitialize")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reinitialize camera {camera_name}", camera_name=camera_name, operation="reinitialize")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reinitializing camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/recordings", response_model=Dict[str, RecordingInfoResponse])
|
||||
async def get_recordings():
|
||||
"""Get all recording sessions"""
|
||||
@@ -266,14 +343,14 @@ class APIServer:
|
||||
file_size_bytes=recording.file_size_bytes,
|
||||
frame_count=recording.frame_count,
|
||||
duration_seconds=(recording.end_time - recording.start_time).total_seconds() if recording.end_time else None,
|
||||
error_message=recording.error_message
|
||||
error_message=recording.error_message,
|
||||
)
|
||||
for rid, recording in recordings.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting recordings: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/storage/stats", response_model=StorageStatsResponse)
|
||||
async def get_storage_stats():
|
||||
"""Get storage statistics"""
|
||||
@@ -283,34 +360,26 @@ class APIServer:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting storage stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/storage/files", response_model=FileListResponse)
|
||||
async def get_files(request: FileListRequest):
|
||||
"""Get list of recording files"""
|
||||
try:
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
|
||||
if request.start_date:
|
||||
start_date = datetime.fromisoformat(request.start_date)
|
||||
if request.end_date:
|
||||
end_date = datetime.fromisoformat(request.end_date)
|
||||
|
||||
files = self.storage_manager.get_recording_files(
|
||||
camera_name=request.camera_name,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=request.limit
|
||||
)
|
||||
|
||||
return FileListResponse(
|
||||
files=files,
|
||||
total_count=len(files)
|
||||
)
|
||||
|
||||
files = self.storage_manager.get_recording_files(camera_name=request.camera_name, start_date=start_date, end_date=end_date, limit=request.limit)
|
||||
|
||||
return FileListResponse(files=files, total_count=len(files))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting files: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/storage/cleanup", response_model=CleanupResponse)
|
||||
async def cleanup_storage(request: CleanupRequest):
|
||||
"""Clean up old storage files"""
|
||||
@@ -320,7 +389,7 @@ class APIServer:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during cleanup: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for real-time updates"""
|
||||
@@ -330,9 +399,7 @@ class APIServer:
|
||||
# Keep connection alive and handle incoming messages
|
||||
data = await websocket.receive_text()
|
||||
# Echo back for now - could implement commands later
|
||||
await self.websocket_manager.send_personal_message(
|
||||
{"type": "echo", "data": data}, websocket
|
||||
)
|
||||
await self.websocket_manager.send_personal_message({"type": "echo", "data": data}, websocket)
|
||||
except WebSocketDisconnect:
|
||||
self.websocket_manager.disconnect(websocket)
|
||||
|
||||
@@ -342,21 +409,12 @@ class APIServer:
|
||||
def broadcast_event(event: Event):
|
||||
"""Broadcast event to all WebSocket connections"""
|
||||
try:
|
||||
message = {
|
||||
"type": "event",
|
||||
"event_type": event.event_type.value,
|
||||
"source": event.source,
|
||||
"data": event.data,
|
||||
"timestamp": event.timestamp.isoformat()
|
||||
}
|
||||
message = {"type": "event", "event_type": event.event_type.value, "source": event.source, "data": event.data, "timestamp": event.timestamp.isoformat()}
|
||||
|
||||
# Schedule the broadcast in the event loop thread-safely
|
||||
if self._event_loop and not self._event_loop.is_closed():
|
||||
# Use call_soon_threadsafe to schedule the coroutine from another thread
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.websocket_manager.broadcast(message),
|
||||
self._event_loop
|
||||
)
|
||||
asyncio.run_coroutine_threadsafe(self.websocket_manager.broadcast(message), self._event_loop)
|
||||
else:
|
||||
self.logger.debug("Event loop not available for broadcasting")
|
||||
|
||||
@@ -411,12 +469,7 @@ class APIServer:
|
||||
self._event_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._event_loop)
|
||||
|
||||
uvicorn.run(
|
||||
self.app,
|
||||
host=self.config.system.api_host,
|
||||
port=self.config.system.api_port,
|
||||
log_level="info"
|
||||
)
|
||||
uvicorn.run(self.app, host=self.config.system.api_host, port=self.config.system.api_port, log_level="info")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error running API server: {e}")
|
||||
finally:
|
||||
@@ -429,11 +482,4 @@ class APIServer:
|
||||
|
||||
def get_server_info(self) -> Dict[str, Any]:
|
||||
"""Get server information"""
|
||||
return {
|
||||
"running": self.running,
|
||||
"host": self.config.system.api_host,
|
||||
"port": self.config.system.api_port,
|
||||
"start_time": self.server_start_time.isoformat(),
|
||||
"uptime_seconds": (datetime.now() - self.server_start_time).total_seconds(),
|
||||
"websocket_connections": len(self.websocket_manager.active_connections)
|
||||
}
|
||||
return {"running": self.running, "host": self.config.system.api_host, "port": self.config.system.api_port, "start_time": self.server_start_time.isoformat(), "uptime_seconds": (datetime.now() - self.server_start_time).total_seconds(), "websocket_connections": len(self.websocket_manager.active_connections)}
|
||||
|
||||
Reference in New Issue
Block a user