Files

124 lines
3.9 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from threading import Lock
from typing import Protocol
from monitorcontrol import get_monitors
from app.config import SUPPORTED_TARGET_PORTS
ALIENWARE_MODEL_TOKEN = "AW3423DWF"
@dataclass(slots=True)
class DDMCommandResult:
success: bool
message: str
raw_output: str = ""
class DDMBackend(Protocol):
def is_available(self) -> bool: ...
def supports_monitor_targeting(self) -> bool: ...
def resolve_alienware_slot(self, force: bool = False) -> int | None: ...
def invalidate_slot(self) -> None: ...
def switch_to_port(self, slot: int, port_name: str) -> DDMCommandResult: ...
class RealDDMBackend:
def __init__(
self,
alienware_model_token: str = ALIENWARE_MODEL_TOKEN,
):
self.alienware_model_token = alienware_model_token
self._cached_slot: int | None = None
self._lock = Lock()
def is_available(self) -> bool:
_, error = self._resolve_alienware_monitor()
return error is None
def supports_monitor_targeting(self) -> bool:
return True
def invalidate_slot(self) -> None:
with self._lock:
self._cached_slot = None
def resolve_alienware_slot(self, force: bool = False) -> int | None:
with self._lock:
if self._cached_slot is not None and not force:
return self._cached_slot
monitor, error = self._resolve_alienware_monitor()
del monitor
if error is not None:
self._cached_slot = None
return None
# Service still expects a slot-like value. With monitorcontrol we
# target by model, so we expose a synthetic stable slot.
self._cached_slot = 1
return self._cached_slot
def switch_to_port(self, slot: int, port_name: str) -> DDMCommandResult:
del slot
port_spec = SUPPORTED_TARGET_PORTS.get(port_name.upper())
if not port_spec:
return DDMCommandResult(False, f"Port {port_name} is not supported.")
target_input = _to_monitor_input_value(str(port_spec["ddm_input"]))
monitor, error = self._resolve_alienware_monitor()
if error is not None:
return DDMCommandResult(False, error)
assert monitor is not None
try:
with monitor:
monitor.set_input_source(target_input)
except Exception as exc:
return DDMCommandResult(
False,
f"monitorcontrol failed to switch Alienware input to {target_input}: {exc}",
str(exc),
)
return DDMCommandResult(
True,
f"Alienware {self.alienware_model_token} was instructed to switch to {target_input} via monitorcontrol.",
)
def _resolve_alienware_monitor(self) -> tuple[object | None, str | None]:
try:
monitors = list(get_monitors())
except Exception as exc:
return None, f"monitorcontrol could not enumerate monitors: {exc}"
candidates: list[object] = []
token = self.alienware_model_token.upper()
for monitor in monitors:
description = str(getattr(getattr(monitor, "vcp", None), "description", "") or str(monitor))
if token in description.upper():
candidates.append(monitor)
if not candidates:
return None, f"Alienware {self.alienware_model_token} was not found via monitorcontrol."
if len(candidates) > 1:
return None, f"Multiple Alienware {self.alienware_model_token} monitors were detected; refusing to switch."
return candidates[0], None
def _to_monitor_input_value(port_name: str) -> int | str:
normalized = port_name.strip().upper()
if normalized == "DP2":
# Alienware AW3423DWF uses vendor-specific DP2 code.
return 19
if normalized == "HDMI":
return "HDMI1"
return normalized