How to distinguish between different monitors of the same model type?
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:


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.
Do you know what command they are using to extract that serial number? Sounds like a similar issue to #46
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
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}')
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).
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.
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}