""" Configuration API for managing GSPro Remote settings. """ import logging from typing import Any, Dict, Optional from fastapi import APIRouter, BackgroundTasks, HTTPException, Query from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from ..core.config import get_config, reset_config logger = logging.getLogger(__name__) router = APIRouter() class ConfigUpdateRequest(BaseModel): """Request model for configuration updates.""" server: Optional[Dict[str, Any]] = Field(None, description="Server configuration") capture: Optional[Dict[str, Any]] = Field(None, description="Capture configuration") gspro: Optional[Dict[str, Any]] = Field(None, description="GSPro configuration") vision: Optional[Dict[str, Any]] = Field(None, description="Vision configuration") debug: Optional[bool] = Field(None, description="Debug mode") class ConfigResponse(BaseModel): """Response model for configuration endpoints.""" success: bool message: str config: Optional[Dict[str, Any]] = None class CaptureConfigUpdate(BaseModel): """Request model for capture configuration updates.""" fps: Optional[int] = Field(None, ge=1, le=60) quality: Optional[int] = Field(None, ge=1, le=100) resolution: Optional[str] = None region_x: Optional[int] = Field(None, ge=0) region_y: Optional[int] = Field(None, ge=0) region_width: Optional[int] = Field(None, gt=0) region_height: Optional[int] = Field(None, gt=0) class ServerConfigUpdate(BaseModel): """Request model for server configuration updates.""" host: Optional[str] = None port: Optional[int] = None mdns_enabled: Optional[bool] = None class GSProConfigUpdate(BaseModel): """Request model for GSPro configuration updates.""" window_title: Optional[str] = None auto_focus: Optional[bool] = None key_delay: Optional[float] = Field(None, ge=0, le=1) class VisionConfigUpdate(BaseModel): """Request model for vision configuration updates.""" enabled: Optional[bool] = None ocr_engine: Optional[str] = None confidence_threshold: Optional[float] = Field(None, ge=0, le=1) @router.get("/", response_model=Dict[str, Any]) async def get_configuration(): """ Get the current application configuration. Returns all configuration sections including server, capture, gspro, and vision settings. """ config = get_config() return config.to_dict() @router.put("/", response_model=ConfigResponse) async def update_configuration(request: ConfigUpdateRequest): """ Update application configuration. Only provided fields will be updated. Nested configuration objects are merged. """ config = get_config() try: update_dict = request.model_dump(exclude_none=True) if not update_dict: return ConfigResponse(success=False, message="No configuration changes provided") # Update configuration config.update(**update_dict) logger.info(f"Configuration updated: {list(update_dict.keys())}") return ConfigResponse(success=True, message="Configuration updated successfully", config=config.to_dict()) except Exception as e: logger.error(f"Failed to update configuration: {e}") raise HTTPException(status_code=500, detail=f"Failed to update configuration: {str(e)}") @router.post("/save", response_model=ConfigResponse) async def save_configuration(): """ Save the current configuration to disk. This ensures changes persist across application restarts. """ config = get_config() try: config.save() return ConfigResponse( success=True, message=f"Configuration saved to {config.config_path}", config=config.to_dict() ) except Exception as e: logger.error(f"Failed to save configuration: {e}") raise HTTPException(status_code=500, detail=f"Failed to save configuration: {str(e)}") @router.post("/reset", response_model=ConfigResponse) async def reset_configuration(): """ Reset configuration to default values. This will overwrite any custom settings with the application defaults. """ config = get_config() try: config.reset() logger.info("Configuration reset to defaults") return ConfigResponse(success=True, message="Configuration reset to defaults", config=config.to_dict()) except Exception as e: logger.error(f"Failed to reset configuration: {e}") raise HTTPException(status_code=500, detail=f"Failed to reset configuration: {str(e)}") @router.get("/server") async def get_server_config(): """Get server-specific configuration.""" config = get_config() return config.server.model_dump() @router.put("/server") async def update_server_config(request: ServerConfigUpdate): """ Update server configuration. Note: Changing host or port requires application restart to take effect. """ config = get_config() updates = request.model_dump(exclude_none=True) if not updates: return {"success": False, "message": "No updates provided"} try: config.update(server=updates) return { "success": True, "message": "Server configuration updated (restart required for host/port changes)", "config": config.server.model_dump(), } except Exception as e: logger.error(f"Failed to update server config: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/capture") async def get_capture_config(): """Get capture/streaming configuration.""" config = get_config() return config.capture.model_dump() @router.put("/capture") async def update_capture_config(request: CaptureConfigUpdate): """Update capture/streaming configuration.""" config = get_config() updates = request.model_dump(exclude_none=True) if not updates: return {"success": False, "message": "No updates provided"} try: config.update(capture=updates) return {"success": True, "message": "Capture configuration updated", "config": config.capture.model_dump()} except Exception as e: logger.error(f"Failed to update capture config: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/gspro") async def get_gspro_config(): """Get GSPro-specific configuration.""" config = get_config() return config.gspro.model_dump() @router.put("/gspro") async def update_gspro_config(request: GSProConfigUpdate): """Update GSPro-specific configuration.""" config = get_config() updates = request.model_dump(exclude_none=True) if not updates: return {"success": False, "message": "No updates provided"} try: config.update(gspro=updates) return {"success": True, "message": "GSPro configuration updated", "config": config.gspro.model_dump()} except Exception as e: logger.error(f"Failed to update GSPro config: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/vision") async def get_vision_config(): """Get vision/OCR configuration (V2 features).""" config = get_config() return {**config.vision.model_dump(), "note": "Vision features are planned for V2"} @router.put("/vision") async def update_vision_config(request: VisionConfigUpdate): """ Update vision/OCR configuration. Note: These features are planned for V2 and not yet implemented. """ config = get_config() updates = request.model_dump(exclude_none=True) if not updates: return {"success": False, "message": "No updates provided"} # Validate OCR engine if provided if "ocr_engine" in updates and updates["ocr_engine"] not in ["easyocr", "tesseract"]: raise HTTPException(status_code=400, detail="Invalid OCR engine. Must be 'easyocr' or 'tesseract'") try: config.update(vision=updates) return { "success": True, "message": "Vision configuration updated (V2 features)", "config": config.vision.model_dump(), } except Exception as e: logger.error(f"Failed to update vision config: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/reload", response_model=ConfigResponse) async def reload_configuration(): """ Reload configuration from disk. This discards any unsaved changes and reloads from the configuration file. """ try: # Reset the global config instance reset_config() # Get new config (will load from disk) config = get_config() logger.info("Configuration reloaded from disk") return ConfigResponse( success=True, message=f"Configuration reloaded from {config.config_path}", config=config.to_dict() ) except Exception as e: logger.error(f"Failed to reload configuration: {e}") raise HTTPException(status_code=500, detail=f"Failed to reload configuration: {str(e)}") @router.get("/path") async def get_config_path(): """Get the configuration file path.""" config = get_config() return {"path": str(config.config_path), "exists": config.config_path.exists() if config.config_path else False} @router.get("/validate") async def validate_configuration(): """ Validate the current configuration. Checks for common issues and required settings. """ config = get_config() issues = [] warnings = [] # Check server configuration if config.server.port < 1024: warnings.append("Server port is below 1024, may require administrator privileges") # Check capture configuration if config.capture.fps > 30: warnings.append("High FPS may impact performance") if config.capture.quality < 50: warnings.append("Low JPEG quality may result in poor image clarity") # Check GSPro configuration if not config.gspro.window_title: issues.append("GSPro window title is not set") # Check vision configuration if config.vision.enabled: warnings.append("Vision features are enabled but not yet implemented (V2)") return { "valid": len(issues) == 0, "issues": issues, "warnings": warnings, "summary": f"{len(issues)} issues, {len(warnings)} warnings", }