Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to distinguish between different monitors of the same model type? #250

Open
JohnTravolski opened this issue Apr 21, 2023 · 6 comments
Open
Labels
enhancement New feature or request question Further information is requested

Comments

@JohnTravolski
Copy link

JohnTravolski commented Apr 21, 2023

I have several monitors which are the same model; for example, two Dell P2422H monitors. When I print the VCP capabilities, I don't see any information that distinguishes them:

{'prot': 'monitor', 'type': 'lcd', 'model': 'P2422H', 'cmds': {1: [], 2: [], 3: [], 7: [], 12: [], 227: [], 243: []}, 'vcp': {2: [], 4: [], 5: [], 8: [], 16: [], 18: [], 20: [5, 8, 11, 12], 22: [], 24: [], 26: [], 82: [], 96: [1, 15, 17], 170: [1, 2, 4], 172: [], 174: [], 178: [], 182: [], 198: [], 200: [], 201: [], 204: [2, 3, 4, 6, 9, 10, 13, 14], 214: [1, 4, 5], 220: [0, 3, 5], 223: [], 224: [], 225: [], 226: [0, 2, 4, 14, 18, 20], 241: [], 242: [], 253: [], 254: []}, 'mswhql': '1', 'asset_eep': '', 'mccs_ver': '2.1', 'window': '', 'vcpname': '', 'inputs': [<InputSource.ANALOG1: 1>, <InputSource.DP1: 15>, <InputSource.HDMI1: 17>]}

I checked and this output is identical for both monitors. How can I distinguish them other than the index? For example, in another software "Nirsoft ControlMyMonitor" I get a serial number of some kind as well:

image

image

In Nirsoft ControlMyMonitor, I can address the monitor by the number in the red box directly, which makes it possible to distinguish them when controlling them.

It would make more sense to identify them this way since monitor indexes in Windows can change, especially as I add and remove other monitors. The indexes are not consistent in my experience.

I have a setup with 8 total monitors on Windows 10, Python 3.8, monitorcontrol==3.0.2.

@newAM
Copy link
Owner

newAM commented Apr 22, 2023

Do you know what command they are using to extract that serial number? Sounds like a similar issue to #46

@JohnTravolski
Copy link
Author

Do you know what command they are using to extract that serial number? Sounds like a similar issue to #46

Unfortunately I do not. I don't think the program is open source. Here is the project homepage:
https://www.nirsoft.net/utils/control_my_monitor.html

@newAM newAM added enhancement New feature or request question Further information is requested labels Apr 22, 2023
@tateu
Copy link

tateu commented May 7, 2023

You could use GetMonitorInfo and then check the position of the monitor to find the one you want

from monitorcontrol import get_monitors
import ctypes

CCHDEVICENAME = 32
GetMonitorInfo = ctypes.windll.user32.GetMonitorInfoW

class MONITORINFOEX(ctypes.Structure):
    _fields_ = [('cbSize', ctypes.wintypes.DWORD),
                ('rcMonitor', ctypes.wintypes.RECT),
                ('rcWork', ctypes.wintypes.RECT),
                ('dwFlags', ctypes.wintypes.DWORD),
                ('szDevice', ctypes.wintypes.WCHAR * CCHDEVICENAME)]

# two 1920x1080 Monitors are side by side
for monitor in get_monitors():
	with monitor:
		info = MONITORINFOEX()
		info.cbSize = ctypes.sizeof(MONITORINFOEX)
		GetMonitorInfo(monitor.vcp.hmonitor, ctypes.byref(info))

		if info.rcMonitor.left == 0:
			print(f'Found Left Monitor {info.szDevice}')
		elif info.rcMonitor.left == 1920:
			print(f'Found Right Monitor {info.szDevice}')

@JohnTravolski
Copy link
Author

You could use GetMonitorInfo and then check the position of the monitor to find the one you want

from monitorcontrol import get_monitors
import ctypes

CCHDEVICENAME = 32
GetMonitorInfo = ctypes.windll.user32.GetMonitorInfoW

class MONITORINFOEX(ctypes.Structure):
    _fields_ = [('cbSize', ctypes.wintypes.DWORD),
                ('rcMonitor', ctypes.wintypes.RECT),
                ('rcWork', ctypes.wintypes.RECT),
                ('dwFlags', ctypes.wintypes.DWORD),
                ('szDevice', ctypes.wintypes.WCHAR * CCHDEVICENAME)]

# two 1920x1080 Monitors are side by side
for monitor in get_monitors():
	with monitor:
		info = MONITORINFOEX()
		info.cbSize = ctypes.sizeof(MONITORINFOEX)
		GetMonitorInfo(monitor.vcp.hmonitor, ctypes.byref(info))

		if info.rcMonitor.left == 0:
			print(f'Found Left Monitor {info.szDevice}')
		elif info.rcMonitor.left == 1920:
			print(f'Found Right Monitor {info.szDevice}')

This is a decent workaround for now, but it's not ideal, especially if I re-arrange the virtual position of the monitors (I do this semi frequently).

@JohnTravolski
Copy link
Author

JohnTravolski commented May 7, 2023

I put together this script as a workaround for now. It sorts the monitors by the x position so I can access them numerically (1 being leftmost and the highest number being rightmost):

import monitorcontrol
import ctypes
import threading
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-i', '--inputmonitors', default="1", help="Specify the monitors numbers, leftmost to rightmost, starting with index 1", nargs='?')
parser.add_argument('-r', '--reverse_monitor_numbers', default = 'False', nargs='?', const='', help='Use flag to have monitors numbers specified from rightmost to leftmost')
parser.add_argument('-d', '--debugmode', default = 'False', nargs='?', const='', help='Use flag to enable debug mode')
parser.add_argument('-p', '--toggle_powermode', default = 'False', nargs='?', const='', help='Use flag to toggle between off_soft (0x04) and on (0x01) display power modes')
parser.add_argument('-t', '--toggle_inputs', default=None, help="Specify comma-separated inputs you want to toggle between, such as HDMI1,DP1", nargs='?')

args = parser.parse_args()
reverse_monitor_numbers = not str(args.reverse_monitor_numbers).lower() in ['false']
toggle_powermode = not str(args.toggle_powermode).lower() in ['false']
debugmode = not str(args.debugmode).lower() in ['false']

LOOKUP = {
  "HDMI1":monitorcontrol.InputSource.HDMI1,
  "HDMI2":monitorcontrol.InputSource.HDMI2,
  "DP1":monitorcontrol.InputSource.DP1,
  "DP2":monitorcontrol.InputSource.DP2, # this is the mini DP port on the Asus Pro Art PA278QV Monitors
  "ANALOG1":monitorcontrol.InputSource.ANALOG1, # VGA port
  "ANALOG2":monitorcontrol.InputSource.ANALOG2,
  "DVI1":monitorcontrol.InputSource.DVI1,
  "DVI2":monitorcontrol.InputSource.DVI2,
}

CCHDEVICENAME = 32
GetMonitorInfo = ctypes.windll.user32.GetMonitorInfoW
class MONITORINFOEX(ctypes.Structure):
    _fields_ = [('cbSize', ctypes.wintypes.DWORD),
                ('rcMonitor', ctypes.wintypes.RECT),
                ('rcWork', ctypes.wintypes.RECT),
                ('dwFlags', ctypes.wintypes.DWORD),
                ('szDevice', ctypes.wintypes.WCHAR * CCHDEVICENAME)]

def toggle_power(monitor):
  with monitor:
    powmode = monitor.get_power_mode()
    if powmode == monitorcontrol.PowerMode.on:
      monitor.set_power_mode('off_soft') # this is what I have been using with nirsoft controlmymonitor; don't really know the difference between this and standby
    else:
      monitor.set_power_mode('on')

def toggle_inputs(monitor, inpt1, inpt2):
  inp1str = str(inpt1).split('.')[-1]
  inp2str = str(inpt2).split('.')[-1]
  with monitor:
    inputsource = monitor.get_input_source()
    if inputsource == inpt2:
      monitor.set_input_source(inp1str)
    else:
      monitor.set_input_source(inp2str)

if __name__ == '__main__':
  sorted_monitors = []
  monitors = monitorcontrol.get_monitors()
  for monitor in monitors:
    info = MONITORINFOEX()
    info.cbSize = ctypes.sizeof(MONITORINFOEX)
    GetMonitorInfo(monitor.vcp.hmonitor, ctypes.byref(info))
    sorted_monitors.append((monitor, info.rcMonitor.left))
    if debugmode:
      with monitor:
        res = monitor.get_input_source()
        print(f'{str(info.rcMonitor.left):7.7} - {res}')
  sorted_monitors.sort(key=lambda ii : ii[1], reverse=reverse_monitor_numbers)
  
  selected_monitors = []
  for ii, (monitor, xx) in enumerate(sorted_monitors):
    if str(ii + 1) in args.inputmonitors:
      selected_monitors.append(monitor)
  
  threads = []
  if toggle_powermode:
    for monitor in selected_monitors:
      t1 = threading.Thread(target=toggle_power, args=(monitor,))
      t1.start()
      threads.append(t1)
  elif args.toggle_inputs is not None:
    for monitor in selected_monitors:
      inpt1, inpt2 = (LOOKUP[ii] for ii in args.toggle_inputs.split(','))
      t1 = threading.Thread(target=toggle_inputs, args=(monitor,inpt1,inpt2))
      t1.start()
      threads.append(t1)
  for thread in threads:
    thread.join()

This works for now, but I would like to be able to specify some type of unique serial number at some point in the future.

@char101
Copy link

char101 commented Feb 23, 2024

Using DeviceID might be enough to differentiante the monitors

import monitorcontrol
from win32api import EnumDisplayDevices, EnumDisplayMonitors, GetMonitorInfo

hmonitorToDeviceId = {}
for hmon, hdcmon, rect in EnumDisplayMonitors(None, None):
    info = GetMonitorInfo(hmon)
    dev = EnumDisplayDevices(info['Device'], 0, 1)
    hmonitorToDeviceId[int(hmon)] = dev.DeviceID

for mon in monitorcontrol.get_monitors():
    print(mon, hmonitorToDeviceId[mon.vcp.hmonitor.value])
<monitorcontrol.monitorcontrol.Monitor object at 0x00000153D53B81A0> \\?\DISPLAY#LEN61A5#5&186cc93e&0&UID4354#{e6f07b5f-ee97-4a90-
b076-33f57bf4eaa7}
<monitorcontrol.monitorcontrol.Monitor object at 0x00000153D7384530> \\?\DISPLAY#HPN366C#5&186cc93e&0&UID4353#{e6f07b5f-ee97-4a90-
b076-33f57bf4eaa7}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants