Files
KVM_Switch/app/config.py
T

117 lines
3.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
import json
import os
from pathlib import Path
from threading import Lock
PROJECT_ROOT = Path(__file__).resolve().parent.parent
CONFIG_PATH = PROJECT_ROOT / "config.json"
DEVICE_ROLE_ENV_VAR = "KVM_DEVICE_ROLE"
TOWER_TRIGGER_CODE = 15
LAPTOP_TRIGGER_CODE = 19
SUPPORTED_DEVICE_ROLES = {"tower", "laptop"}
SUPPORTED_TARGET_PORTS = {
"DP1": {"ddm_input": "DP1", "input_codes": {15}},
"DP2": {"ddm_input": "DP2", "input_codes": {19}},
"HDMI": {"ddm_input": "HDMI", "input_codes": {17}},
}
@dataclass(slots=True)
class AppConfig:
device_role: str | None = None
device_port: str = "DP1"
@classmethod
def from_dict(cls, data: dict[str, object], default_role: str | None = None) -> "AppConfig":
return cls(
device_role=_coerce_role_name(data.get("device_role"), default_role),
device_port=_coerce_port_name(data.get("device_port"), "DP1"),
)
def to_dict(self) -> dict[str, str | None]:
return {
"device_role": self.device_role,
"device_port": self.device_port,
}
def validate(self) -> list[str]:
errors: list[str] = []
if self.device_role not in SUPPORTED_DEVICE_ROLES:
errors.append(
f"Device role is not configured. Set it in the UI, set {DEVICE_ROLE_ENV_VAR} to TOWER or LAPTOP, or add device_role to config.json."
)
supported = ", ".join(SUPPORTED_TARGET_PORTS)
if self.device_port not in SUPPORTED_TARGET_PORTS:
errors.append(f"Device Port must be one of: {supported}.")
return errors
class ConfigStore:
def __init__(self, path: Path = CONFIG_PATH):
self.path = path
self._lock = Lock()
self._default_role = _coerce_role_name(os.environ.get(DEVICE_ROLE_ENV_VAR), None)
self._config = self._load_from_disk()
def get(self) -> AppConfig:
with self._lock:
return AppConfig(
device_role=self._config.device_role,
device_port=self._config.device_port,
)
def save(self, config: AppConfig) -> AppConfig:
errors = config.validate()
if errors:
raise ValueError("; ".join(errors))
with self._lock:
self.path.write_text(
json.dumps(config.to_dict(), indent=2),
encoding="utf-8",
)
self._config = AppConfig(
device_role=config.device_role,
device_port=config.device_port,
)
return AppConfig(
device_role=self._config.device_role,
device_port=self._config.device_port,
)
def _load_from_disk(self) -> AppConfig:
if not self.path.exists():
return AppConfig(device_role=self._default_role)
try:
data = json.loads(self.path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return AppConfig(device_role=self._default_role)
if not isinstance(data, dict):
return AppConfig(device_role=self._default_role)
return AppConfig.from_dict(data, default_role=self._default_role)
def _coerce_port_name(value: object, default: str) -> str:
if isinstance(value, str):
normalized = value.strip().upper()
if normalized in SUPPORTED_TARGET_PORTS:
return normalized
return default
def _coerce_role_name(value: object, default: str | None) -> str | None:
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in SUPPORTED_DEVICE_ROLES:
return normalized
return default