gsproremote/backend/app/api/config.py

348 lines
10 KiB
Python

"""
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",
}