""" Camera-related API routes. """ import logging import os from typing import Dict from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse from ...core.config import Config from ...core.state_manager import StateManager from ...camera.manager import CameraManager from ..models import ( CameraStatusResponse, CameraTestResponse, CameraConfigResponse, CameraConfigRequest, CameraRecoveryResponse, MachineStatusResponse, ) def register_camera_routes( app: FastAPI, config: Config, state_manager: StateManager, camera_manager: CameraManager, logger: logging.Logger ): """Register camera-related routes""" @app.get("/machines", response_model=Dict[str, MachineStatusResponse]) async def get_machines(): """Get all machine statuses""" try: machines = 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() } except Exception as e: logger.error(f"Error getting machines: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/cameras", response_model=Dict[str, CameraStatusResponse]) async def get_cameras(): """Get all camera statuses""" try: cameras = 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, auto_recording_enabled=camera.auto_recording_enabled, auto_recording_active=camera.auto_recording_active, auto_recording_failure_count=camera.auto_recording_failure_count, auto_recording_last_attempt=camera.auto_recording_last_attempt.isoformat() if camera.auto_recording_last_attempt else None, auto_recording_last_error=camera.auto_recording_last_error, ) for name, camera in cameras.items() } except Exception as e: logger.error(f"Error getting cameras: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/cameras/{camera_name}/status", response_model=CameraStatusResponse) async def get_camera_status(camera_name: str): """Get specific camera status""" try: camera = 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 ) except HTTPException: raise except Exception as e: logger.error(f"Error getting camera status: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/test-connection", response_model=CameraTestResponse) async def test_camera_connection(camera_name: str): """Test camera connection""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = 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: logger.error(f"Error testing camera connection: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/cameras/{camera_name}/stream") async def camera_stream(camera_name: str): """Get live MJPEG stream from camera""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") # Get camera streamer streamer = camera_manager.get_camera_streamer(camera_name) if not streamer: raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found") # Start streaming if not already active if not streamer.is_streaming(): success = streamer.start_streaming() if not success: raise HTTPException(status_code=500, detail=f"Failed to start streaming for camera {camera_name}") # Return MJPEG stream return StreamingResponse(streamer.get_frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame") except HTTPException: raise except Exception as e: logger.error(f"Error starting camera stream: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/start-stream") async def start_camera_stream(camera_name: str): """Start streaming for a camera""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = camera_manager.start_camera_streaming(camera_name) if success: return {"success": True, "message": f"Started streaming for camera {camera_name}"} else: return {"success": False, "message": f"Failed to start streaming for camera {camera_name}"} except Exception as e: logger.error(f"Error starting camera stream: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/stop-stream") async def stop_camera_stream(camera_name: str): """Stop streaming for a camera""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = camera_manager.stop_camera_streaming(camera_name) if success: return {"success": True, "message": f"Stopped streaming for camera {camera_name}"} else: return {"success": False, "message": f"Failed to stop streaming for camera {camera_name}"} except Exception as e: logger.error(f"Error stopping camera stream: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/start-rtsp") async def start_camera_rtsp_stream(camera_name: str): """Start RTSP streaming for a camera to MediaMTX""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = camera_manager.start_camera_rtsp_streaming(camera_name) if success: rtsp_url = f"rtsp://{os.getenv('MEDIAMTX_HOST', 'localhost')}:{os.getenv('MEDIAMTX_RTSP_PORT', '8554')}/{camera_name}" return { "success": True, "message": f"Started RTSP streaming for camera {camera_name}", "rtsp_url": rtsp_url } else: return {"success": False, "message": f"Failed to start RTSP streaming for camera {camera_name}"} except Exception as e: logger.error(f"Error starting RTSP stream: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/stop-rtsp") async def stop_camera_rtsp_stream(camera_name: str): """Stop RTSP streaming for a camera""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = camera_manager.stop_camera_rtsp_streaming(camera_name) if success: return {"success": True, "message": f"Stopped RTSP streaming for camera {camera_name}"} else: return {"success": False, "message": f"Failed to stop RTSP streaming for camera {camera_name}"} except Exception as e: logger.error(f"Error stopping RTSP stream: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/cameras/{camera_name}/config", response_model=CameraConfigResponse) async def get_camera_config(camera_name: str): """Get camera configuration""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") camera_config = config.get_camera_by_name(camera_name) if not camera_config: raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found") return CameraConfigResponse( name=camera_config.name, machine_topic=camera_config.machine_topic, storage_path=camera_config.storage_path, enabled=camera_config.enabled, # Auto-recording settings auto_start_recording_enabled=camera_config.auto_start_recording_enabled, auto_recording_max_retries=camera_config.auto_recording_max_retries, auto_recording_retry_delay_seconds=camera_config.auto_recording_retry_delay_seconds, # Basic settings exposure_ms=camera_config.exposure_ms, gain=camera_config.gain, target_fps=camera_config.target_fps, # Video recording settings video_format=camera_config.video_format, video_codec=camera_config.video_codec, video_quality=camera_config.video_quality, # Image Quality Settings sharpness=camera_config.sharpness, contrast=camera_config.contrast, saturation=camera_config.saturation, gamma=camera_config.gamma, # Noise Reduction noise_filter_enabled=camera_config.noise_filter_enabled, denoise_3d_enabled=camera_config.denoise_3d_enabled, # Color Settings auto_white_balance=camera_config.auto_white_balance, color_temperature_preset=camera_config.color_temperature_preset, # Manual White Balance RGB Gains wb_red_gain=camera_config.wb_red_gain, wb_green_gain=camera_config.wb_green_gain, wb_blue_gain=camera_config.wb_blue_gain, # Advanced Settings anti_flicker_enabled=camera_config.anti_flicker_enabled, light_frequency=camera_config.light_frequency, bit_depth=camera_config.bit_depth, # HDR Settings hdr_enabled=camera_config.hdr_enabled, hdr_gain_mode=camera_config.hdr_gain_mode, ) except HTTPException: raise except Exception as e: logger.error(f"Error getting camera config: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.put("/cameras/{camera_name}/config") async def update_camera_config(camera_name: str, request: CameraConfigRequest): """Update camera configuration""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") # Convert request to dict, excluding None values config_updates = {k: v for k, v in request.dict().items() if v is not None} if not config_updates: raise HTTPException(status_code=400, detail="No configuration updates provided") success = camera_manager.update_camera_config(camera_name, **config_updates) if success: return { "success": True, "message": f"Camera {camera_name} configuration updated", "updated_settings": list(config_updates.keys()) } else: raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found or update failed") except HTTPException: raise except Exception as e: logger.error(f"Error updating camera config: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/apply-config") async def apply_camera_config(camera_name: str): """Apply current configuration to active camera (requires camera restart)""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = camera_manager.apply_camera_config(camera_name) if success: return {"success": True, "message": f"Configuration applied to camera {camera_name}"} else: return {"success": False, "message": f"Failed to apply configuration to camera {camera_name}"} except Exception as e: logger.error(f"Error applying camera config: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/reconnect", response_model=CameraRecoveryResponse) async def reconnect_camera(camera_name: str): """Reconnect to a camera""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = 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: logger.error(f"Error reconnecting camera: {e}") raise HTTPException(status_code=500, detail=str(e)) @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 camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = 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: logger.error(f"Error restarting camera grab: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/reset-timestamp", response_model=CameraRecoveryResponse) async def reset_camera_timestamp(camera_name: str): """Reset camera timestamp""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = 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: logger.error(f"Error resetting camera timestamp: {e}") raise HTTPException(status_code=500, detail=str(e)) @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 camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = 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: logger.error(f"Error performing full camera reset: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/cameras/{camera_name}/reinitialize", response_model=CameraRecoveryResponse) async def reinitialize_camera(camera_name: str): """Reinitialize a failed camera""" try: if not camera_manager: raise HTTPException(status_code=503, detail="Camera manager not available") success = 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: logger.error(f"Error reinitializing camera: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/debug/camera-manager") async def debug_camera_manager(): """Debug endpoint to check camera manager state""" try: if not camera_manager: return {"error": "Camera manager not available"} return { "available_cameras": len(camera_manager.available_cameras), "camera_recorders": list(camera_manager.camera_recorders.keys()), "camera_streamers": list(camera_manager.camera_streamers.keys()), "streamer_states": { name: { "exists": streamer is not None, "is_streaming": streamer.is_streaming() if streamer else False, "streaming": getattr(streamer, 'streaming', False) if streamer else False } for name, streamer in camera_manager.camera_streamers.items() } } except Exception as e: return {"error": str(e)}