|
1 | 1 | import logging |
| 2 | +import re |
2 | 3 | import threading |
3 | 4 | import time |
| 5 | +from contextlib import contextmanager |
4 | 6 | from ctypes import Structure, WinError, byref, windll |
5 | 7 | from ctypes.wintypes import BYTE, DWORD, HANDLE, WCHAR |
6 | 8 | from typing import List, Optional |
|
10 | 12 | import win32api |
11 | 13 | import win32con |
12 | 14 | import wmi |
13 | | -import re |
| 15 | + |
14 | 16 | from . import filter_monitors, get_methods |
15 | 17 | from .exceptions import EDIDParseError, NoValidDisplayError, format_exc |
16 | 18 | from .helpers import EDID, BrightnessMethod, __Cache, _monitor_brand_lookup |
|
34 | 36 | ''' |
35 | 37 |
|
36 | 38 |
|
| 39 | +@contextmanager |
37 | 40 | def _wmi_init(): |
38 | 41 | '''internal function to create and return a wmi instance''' |
39 | 42 | # WMI calls don't work in new threads so we have to run this check |
40 | | - if threading.current_thread() != threading.main_thread(): |
| 43 | + com_init = threading.current_thread() != threading.main_thread() |
| 44 | + if com_init: |
41 | 45 | if COM_MODEL is None: |
42 | 46 | pythoncom.CoInitialize() |
43 | 47 | else: |
44 | 48 | pythoncom.CoInitializeEx(COM_MODEL) |
45 | | - return wmi.WMI(namespace='wmi') |
| 49 | + |
| 50 | + yield wmi.WMI(namespace='wmi') |
| 51 | + |
| 52 | + if com_init: |
| 53 | + pythoncom.CoUninitialize() |
46 | 54 |
|
47 | 55 |
|
48 | 56 | def enum_display_devices() -> Generator[win32api.PyDISPLAY_DEVICEType, None, None]: |
@@ -87,92 +95,92 @@ def get_display_info() -> List[dict]: |
87 | 95 | monitor_uids[device.DeviceID.split('#')[2]] = device |
88 | 96 |
|
89 | 97 | # gather list of laptop displays to check against later |
90 | | - wmi = _wmi_init() |
91 | | - try: |
92 | | - laptop_displays = [ |
93 | | - i.InstanceName |
94 | | - for i in wmi.WmiMonitorBrightness() |
95 | | - ] |
96 | | - except Exception as e: |
97 | | - # don't do specific exception classes here because WMI does not play ball with it |
98 | | - _logger.warning( |
99 | | - f'get_display_info: failed to gather list of laptop displays - {format_exc(e)}') |
100 | | - laptop_displays = [] |
101 | | - |
102 | | - extras, desktop, laptop = [], 0, 0 |
103 | | - uid_keys = list(monitor_uids.keys()) |
104 | | - for monitor in wmi.WmiMonitorDescriptorMethods(): |
105 | | - model, serial, manufacturer, man_id, edid = None, None, None, None, None |
106 | | - instance_name = monitor.InstanceName.replace( |
107 | | - '_0', '', 1).split('\\')[2] |
108 | | - try: |
109 | | - pydevice = monitor_uids[instance_name] |
110 | | - except KeyError: |
111 | | - # if laptop display WAS connected but was later put to sleep (#33) |
112 | | - if instance_name in laptop_displays: |
113 | | - laptop += 1 |
114 | | - else: |
115 | | - desktop += 1 |
116 | | - _logger.warning( |
117 | | - f'display {instance_name!r} is detected but not present in monitor_uids.' |
118 | | - ' Maybe it is asleep?' |
119 | | - ) |
120 | | - continue |
121 | | - |
122 | | - # get the EDID |
| 98 | + with _wmi_init() as wmi: |
123 | 99 | try: |
124 | | - edid = ''.join( |
125 | | - f'{char:02x}' for char in monitor.WmiGetMonitorRawEEdidV1Block(0)[0]) |
126 | | - # we do the EDID parsing ourselves because calling wmi.WmiMonitorID |
127 | | - # takes too long |
128 | | - parsed = EDID.parse(edid) |
129 | | - man_id, manufacturer, model, name, serial = parsed |
130 | | - if name is None: |
131 | | - raise EDIDParseError( |
132 | | - 'parsed EDID returned invalid display name') |
133 | | - except EDIDParseError as e: |
134 | | - edid = None |
135 | | - _logger.warning( |
136 | | - f'exception parsing edid str for {monitor.InstanceName} - {format_exc(e)}') |
| 100 | + laptop_displays = [ |
| 101 | + i.InstanceName |
| 102 | + for i in wmi.WmiMonitorBrightness() |
| 103 | + ] |
137 | 104 | except Exception as e: |
138 | | - edid = None |
139 | | - _logger.error( |
140 | | - f'failed to get EDID string for {monitor.InstanceName} - {format_exc(e)}') |
141 | | - finally: |
142 | | - if edid is None: |
143 | | - devid = pydevice.DeviceID.split('#') |
144 | | - serial = devid[2] |
145 | | - man_id = devid[1][:3] |
146 | | - model = devid[1][3:] or 'Generic Monitor' |
147 | | - del devid |
148 | | - if (brand := _monitor_brand_lookup(man_id)): |
149 | | - man_id, manufacturer = brand |
150 | | - |
151 | | - if (serial, model) != (None, None): |
152 | | - data: dict = { |
153 | | - 'name': f'{manufacturer} {model}', |
154 | | - 'model': model, |
155 | | - 'serial': serial, |
156 | | - 'manufacturer': manufacturer, |
157 | | - 'manufacturer_id': man_id, |
158 | | - 'edid': edid, |
159 | | - 'uid': uid_match.group(1) if (uid_match := re.search(r"UID(\d+)", instance_name)) else None, |
160 | | - } |
161 | | - if monitor.InstanceName in laptop_displays: |
162 | | - data['index'] = laptop |
163 | | - data['method'] = WMI |
164 | | - laptop += 1 |
165 | | - else: |
166 | | - data['method'] = VCP |
167 | | - desktop += 1 |
168 | | - |
169 | | - if instance_name in uid_keys: |
170 | | - # insert the data into the uid_keys list because |
171 | | - # uid_keys has the monitors sorted correctly. This |
172 | | - # means we don't have to re-sort the list later |
173 | | - uid_keys[uid_keys.index(instance_name)] = data |
174 | | - else: |
175 | | - extras.append(data) |
| 105 | + # don't do specific exception classes here because WMI does not play ball with it |
| 106 | + _logger.warning( |
| 107 | + f'get_display_info: failed to gather list of laptop displays - {format_exc(e)}') |
| 108 | + laptop_displays = [] |
| 109 | + |
| 110 | + extras, desktop, laptop = [], 0, 0 |
| 111 | + uid_keys = list(monitor_uids.keys()) |
| 112 | + for monitor in wmi.WmiMonitorDescriptorMethods(): |
| 113 | + model, serial, manufacturer, man_id, edid = None, None, None, None, None |
| 114 | + instance_name = monitor.InstanceName.replace( |
| 115 | + '_0', '', 1).split('\\')[2] |
| 116 | + try: |
| 117 | + pydevice = monitor_uids[instance_name] |
| 118 | + except KeyError: |
| 119 | + # if laptop display WAS connected but was later put to sleep (#33) |
| 120 | + if instance_name in laptop_displays: |
| 121 | + laptop += 1 |
| 122 | + else: |
| 123 | + desktop += 1 |
| 124 | + _logger.warning( |
| 125 | + f'display {instance_name!r} is detected but not present in monitor_uids.' |
| 126 | + ' Maybe it is asleep?' |
| 127 | + ) |
| 128 | + continue |
| 129 | + |
| 130 | + # get the EDID |
| 131 | + try: |
| 132 | + edid = ''.join( |
| 133 | + f'{char:02x}' for char in monitor.WmiGetMonitorRawEEdidV1Block(0)[0]) |
| 134 | + # we do the EDID parsing ourselves because calling wmi.WmiMonitorID |
| 135 | + # takes too long |
| 136 | + parsed = EDID.parse(edid) |
| 137 | + man_id, manufacturer, model, name, serial = parsed |
| 138 | + if name is None: |
| 139 | + raise EDIDParseError( |
| 140 | + 'parsed EDID returned invalid display name') |
| 141 | + except EDIDParseError as e: |
| 142 | + edid = None |
| 143 | + _logger.warning( |
| 144 | + f'exception parsing edid str for {monitor.InstanceName} - {format_exc(e)}') |
| 145 | + except Exception as e: |
| 146 | + edid = None |
| 147 | + _logger.error( |
| 148 | + f'failed to get EDID string for {monitor.InstanceName} - {format_exc(e)}') |
| 149 | + finally: |
| 150 | + if edid is None: |
| 151 | + devid = pydevice.DeviceID.split('#') |
| 152 | + serial = devid[2] |
| 153 | + man_id = devid[1][:3] |
| 154 | + model = devid[1][3:] or 'Generic Monitor' |
| 155 | + del devid |
| 156 | + if (brand := _monitor_brand_lookup(man_id)): |
| 157 | + man_id, manufacturer = brand |
| 158 | + |
| 159 | + if (serial, model) != (None, None): |
| 160 | + data: dict = { |
| 161 | + 'name': f'{manufacturer} {model}', |
| 162 | + 'model': model, |
| 163 | + 'serial': serial, |
| 164 | + 'manufacturer': manufacturer, |
| 165 | + 'manufacturer_id': man_id, |
| 166 | + 'edid': edid, |
| 167 | + 'uid': uid_match.group(1) if (uid_match := re.search(r"UID(\d+)", instance_name)) else None, |
| 168 | + } |
| 169 | + if monitor.InstanceName in laptop_displays: |
| 170 | + data['index'] = laptop |
| 171 | + data['method'] = WMI |
| 172 | + laptop += 1 |
| 173 | + else: |
| 174 | + data['method'] = VCP |
| 175 | + desktop += 1 |
| 176 | + |
| 177 | + if instance_name in uid_keys: |
| 178 | + # insert the data into the uid_keys list because |
| 179 | + # uid_keys has the monitors sorted correctly. This |
| 180 | + # means we don't have to re-sort the list later |
| 181 | + uid_keys[uid_keys.index(instance_name)] = data |
| 182 | + else: |
| 183 | + extras.append(data) |
176 | 184 |
|
177 | 185 | info = uid_keys + extras |
178 | 186 | if desktop: |
@@ -205,21 +213,23 @@ def get_display_info(cls, display: Optional[DisplayIdentifier] = None) -> List[d |
205 | 213 |
|
206 | 214 | @classmethod |
207 | 215 | def set_brightness(cls, value: IntPercentage, display: Optional[int] = None): |
208 | | - brightness_method = _wmi_init().WmiMonitorBrightnessMethods() |
209 | | - if display is not None: |
210 | | - brightness_method = [brightness_method[display]] |
| 216 | + with _wmi_init() as wmi: |
| 217 | + brightness_method = wmi.WmiMonitorBrightnessMethods() |
| 218 | + if display is not None: |
| 219 | + brightness_method = [brightness_method[display]] |
211 | 220 |
|
212 | | - for method in brightness_method: |
213 | | - method.WmiSetBrightness(value, 0) |
| 221 | + for method in brightness_method: |
| 222 | + method.WmiSetBrightness(value, 0) |
214 | 223 |
|
215 | 224 | @classmethod |
216 | 225 | def get_brightness(cls, display: Optional[int] = None) -> List[IntPercentage]: |
217 | | - brightness_method = _wmi_init().WmiMonitorBrightness() |
218 | | - if display is not None: |
219 | | - brightness_method = [brightness_method[display]] |
| 226 | + with _wmi_init() as wmi: |
| 227 | + brightness_method = wmi.WmiMonitorBrightness() |
| 228 | + if display is not None: |
| 229 | + brightness_method = [brightness_method[display]] |
220 | 230 |
|
221 | | - values = [i.CurrentBrightness for i in brightness_method] |
222 | | - return values |
| 231 | + values = [i.CurrentBrightness for i in brightness_method] |
| 232 | + return values |
223 | 233 |
|
224 | 234 |
|
225 | 235 | class VCP(BrightnessMethod): |
@@ -250,16 +260,16 @@ def iter_physical_monitors(cls, start: int = 0) -> Generator[HANDLE, None, None] |
250 | 260 | monitor_index = 0 |
251 | 261 | display_devices = list(enum_display_devices()) |
252 | 262 |
|
253 | | - wmi = _wmi_init() |
254 | | - try: |
255 | | - laptop_displays = [ |
256 | | - i.InstanceName.replace('_0', '').split('\\')[2] |
257 | | - for i in wmi.WmiMonitorBrightness() |
258 | | - ] |
259 | | - except Exception as e: |
260 | | - cls._logger.warning( |
261 | | - f'failed to gather list of laptop displays - {format_exc(e)}') |
262 | | - laptop_displays = [] |
| 263 | + with _wmi_init() as wmi: |
| 264 | + try: |
| 265 | + laptop_displays = [ |
| 266 | + i.InstanceName.replace('_0', '').split('\\')[2] |
| 267 | + for i in wmi.WmiMonitorBrightness() |
| 268 | + ] |
| 269 | + except Exception as e: |
| 270 | + cls._logger.warning( |
| 271 | + f'failed to gather list of laptop displays - {format_exc(e)}') |
| 272 | + laptop_displays = [] |
263 | 273 |
|
264 | 274 | for monitor in map(lambda m: m[0].handle, win32api.EnumDisplayMonitors()): |
265 | 275 | # Get physical monitor count |
|
0 commit comments