gsproremote/backend/app/core/config.py

194 lines
6.7 KiB
Python
Raw Permalink Normal View History

"""
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()