Add display refresh rate detection and display
Some checks failed
Validate / validate (push) Failing after 8s
Some checks failed
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>
This commit is contained in:
@@ -36,6 +36,7 @@ class DisplayInfo(BaseModel):
|
|||||||
x: int = Field(description="Display X position")
|
x: int = Field(description="Display X position")
|
||||||
y: int = Field(description="Display Y position")
|
y: int = Field(description="Display Y position")
|
||||||
is_primary: bool = Field(default=False, description="Whether this is the primary display")
|
is_primary: bool = Field(default=False, description="Whether this is the primary display")
|
||||||
|
refresh_rate: int = Field(description="Display refresh rate in Hz")
|
||||||
|
|
||||||
|
|
||||||
class DisplayListResponse(BaseModel):
|
class DisplayListResponse(BaseModel):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import mss
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from wled_controller.utils import get_logger, get_monitor_names
|
from wled_controller.utils import get_logger, get_monitor_names, get_monitor_refresh_rates
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ class DisplayInfo:
|
|||||||
x: int
|
x: int
|
||||||
y: int
|
y: int
|
||||||
is_primary: bool
|
is_primary: bool
|
||||||
|
refresh_rate: int # in Hz
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -58,6 +59,9 @@ def get_available_displays() -> List[DisplayInfo]:
|
|||||||
# Get friendly monitor names (Windows only, falls back to generic names)
|
# Get friendly monitor names (Windows only, falls back to generic names)
|
||||||
monitor_names = get_monitor_names()
|
monitor_names = get_monitor_names()
|
||||||
|
|
||||||
|
# Get monitor refresh rates (Windows only, falls back to 60Hz)
|
||||||
|
refresh_rates = get_monitor_refresh_rates()
|
||||||
|
|
||||||
with mss.mss() as sct:
|
with mss.mss() as sct:
|
||||||
displays = []
|
displays = []
|
||||||
|
|
||||||
@@ -66,6 +70,9 @@ def get_available_displays() -> List[DisplayInfo]:
|
|||||||
# Use friendly name from WMI if available, otherwise generic name
|
# Use friendly name from WMI if available, otherwise generic name
|
||||||
friendly_name = monitor_names.get(idx, f"Display {idx}")
|
friendly_name = monitor_names.get(idx, f"Display {idx}")
|
||||||
|
|
||||||
|
# Use detected refresh rate or default to 60Hz
|
||||||
|
refresh_rate = refresh_rates.get(idx, 60)
|
||||||
|
|
||||||
display_info = DisplayInfo(
|
display_info = DisplayInfo(
|
||||||
index=idx,
|
index=idx,
|
||||||
name=friendly_name,
|
name=friendly_name,
|
||||||
@@ -74,6 +81,7 @@ def get_available_displays() -> List[DisplayInfo]:
|
|||||||
x=monitor["left"],
|
x=monitor["left"],
|
||||||
y=monitor["top"],
|
y=monitor["top"],
|
||||||
is_primary=(idx == 0),
|
is_primary=(idx == 0),
|
||||||
|
refresh_rate=refresh_rate,
|
||||||
)
|
)
|
||||||
displays.append(display_info)
|
displays.append(display_info)
|
||||||
|
|
||||||
|
|||||||
@@ -268,6 +268,10 @@ async function loadDisplays() {
|
|||||||
<span class="info-label">${t('displays.resolution')}</span>
|
<span class="info-label">${t('displays.resolution')}</span>
|
||||||
<span class="info-value">${display.width} × ${display.height}</span>
|
<span class="info-value">${display.width} × ${display.height}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">${t('displays.refresh_rate')}</span>
|
||||||
|
<span class="info-value">${display.refresh_rate}Hz</span>
|
||||||
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">${t('displays.position')}</span>
|
<span class="info-label">${t('displays.position')}</span>
|
||||||
<span class="info-value">(${display.x}, ${display.y})</span>
|
<span class="info-value">(${display.x}, ${display.y})</span>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"displays.badge.primary": "Primary",
|
"displays.badge.primary": "Primary",
|
||||||
"displays.badge.secondary": "Secondary",
|
"displays.badge.secondary": "Secondary",
|
||||||
"displays.resolution": "Resolution:",
|
"displays.resolution": "Resolution:",
|
||||||
|
"displays.refresh_rate": "Refresh Rate:",
|
||||||
"displays.position": "Position:",
|
"displays.position": "Position:",
|
||||||
"displays.index": "Display Index:",
|
"displays.index": "Display Index:",
|
||||||
"displays.loading": "Loading displays...",
|
"displays.loading": "Loading displays...",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"displays.badge.primary": "Основной",
|
"displays.badge.primary": "Основной",
|
||||||
"displays.badge.secondary": "Вторичный",
|
"displays.badge.secondary": "Вторичный",
|
||||||
"displays.resolution": "Разрешение:",
|
"displays.resolution": "Разрешение:",
|
||||||
|
"displays.refresh_rate": "Частота Обновления:",
|
||||||
"displays.position": "Позиция:",
|
"displays.position": "Позиция:",
|
||||||
"displays.index": "Индекс Дисплея:",
|
"displays.index": "Индекс Дисплея:",
|
||||||
"displays.loading": "Загрузка дисплеев...",
|
"displays.loading": "Загрузка дисплеев...",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Utility functions and helpers."""
|
"""Utility functions and helpers."""
|
||||||
|
|
||||||
from .logger import setup_logging, get_logger
|
from .logger import setup_logging, get_logger
|
||||||
from .monitor_names import get_monitor_names, get_monitor_name
|
from .monitor_names import get_monitor_names, get_monitor_name, get_monitor_refresh_rates
|
||||||
|
|
||||||
__all__ = ["setup_logging", "get_logger", "get_monitor_names", "get_monitor_name"]
|
__all__ = ["setup_logging", "get_logger", "get_monitor_names", "get_monitor_name", "get_monitor_refresh_rates"]
|
||||||
|
|||||||
@@ -77,3 +77,90 @@ def get_monitor_name(index: int) -> str:
|
|||||||
"""
|
"""
|
||||||
monitor_names = get_monitor_names()
|
monitor_names = get_monitor_names()
|
||||||
return monitor_names.get(index, f"Display {index}")
|
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 {}
|
||||||
|
|||||||
Reference in New Issue
Block a user