c1259a9a7f
Validate / validate (push) Failing after 8s
- Added get_monitor_refresh_rates() function in monitor_names.py using Windows ctypes/DEVMODE to detect monitor refresh rates - Updated DisplayInfo dataclass and Pydantic schema to include refresh_rate field (in Hz) - Modified get_available_displays() to detect and include refresh rates (defaults to 60Hz on non-Windows or if detection fails) - Added refresh rate display in Web UI between Resolution and Position - Added translations for refresh rate label (displays.refresh_rate) in English and Russian locales - Cross-platform compatible: gracefully falls back to 60Hz default on non-Windows systems Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
167 lines
5.7 KiB
Python
167 lines
5.7 KiB
Python
"""Utility functions for retrieving friendly monitor/display names."""
|
|
|
|
import sys
|
|
from typing import Dict, Optional
|
|
|
|
from wled_controller.utils import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
def get_monitor_names() -> Dict[int, str]:
|
|
"""Get friendly names for connected monitors.
|
|
|
|
On Windows, attempts to retrieve monitor names from WMI.
|
|
On other platforms, returns empty dict (will fall back to generic names).
|
|
|
|
Returns:
|
|
Dictionary mapping display indices to friendly names
|
|
"""
|
|
if sys.platform != "win32":
|
|
logger.debug("Monitor name detection only supported on Windows")
|
|
return {}
|
|
|
|
try:
|
|
import wmi
|
|
|
|
w = wmi.WMI(namespace="wmi")
|
|
monitors = w.WmiMonitorID()
|
|
|
|
monitor_names = {}
|
|
|
|
for idx, monitor in enumerate(monitors):
|
|
try:
|
|
# Extract manufacturer name
|
|
manufacturer = ""
|
|
if monitor.ManufacturerName:
|
|
manufacturer = "".join(chr(c) for c in monitor.ManufacturerName if c != 0)
|
|
|
|
# Extract user-friendly name
|
|
user_name = ""
|
|
if monitor.UserFriendlyName:
|
|
user_name = "".join(chr(c) for c in monitor.UserFriendlyName if c != 0)
|
|
|
|
# Build friendly name
|
|
if user_name:
|
|
friendly_name = user_name.strip()
|
|
elif manufacturer:
|
|
friendly_name = f"{manufacturer.strip()} Monitor"
|
|
else:
|
|
friendly_name = f"Display {idx}"
|
|
|
|
monitor_names[idx] = friendly_name
|
|
logger.debug(f"Monitor {idx}: {friendly_name}")
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Failed to parse monitor {idx} name: {e}")
|
|
monitor_names[idx] = f"Display {idx}"
|
|
|
|
return monitor_names
|
|
|
|
except ImportError:
|
|
logger.debug("WMI library not available - install with: pip install wmi")
|
|
return {}
|
|
except Exception as e:
|
|
logger.debug(f"Failed to retrieve monitor names via WMI: {e}")
|
|
return {}
|
|
|
|
|
|
def get_monitor_name(index: int) -> str:
|
|
"""Get friendly name for a specific monitor.
|
|
|
|
Args:
|
|
index: Monitor index (0-based)
|
|
|
|
Returns:
|
|
Friendly monitor name or generic fallback
|
|
"""
|
|
monitor_names = get_monitor_names()
|
|
return monitor_names.get(index, f"Display {index}")
|
|
|
|
|
|
def get_monitor_refresh_rates() -> Dict[int, int]:
|
|
"""Get refresh rates (in Hz) for connected monitors.
|
|
|
|
On Windows, attempts to retrieve refresh rates from display settings.
|
|
On other platforms, returns empty dict (will use default 60Hz).
|
|
|
|
Returns:
|
|
Dictionary mapping display indices to refresh rates in Hz
|
|
"""
|
|
if sys.platform != "win32":
|
|
logger.debug("Refresh rate detection only supported on Windows")
|
|
return {}
|
|
|
|
try:
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
|
|
# Define Windows structures
|
|
class DEVMODE(ctypes.Structure):
|
|
_fields_ = [
|
|
('dmDeviceName', ctypes.c_wchar * 32),
|
|
('dmSpecVersion', wintypes.WORD),
|
|
('dmDriverVersion', wintypes.WORD),
|
|
('dmSize', wintypes.WORD),
|
|
('dmDriverExtra', wintypes.WORD),
|
|
('dmFields', wintypes.DWORD),
|
|
('dmPositionX', wintypes.LONG),
|
|
('dmPositionY', wintypes.LONG),
|
|
('dmDisplayOrientation', wintypes.DWORD),
|
|
('dmDisplayFixedOutput', wintypes.DWORD),
|
|
('dmColor', wintypes.SHORT),
|
|
('dmDuplex', wintypes.SHORT),
|
|
('dmYResolution', wintypes.SHORT),
|
|
('dmTTOption', wintypes.SHORT),
|
|
('dmCollate', wintypes.SHORT),
|
|
('dmFormName', ctypes.c_wchar * 32),
|
|
('dmLogPixels', wintypes.WORD),
|
|
('dmBitsPerPel', wintypes.DWORD),
|
|
('dmPelsWidth', wintypes.DWORD),
|
|
('dmPelsHeight', wintypes.DWORD),
|
|
('dmDisplayFlags', wintypes.DWORD),
|
|
('dmDisplayFrequency', wintypes.DWORD),
|
|
]
|
|
|
|
user32 = ctypes.windll.user32
|
|
refresh_rates = {}
|
|
|
|
# Enumerate all display devices
|
|
idx = 0
|
|
while True:
|
|
device_name = ctypes.create_unicode_buffer(32)
|
|
if not user32.EnumDisplayDevicesW(None, idx, None, 0):
|
|
# Try getting display settings by index
|
|
devmode = DEVMODE()
|
|
devmode.dmSize = ctypes.sizeof(DEVMODE)
|
|
|
|
if user32.EnumDisplaySettingsW(None, -1, ctypes.byref(devmode)): # ENUM_CURRENT_SETTINGS = -1
|
|
refresh_rate = devmode.dmDisplayFrequency
|
|
if refresh_rate > 0:
|
|
refresh_rates[idx] = refresh_rate
|
|
logger.debug(f"Display {idx}: {refresh_rate}Hz")
|
|
|
|
idx += 1
|
|
if idx > 10: # Safety limit
|
|
break
|
|
else:
|
|
break
|
|
|
|
# If no refresh rates found, try alternative method
|
|
if not refresh_rates:
|
|
devmode = DEVMODE()
|
|
devmode.dmSize = ctypes.sizeof(DEVMODE)
|
|
|
|
for monitor_idx in range(4): # Check up to 4 monitors
|
|
if user32.EnumDisplaySettingsW(None, -1, ctypes.byref(devmode)):
|
|
refresh_rate = devmode.dmDisplayFrequency
|
|
if refresh_rate > 0:
|
|
refresh_rates[monitor_idx] = refresh_rate
|
|
logger.debug(f"Monitor {monitor_idx}: {refresh_rate}Hz")
|
|
|
|
return refresh_rates
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Failed to retrieve monitor refresh rates: {e}")
|
|
return {}
|