Just the good part: Here's my Python script for doing this, which requires python-xlib be installed.
I have found one source, so far, to get the list of X Server display connections and simultaneously get the monitors connected to each, and that is X itself. There are multiple ways to get this information from X, depending on your tolerance to parsing vs. getting the data directly from X.
Method 1: Parsing xrandr --verbose
The xrandr --verbose command has all the information needed to match display connections with monitors. Its output for a single display connection looks like this:
Screen 0: minimum 320 x 200, current 6400 x 1600, maximum 8192 x 8192
DP-1 disconnected (normal left inverted right x axis y axis)
[extra content removed]
DisplayPort-1-7 connected 1920x1080+0+464 (0x4a) normal (normal left inverted right x axis y axis) 510mm x 287mm
Identifier: 0x224
Timestamp: 6668772
Subpixel: unknown
Gamma: 1.0:1.0:1.0
Brightness: 1.0
Clones:
CRTC: 4
CRTCs: 0 4 5 6 7
Transform: 1.000000 0.000000 0.000000
0.000000 1.000000 0.000000
0.000000 0.000000 1.000000
filter:
EDID:
00ffffffffffff0010ac73404c424241
------[more hex redacted]-------
001155223811000a202020202020007e
GAMMA_LUT_SIZE: 4096
range: (0, -1)
DEGAMMA_LUT_SIZE: 4096
range: (0, -1)
GAMMA_LUT: 0
range: (0, 65535)
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
DEGAMMA_LUT: 0
range: (0, 65535)
TearFree: auto
supported: off, on, auto
vrr_capable: 0
range: (0, 1)
max bpc: 8
range: (8, 16)
underscan vborder: 0
range: (0, 128)
underscan hborder: 0
range: (0, 128)
underscan: off
supported: off, on, auto
scaling mode: None
supported: None, Full, Center, Full aspect
link-status: Good
supported: Good, Bad
CONNECTOR_ID: 76
supported: 76
non-desktop: 0
range: (0, 1)
1920x1080 (0x4a) 148.500MHz +HSync +VSync *current +preferred
h: width 1920 start 2008 end 2052 total 2200 skew 0 clock 67.50KHz
v: height 1080 start 1084 end 1089 total 1125 clock 60.00Hz
1680x1050 (0x9b) 146.250MHz -HSync +VSync
h: width 1680 start 1784 end 1960 total 2240 skew 0 clock 65.29KHz
v: height 1050 start 1053 end 1059 total 1089 clock 59.95Hz
[extra content removed]
Parsing this output isn't great: you'll need to identify the lines that represent a display connection vs. the screen, with variations based on whether it's connected/disconnected, etc. But if you do, you can extract the EDID, convert it to raw bytes, and pass it to a tool like parse-edid.
Method 2: Xlib client and EDID parser
There is a Python client for Xlib that has the tools needed to fetch the display connections and the relevant EDID bytes from each connected monitor. Similarly, there is a Python library for parsing EDID data, though it is not quite ideal (explained below). However, I've made an example script that puts the two of these items together to yield the answer I want:
#!/usr/bin/env python3
import pyedid.edid, pyedid.helpers.registry
import Xlib.display
def get_x_displays():
# Used by PyEDID to fetch manufacturers given their EDID code
registry = pyedid.helpers.registry.Registry.from_web()
# Xlib resources
d = Xlib.display.Display()
root = d.screen().root
resources = root.xrandr_get_screen_resources()._data
outputs = {}
for output in resources['outputs']:
output_info = d.xrandr_get_output_info(output, resources['config_timestamp'])._data
output_name = output_info['name']
props = d.xrandr_list_output_properties(output)
edid_data = None
# Look through the atoms (properties) of each output to see if there's one named 'EDID'
for atom in props._data['atoms']:
atom_name = d.get_atom_name(atom)
if atom_name == 'EDID':
edid_raw = d.xrandr_get_output_property(output, atom, 0, 0, 1000)._data['value']
edid_data = pyedid.edid.Edid(bytes(edid_raw)[:128], registry)
break
outputs[output_name] = edid_data
return outputs
if __name__ == '__main__':
displays = get_x_displays()
for connection, monitor in sorted(displays.items(), key=lambda kv: kv[0]):
print(connection)
print(' ' + ("No connection or empty EDID" if monitor is None else
"{} ({})".format(monitor.name, monitor.serial)))
On my machine, this produces the following output (serial numbers mangled):
DP-1
No connection or empty EDID
DP-2
No connection or empty EDID
DP-3
No connection or empty EDID
DisplayPort-1-3
No connection or empty EDID
DisplayPort-1-4
No connection or empty EDID
DisplayPort-1-5
No connection or empty EDID
DisplayPort-1-6
DELL UP3017 (DDSB553SBDFL)
DisplayPort-1-7
DELL U2312HM (V092DMLS657D)
DisplayPort-1-8
No connection or empty EDID
HDMI-1
No connection or empty EDID
HDMI-A-1-1
No connection or empty EDID
eDP-1
None (0)
eDP-1-1
No connection or empty EDID
The PyEDID library requires a "registry" of manufacturers so that it can populate the manufacturer field of the EDID. The from_web() call to create the registry from an online source is the slowest part of this script. I have created an alternative version of the script that eliminates that requirement by skipping the manufacturer lookup altogether, instead passing out the raw value.