gsproremote/backend/app/core/config.py

193 lines
6.7 KiB
Python

"""
Configuration management for GSPro Remote.
"""
import json
import logging
from pathlib import Path
from typing import Optional
from functools import lru_cache
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
logger = logging.getLogger(__name__)
class ServerConfig(BaseModel):
"""Server configuration settings."""
host: str = Field("0.0.0.0", description="Server host address")
port: int = Field(5005, description="Server port")
mdns_enabled: bool = Field(True, description="Enable mDNS service discovery")
class CaptureConfig(BaseModel):
"""Screen capture configuration settings."""
fps: int = Field(30, description="Frames per second for streaming")
quality: int = Field(85, description="JPEG quality (0-100)")
resolution: str = Field("720p", description="Stream resolution")
region_x: int = Field(0, description="Map region X coordinate")
region_y: int = Field(0, description="Map region Y coordinate")
region_width: int = Field(640, description="Map region width")
region_height: int = Field(480, description="Map region height")
class GSProConfig(BaseModel):
"""GSPro application configuration settings."""
window_title: str = Field("GSPro", description="GSPro window title")
auto_focus: bool = Field(True, description="Auto-focus GSPro window before sending keys")
key_delay: float = Field(0.05, description="Default delay between key presses (seconds)")
class VisionConfig(BaseModel):
"""Computer vision configuration settings (for V2 features)."""
enabled: bool = Field(False, description="Enable vision features")
ocr_engine: str = Field("easyocr", description="OCR engine to use (easyocr or tesseract)")
confidence_threshold: float = Field(0.7, description="Minimum confidence for OCR detection")
class AppConfig(BaseSettings):
"""Main application configuration."""
server: ServerConfig = Field(default_factory=ServerConfig)
capture: CaptureConfig = Field(default_factory=CaptureConfig)
gspro: GSProConfig = Field(default_factory=GSProConfig)
vision: VisionConfig = Field(default_factory=VisionConfig)
config_path: Optional[Path] = None
debug: bool = Field(False, description="Enable debug mode")
class Config:
env_prefix = "GSPRO_REMOTE_"
env_nested_delimiter = "__"
case_sensitive = False
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.config_path is None:
self.config_path = self._get_default_config_path()
self.load()
@staticmethod
def _get_default_config_path() -> Path:
"""Get the default configuration file path."""
import os
if os.name == "nt": # Windows
base_path = Path(os.environ.get("LOCALAPPDATA", ""))
if not base_path:
base_path = Path.home() / "AppData" / "Local"
else: # Unix-like
base_path = Path.home() / ".config"
config_dir = base_path / "GSPro Remote"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir / "config.json"
def load(self) -> None:
"""Load configuration from file."""
if self.config_path and self.config_path.exists():
try:
with open(self.config_path, "r") as f:
data = json.load(f)
# Update configuration with loaded data
if "server" in data:
self.server = ServerConfig(**data["server"])
if "capture" in data:
self.capture = CaptureConfig(**data["capture"])
if "gspro" in data:
self.gspro = GSProConfig(**data["gspro"])
if "vision" in data:
self.vision = VisionConfig(**data["vision"])
if "debug" in data:
self.debug = data["debug"]
logger.info(f"Configuration loaded from {self.config_path}")
except Exception as e:
logger.warning(f"Failed to load configuration: {e}")
self.save() # Save default configuration
else:
# Create default configuration file
self.save()
logger.info(f"Created default configuration at {self.config_path}")
def save(self) -> None:
"""Save configuration to file."""
if self.config_path:
try:
self.config_path.parent.mkdir(parents=True, exist_ok=True)
data = {
"server": self.server.model_dump(),
"capture": self.capture.model_dump(),
"gspro": self.gspro.model_dump(),
"vision": self.vision.model_dump(),
"debug": self.debug,
}
with open(self.config_path, "w") as f:
json.dump(data, f, indent=2)
logger.info(f"Configuration saved to {self.config_path}")
except Exception as e:
logger.error(f"Failed to save configuration: {e}")
def update(self, **kwargs) -> None:
"""Update configuration with new values."""
for key, value in kwargs.items():
if hasattr(self, key):
if isinstance(value, dict):
# Update nested configuration
current = getattr(self, key)
if isinstance(current, BaseModel):
for sub_key, sub_value in value.items():
if hasattr(current, sub_key):
setattr(current, sub_key, sub_value)
else:
setattr(self, key, value)
self.save()
def reset(self) -> None:
"""Reset configuration to defaults."""
self.server = ServerConfig()
self.capture = CaptureConfig()
self.gspro = GSProConfig()
self.vision = VisionConfig()
self.debug = False
self.save()
def to_dict(self) -> dict:
"""Convert configuration to dictionary."""
return {
"server": self.server.model_dump(),
"capture": self.capture.model_dump(),
"gspro": self.gspro.model_dump(),
"vision": self.vision.model_dump(),
"debug": self.debug,
"config_path": str(self.config_path) if self.config_path else None,
}
# Global configuration instance
_config: Optional[AppConfig] = None
@lru_cache(maxsize=1)
def get_config() -> AppConfig:
"""Get the global configuration instance."""
global _config
if _config is None:
_config = AppConfig()
return _config
def reset_config() -> None:
"""Reset the global configuration instance."""
global _config
_config = None
get_config.cache_clear()