"""Display brightness and power control service.""" import ctypes import ctypes.wintypes import logging import platform import struct import time from dataclasses import dataclass, field logger = logging.getLogger(__name__) _sbc = None _monitorcontrol = None def _load_sbc(): global _sbc if _sbc is None: try: import screen_brightness_control as sbc _sbc = sbc except ImportError: logger.warning("screen_brightness_control not installed - brightness control unavailable") return _sbc def _load_monitorcontrol(): global _monitorcontrol if _monitorcontrol is None: try: import monitorcontrol _monitorcontrol = monitorcontrol except ImportError: logger.warning("monitorcontrol not installed - display power control unavailable") return _monitorcontrol def _parse_edid_resolution(edid_hex: str) -> str | None: """Parse resolution from EDID hex string (first detailed timing descriptor).""" try: edid = bytes.fromhex(edid_hex) if len(edid) < 58: return None dtd = edid[54:] pixel_clock = struct.unpack('> 4) << 8) v_active = dtd[5] | ((dtd[7] >> 4) << 8) return f"{h_active}x{v_active}" except Exception: return None @dataclass class MonitorInfo: id: int name: str brightness: int | None power_supported: bool power_on: bool = True model: str = "" manufacturer: str = "" resolution: str | None = None is_primary: bool = False def to_dict(self) -> dict: return { "id": self.id, "name": self.name, "brightness": self.brightness, "power_supported": self.power_supported, "power_on": self.power_on, "model": self.model, "manufacturer": self.manufacturer, "resolution": self.resolution, "is_primary": self.is_primary, } def _detect_primary_resolution() -> str | None: """Detect the primary display resolution via Windows API.""" if platform.system() != "Windows": return None try: class MONITORINFO(ctypes.Structure): _fields_ = [ ("cbSize", ctypes.wintypes.DWORD), ("rcMonitor", ctypes.wintypes.RECT), ("rcWork", ctypes.wintypes.RECT), ("dwFlags", ctypes.wintypes.DWORD), ] MONITORINFOF_PRIMARY = 1 primary_res = None def callback(hmon, hdc, rect, data): nonlocal primary_res mi = MONITORINFO() mi.cbSize = ctypes.sizeof(mi) ctypes.windll.user32.GetMonitorInfoW(hmon, ctypes.byref(mi)) if mi.dwFlags & MONITORINFOF_PRIMARY: w = mi.rcMonitor.right - mi.rcMonitor.left h = mi.rcMonitor.bottom - mi.rcMonitor.top primary_res = f"{w}x{h}" return True MONITORENUMPROC = ctypes.WINFUNCTYPE( ctypes.c_int, ctypes.wintypes.HMONITOR, ctypes.wintypes.HDC, ctypes.POINTER(ctypes.wintypes.RECT), ctypes.wintypes.LPARAM, ) ctypes.windll.user32.EnumDisplayMonitors( None, None, MONITORENUMPROC(callback), 0 ) return primary_res except Exception: return None def _mark_primary(monitors: list[MonitorInfo]) -> None: """Mark the primary display in the monitor list.""" if not monitors: return primary_res = _detect_primary_resolution() if primary_res: for m in monitors: if m.resolution == primary_res: m.is_primary = True return # Fallback: mark first monitor as primary monitors[0].is_primary = True # Cache for monitor list _monitor_cache: list[MonitorInfo] | None = None _cache_time: float = 0 _CACHE_TTL = 5.0 # seconds def list_monitors(force_refresh: bool = False) -> list[MonitorInfo]: """List all connected monitors with their current brightness.""" global _monitor_cache, _cache_time if not force_refresh and _monitor_cache is not None and (time.time() - _cache_time) < _CACHE_TTL: return _monitor_cache sbc = _load_sbc() if sbc is None: return [] monitors = [] try: info_list = sbc.list_monitors_info() brightnesses = sbc.get_brightness() # Get DDC/CI monitors for power state mc = _load_monitorcontrol() ddc_monitors = [] if mc: try: ddc_monitors = mc.get_monitors() except Exception: pass for i, info in enumerate(info_list): name = info.get("name", f"Monitor {i}") model = info.get("model", "") manufacturer = info.get("manufacturer", "") brightness = brightnesses[i] if i < len(brightnesses) else None # VCP method monitors support DDC/CI power control method = str(info.get("method", "")).lower() power_supported = "vcp" in method # Parse resolution from EDID edid = info.get("edid", "") resolution = _parse_edid_resolution(edid) if edid else None # Read power state via DDC/CI power_on = True if power_supported and i < len(ddc_monitors): try: with ddc_monitors[i] as mon: power_mode = mon.get_power_mode() power_on = power_mode == mc.PowerMode.on except Exception: pass monitors.append(MonitorInfo( id=i, name=name, brightness=brightness, power_supported=power_supported, power_on=power_on, model=model, manufacturer=manufacturer, resolution=resolution, )) except Exception as e: logger.error("Failed to enumerate monitors: %s", e) _mark_primary(monitors) _monitor_cache = monitors _cache_time = time.time() return monitors def get_brightness(monitor_id: int) -> int | None: """Get brightness for a specific monitor.""" sbc = _load_sbc() if sbc is None: return None try: result = sbc.get_brightness(display=monitor_id) if isinstance(result, list): return result[0] if result else None return result except Exception as e: logger.error("Failed to get brightness for monitor %d: %s", monitor_id, e) return None def set_brightness(monitor_id: int, value: int) -> bool: """Set brightness for a specific monitor (0-100).""" sbc = _load_sbc() if sbc is None: return False value = max(0, min(100, value)) try: sbc.set_brightness(value, display=monitor_id) # Invalidate cache global _monitor_cache _monitor_cache = None return True except Exception as e: logger.error("Failed to set brightness for monitor %d: %s", monitor_id, e) return False def set_power(monitor_id: int, on: bool) -> bool: """Turn a monitor on or off via DDC/CI.""" mc = _load_monitorcontrol() if mc is None: logger.error("monitorcontrol not available for power control") return False try: ddc_monitors = mc.get_monitors() if monitor_id >= len(ddc_monitors): logger.error("Monitor %d not found in DDC/CI monitors", monitor_id) return False with ddc_monitors[monitor_id] as monitor: if on: monitor.set_power_mode(mc.PowerMode.on) else: monitor.set_power_mode(mc.PowerMode.off_soft) # Invalidate cache global _monitor_cache _monitor_cache = None return True except Exception as e: logger.error("Failed to set power for monitor %d: %s", monitor_id, e) return False