• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

newAM / monitorcontrol / 18576588332

16 Oct 2025 10:31PM UTC coverage: 61.654% (-0.4%) from 62.027%
18576588332

Pull #408

github

web-flow
Merge f6397cbf7 into 6f62463bc
Pull Request #408: fix: output monitor input names instead of codes if we have them

7 of 14 new or added lines in 1 file covered. (50.0%)

7 existing lines in 2 files now uncovered.

410 of 665 relevant lines covered (61.65%)

3.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.14
/monitorcontrol/monitorcontrol.py
1
from . import vcp, vcp_codes
5✔
2
from types import TracebackType
5✔
3
from typing import List, Optional, Type, Union
5✔
4
import enum
5✔
5
import sys
5✔
6

7

8
@enum.unique
5✔
9
class ColorPreset(enum.IntEnum):
5✔
10
    """Monitor color presets."""
11

12
    COLOR_TEMP_4000K = 0x03
5✔
13
    COLOR_TEMP_5000K = 0x04
5✔
14
    COLOR_TEMP_6500K = 0x05
5✔
15
    COLOR_TEMP_7500K = 0x06
5✔
16
    COLOR_TEMP_8200K = 0x07
5✔
17
    COLOR_TEMP_9300K = 0x08
5✔
18
    COLOR_TEMP_10000K = 0x09
5✔
19
    COLOR_TEMP_11500K = 0x0A
5✔
20
    COLOR_TEMP_USER1 = 0x0B
5✔
21
    COLOR_TEMP_USER2 = 0x0C
5✔
22
    COLOR_TEMP_USER3 = 0x0D
5✔
23

24

25
@enum.unique
5✔
26
class PowerMode(enum.IntEnum):
5✔
27
    """Monitor power modes."""
28

29
    #: On.
30
    on = 0x01
5✔
31
    #: Standby.
32
    standby = 0x02
5✔
33
    #: Suspend.
34
    suspend = 0x03
5✔
35
    #: Software power off.
36
    off_soft = 0x04
5✔
37
    #: Hardware power off.
38
    off_hard = 0x05
5✔
39

40

41
@enum.unique
5✔
42
class InputSource(enum.IntEnum):
5✔
43
    """Monitor input sources."""
44

45
    OFF = 0x00
5✔
46
    ANALOG1 = 0x01
5✔
47
    ANALOG2 = 0x02
5✔
48
    DVI1 = 0x03
5✔
49
    DVI2 = 0x04
5✔
50
    COMPOSITE1 = 0x05
5✔
51
    COMPOSITE2 = 0x06
5✔
52
    SVIDEO1 = 0x07
5✔
53
    SVIDEO2 = 0x08
5✔
54
    TUNER1 = 0x09
5✔
55
    TUNER2 = 0x0A
5✔
56
    TUNER3 = 0x0B
5✔
57
    CMPONENT1 = 0x0C
5✔
58
    CMPONENT2 = 0x0D
5✔
59
    CMPONENT3 = 0x0E
5✔
60
    DP1 = 0x0F
5✔
61
    DP2 = 0x10
5✔
62
    HDMI1 = 0x11
5✔
63
    HDMI2 = 0x12
5✔
64

65

66
class Monitor:
5✔
67
    """
68
    A physical monitor attached to a Virtual Control Panel (VCP).
69

70
    Typically, you do not use this class directly and instead use
71
    :py:meth:`get_monitors` to get a list of initialized monitors.
72

73
    All class methods must be called from within a context manager unless
74
    otherwise stated.
75

76
    Args:
77
        vcp: Virtual control panel for the monitor.
78
    """
79

80
    def __init__(self, vcp: vcp.VCP):
5✔
81
        self.vcp = vcp
5✔
82
        self.code_maximum = {}
5✔
83
        self._in_ctx = False
5✔
84

85
    def __enter__(self):
5✔
86
        self.vcp.__enter__()
5✔
87
        self._in_ctx = True
5✔
88
        return self
5✔
89

90
    def __exit__(
5✔
91
        self,
92
        exception_type: Optional[Type[BaseException]],
93
        exception_value: Optional[BaseException],
94
        exception_traceback: Optional[TracebackType],
95
    ) -> Optional[bool]:
96
        try:
5✔
97
            return self.vcp.__exit__(
5✔
98
                exception_type, exception_value, exception_traceback
99
            )
100
        finally:
101
            self._in_ctx = False
5✔
102

103
    def _get_code_maximum(self, code: vcp.VCPCode) -> int:
5✔
104
        """
105
        Gets the maximum values for a given code, and caches in the
106
        class dictionary if not already found.
107

108
        Args:
109
            code: Feature code definition class.
110

111
        Returns:
112
            Maximum value for the given code.
113

114
        Raises:
115
            TypeError: Code is write only.
116
        """
117
        assert self._in_ctx, "This function must be run within the context manager"
5✔
118
        if not code.readable:
5✔
119
            raise TypeError(f"code is not readable: {code.name}")
5✔
120

121
        if code.value in self.code_maximum:
5✔
122
            return self.code_maximum[code.value]
5✔
123
        else:
124
            _, maximum = self.vcp.get_vcp_feature(code.value)
5✔
125
            self.code_maximum[code.value] = maximum
5✔
126
            return maximum
5✔
127

128
    def _set_vcp_feature(self, code: vcp.VCPCode, value: int):
5✔
129
        """
130
        Sets the value of a feature on the virtual control panel.
131

132
        Args:
133
            code: Feature code.
134
            value: Feature value.
135

136
        Raises:
137
            TypeError: Code is ready only.
138
            ValueError: Value is greater than the maximum allowable.
139
            VCPError: Failed to get VCP feature.
140
        """
141
        assert self._in_ctx, "This function must be run within the context manager"
5✔
142
        if code.type == "ro":
5✔
143
            raise TypeError(f"cannot write read-only code: {code.name}")
5✔
144
        elif code.type == "rw" and code.function == "c":
5✔
145
            maximum = self._get_code_maximum(code)
5✔
146
            if value > maximum:
5✔
147
                raise ValueError(f"value of {value} exceeds code maximum of {maximum}")
5✔
148

149
        self.vcp.set_vcp_feature(code.value, value)
5✔
150

151
    def _get_vcp_feature(self, code: vcp.VCPCode) -> int:
5✔
152
        """
153
        Gets the value of a feature from the virtual control panel.
154

155
        Args:
156
            code: Feature code.
157

158
        Returns:
159
            Current feature value.
160

161
        Raises:
162
            TypeError: Code is write only.
163
            VCPError: Failed to get VCP feature.
164
        """
165
        assert self._in_ctx, "This function must be run within the context manager"
5✔
166
        if code.type == "wo":
5✔
167
            raise TypeError(f"cannot read write-only code: {code.name}")
5✔
168

169
        current, maximum = self.vcp.get_vcp_feature(code.value)
5✔
170
        return current
5✔
171

172
    def get_vcp_capabilities(self) -> dict:
5✔
173
        """
174
        Gets the capabilities of the monitor
175

176
        Returns:
177
            Dictionary of capabilities in the following example format::
178

179
                {
180
                    "prot": "monitor",
181
                    "type": "LCD",
182
                    "cmds": {
183
                            1: [],
184
                            2: [],
185
                            96: [15, 17, 18],
186
                    },
187
                    "inputs": [
188
                        InputSource.DP1,
189
                        InputSource.HDMI1,
190
                        InputSource.HDMI2
191
                        # this may return integers for out-of-spec values,
192
                        # such as USB Type-C monitors
193
                    ],
194
                }
195
        """
196
        assert self._in_ctx, "This function must be run within the context manager"
5✔
197

198
        cap_str = self.vcp.get_vcp_capabilities()
5✔
199

200
        res = _parse_capabilities(cap_str)
5✔
201
        return res
5✔
202

203
    def get_luminance(self) -> int:
5✔
204
        """
205
        Gets the monitors back-light luminance.
206

207
        Returns:
208
            Current luminance value.
209

210
        Example:
211
            Basic Usage::
212

213
                from monitorcontrol import get_monitors
214

215
                for monitor in get_monitors():
216
                    with monitor:
217
                        print(monitor.get_luminance())
218

219
        Raises:
220
            VCPError: Failed to get luminance from the VCP.
221
        """
222
        return self._get_vcp_feature(vcp_codes.image_luminance)
5✔
223

224
    def set_luminance(self, value: int):
5✔
225
        """
226
        Sets the monitors back-light luminance.
227

228
        Args:
229
            value: New luminance value (typically 0-100).
230

231
        Example:
232
            Basic Usage::
233

234
                from monitorcontrol import get_monitors
235

236
                for monitor in get_monitors():
237
                    with monitor:
238
                        monitor.set_luminance(50)
239

240
        Raises:
241
            ValueError: Luminance outside of valid range.
242
            VCPError: Failed to set luminance in the VCP.
243
        """
244
        self._set_vcp_feature(vcp_codes.image_luminance, value)
5✔
245

246
    def get_color_preset(self) -> int:
5✔
247
        """
248
        Gets the monitors color preset.
249

250
        Returns:
251
            Current color preset.
252
            Valid values are enumerated in :py:class:`ColorPreset`.
253

254
        Example:
255
            Basic Usage::
256

257
                from monitorcontrol import get_monitors
258

259
                for monitor in get_monitors():
260
                    with monitor:
261
                        print(monitor.get_color_preset())
262

263
        Raises:
264
            VCPError: Failed to get color preset from the VCP.
265
        """
266
        return self._get_vcp_feature(vcp_codes.image_color_preset)
×
267

268
    def set_color_preset(self, value: Union[int, str, ColorPreset]):
5✔
269
        """
270
        Sets the monitors color preset.
271

272
        Args:
273
            value:
274
                An integer color preset,
275
                or a string representing the color preset,
276
                or a value from :py:class:`ColorPreset`.
277

278
        Example:
279
            Basic Usage::
280

281
                from monitorcontrol import get_monitors, ColorPreset
282

283
                for monitor in get_monitors():
284
                    with monitor:
285
                        monitor.set_color_preset(ColorPreset.COLOR_TEMP_5000K)
286

287
        Raises:
288
            VCPError: Failed to set color preset in the VCP.
289
            ValueError: Color preset outside valid range.
290
            AttributeError: Color preset string is invalid.
291
            TypeError: Unsupported value
292
        """
293
        if isinstance(value, str):
×
294
            mode_value = getattr(ColorPreset, value).value
×
295
        elif isinstance(value, int):
×
296
            mode_value = ColorPreset(value).value
×
297
        elif isinstance(value, ColorPreset):
×
298
            mode_value = value.value
×
299
        else:
300
            raise TypeError("unsupported color preset: " + repr(type(value)))
×
301

302
        self._set_vcp_feature(vcp_codes.image_color_preset, mode_value)
×
303

304
    def get_contrast(self) -> int:
5✔
305
        """
306
        Gets the monitors contrast.
307

308
        Returns:
309
            Current contrast value.
310

311
        Example:
312
            Basic Usage::
313

314
                from monitorcontrol import get_monitors
315

316
                for monitor in get_monitors():
317
                    with monitor:
318
                        print(monitor.get_contrast())
319

320
        Raises:
321
            VCPError: Failed to get contrast from the VCP.
322
        """
323
        return self._get_vcp_feature(vcp_codes.image_contrast)
5✔
324

325
    def set_contrast(self, value: int):
5✔
326
        """
327
        Sets the monitors back-light contrast.
328

329
        Args:
330
            value: New contrast value (typically 0-100).
331

332
        Example:
333
            Basic Usage::
334

335
                from monitorcontrol import get_monitors
336

337
                for monitor in get_monitors():
338
                    with monitor:
339
                        print(monitor.set_contrast(50))
340

341
        Raises:
342
            ValueError: Contrast outside of valid range.
343
            VCPError: Failed to set contrast in the VCP.
344
        """
345
        self._set_vcp_feature(vcp_codes.image_contrast, value)
5✔
346

347
    def get_power_mode(self) -> PowerMode:
5✔
348
        """
349
        Get the monitor power mode.
350

351
        Returns:
352
            Value from the :py:class:`PowerMode` enumeration.
353

354
        Example:
355
            Basic Usage::
356

357
                from monitorcontrol import get_monitors
358

359
                for monitor in get_monitors():
360
                    with monitor:
361
                        print(monitor.get_power_mode())
362

363
        Raises:
364
            VCPError: Failed to get the power mode.
365
        """
366
        value = self._get_vcp_feature(vcp_codes.display_power_mode)
5✔
367
        return PowerMode(value)
5✔
368

369
    def set_power_mode(self, value: Union[int, str, PowerMode]):
5✔
370
        """
371
        Set the monitor power mode.
372

373
        Args:
374
            value:
375
                An integer power mode,
376
                or a string representing the power mode,
377
                or a value from :py:class:`PowerMode`.
378

379
        Example:
380
            Basic Usage::
381

382
                from monitorcontrol import get_monitors
383

384
                for monitor in get_monitors():
385
                    with monitor:
386
                        monitor.set_power_mode("standby")
387

388
        Raises:
389
            VCPError: Failed to get or set the power mode
390
            ValueError: Power state outside of valid range.
391
            AttributeError: Power mode string is invalid.
392
        """
393
        if isinstance(value, str):
5✔
394
            mode_value = getattr(PowerMode, value).value
5✔
395
        elif isinstance(value, int):
5✔
396
            mode_value = PowerMode(value).value
5✔
397
        elif isinstance(value, PowerMode):
5✔
398
            mode_value = value.value
×
399
        else:
400
            raise TypeError("unsupported mode type: " + repr(type(value)))
5✔
401

402
        self._set_vcp_feature(vcp_codes.display_power_mode, mode_value)
5✔
403

404
    def get_input_source(self) -> int:
5✔
405
        """
406
        Gets the monitors input source
407

408
        Returns:
409
            Current input source.
410

411
        Example:
412
            Basic Usage::
413

414
                from monitorcontrol import get_monitors, InputSource
415

416
                for monitor in get_monitors():
417
                    with monitor:
418
                        input_source_raw: int = monitor.get_input_source()
419
                        print(InputSource(input_source_raw).name)
420

421
        Raises:
422
            VCPError: Failed to get input source from the VCP.
423
        """
424
        return self._get_vcp_feature(vcp_codes.input_select) & 0xFF
5✔
425

426
    def set_input_source(self, value: Union[int, str, InputSource]):
5✔
427
        """
428
        Sets the monitors input source.
429

430
        Args:
431
            value: New input source
432

433
        Example:
434
            Basic Usage::
435

436
                from monitorcontrol import get_monitors
437

438
                for monitor in get_monitors():
439
                    with monitor:
440
                        print(monitor.set_input_source("DP1"))
441

442
        Raises:
443
            VCPError: Failed to get the input source.
444
            KeyError: Set input source string is invalid.
445
        """
446
        if isinstance(value, str):
5✔
447
            if value.isdigit():
5✔
448
                mode_value = int(value)
5✔
449
            else:
450
                mode_value = getattr(InputSource, value.upper()).value
5✔
451
        elif isinstance(value, InputSource):
5✔
452
            mode_value = value.value
5✔
453
        elif isinstance(value, int):
5✔
454
            mode_value = value
5✔
455
        else:
456
            raise TypeError("unsupported input type: " + repr(type(value)))
5✔
457

458
        self._set_vcp_feature(vcp_codes.input_select, mode_value)
5✔
459

460

461
def get_vcps() -> List[Type[vcp.VCP]]:
5✔
462
    """
463
    Discovers virtual control panels.
464

465
    This function should not be used directly in most cases, use
466
    :py:func:`get_monitors` get monitors with VCPs.
467

468
    Returns:
469
        List of VCPs in a closed state.
470

471
    Raises:
472
        NotImplementedError: not implemented for your operating system
473
        VCPError: failed to list VCPs
474
    """
475
    if sys.platform == "win32" or sys.platform.startswith("linux"):
5✔
476
        return vcp.get_vcps()
5✔
477
    else:
478
        raise NotImplementedError(f"not implemented for {sys.platform}")
×
479

480

481
def get_monitors() -> List[Monitor]:
5✔
482
    """
483
    Creates a list of all monitors.
484

485
    Returns:
486
        List of monitors in a closed state.
487

488
    Raises:
489
        VCPError: Failed to list VCPs.
490

491
    Example:
492
        Setting the power mode of all monitors to standby::
493

494
            for monitor in get_monitors():
495
                with monitor:
496
                    monitor.set_power_mode("standby")
497

498
        Setting all monitors to the maximum brightness using the
499
        context manager::
500

501
            for monitor in get_monitors():
502
                with monitor:
503
                    monitor.set_luminance(100)
504
    """
505
    return [Monitor(v) for v in vcp.get_vcps()]
5✔
506

507

508
def _extract_a_cap(caps_str: str, key: str) -> str:
5✔
509
    """
510
    Splits the capabilities string into individual sets.
511

512
    Returns:
513
        Dict of all values for the capability
514
    """
515
    start_of_filter = caps_str.upper().find(key.upper())
5✔
516

517
    if start_of_filter == -1:
5✔
518
        # not all keys are returned by monitor.
519
        # Also, sometimes the string has errors.
520
        return ""
5✔
521

522
    start_of_filter += len(key)
5✔
523
    filtered_caps_str = caps_str[start_of_filter:]
5✔
524
    end_of_filter = 0
5✔
525
    for i in range(len(filtered_caps_str)):
5✔
526
        if filtered_caps_str[i] == "(":
5✔
527
            end_of_filter += 1
5✔
528
        if filtered_caps_str[i] == ")":
5✔
529
            end_of_filter -= 1
5✔
530
        if end_of_filter == 0:
5✔
531
            # don't change end_of_filter to remove the closing ")"
532
            break
5✔
533

534
    # 1:i to remove the first character "("
535
    return filtered_caps_str[1:i]
5✔
536

537

538
def _convert_to_dict(caps_str: str) -> dict:
5✔
539
    """
540
    Parses the VCP capabilities string to a dictionary.
541
    Non-continuous capabilities will include an array of
542
    all supported values.
543

544
    Returns:
545
        Dict with all capabilities in hex
546

547
    Example:
548
        Expected string "04 14(05 06) 16" is converted to::
549

550
            {
551
                0x04: {},
552
                0x14: {0x05: {}, 0x06: {}},
553
                0x16: {},
554
            }
555
    """
556

557
    if len(caps_str) == 0:
5✔
558
        # Sometimes the keys aren't found and the extracting of
559
        # capabilities returns an empty string.
UNCOV
560
        return {}
×
561

562
    result_dict = {}
5✔
563
    group = []
5✔
564
    prev_val = None
5✔
565
    for chunk in caps_str.replace("(", " ( ").replace(")", " ) ").split(" "):
5✔
566
        if chunk == "":
5✔
567
            continue
5✔
568
        elif chunk == "(":
5✔
569
            group.append(prev_val)
5✔
570
        elif chunk == ")":
5✔
571
            group.pop(-1)
5✔
572
        else:
573
            val = int(chunk, 16)
5✔
574
            if len(group) == 0:
5✔
575
                result_dict[val] = {}
5✔
576
            else:
577
                d = result_dict
5✔
578
                for g in group:
5✔
579
                    d = d[g]
5✔
580
                d[val] = {}
5✔
581
            prev_val = val
5✔
582

583
    return result_dict
5✔
584

585

586
def _parse_capabilities(caps_str: str) -> dict:
5✔
587
    """
588
    Converts the capabilities string into a nice dict
589
    """
590
    caps_dict = {
5✔
591
        # Used to specify the protocol class
592
        "prot": "",
593
        # Identifies the type of display
594
        "type": "",
595
        # The display model number
596
        "model": "",
597
        # A list of supported VCP codes. Somehow not the same as "vcp"
598
        "cmds": "",
599
        # A list of supported VCP codes with a list of supported values
600
        # for each nc code
601
        "vcp": "",
602
        # undocumented
603
        "mswhql": "",
604
        # undocumented
605
        "asset_eep": "",
606
        # MCCS version implemented
607
        "mccs_ver": "",
608
        # Specifies the window, window type (PIP or Zone) safe area size
609
        # (bounded safe area) maximum size of the window, minimum size of
610
        # the window, and window supports VCP codes for control/adjustment.
611
        "window": "",
612
        # Alternate name to be used for control
613
        "vcpname": "",
614
        # Parsed input sources into text. Not part of capabilities string.
615
        "inputs": "",
616
        # Parsed color presets into text. Not part of capabilities string.
617
        "color_presets": "",
618
    }
619

620
    for key in caps_dict:
5✔
621
        if key in ["cmds", "vcp"]:
5✔
622
            caps_dict[key] = _convert_to_dict(_extract_a_cap(caps_str, key))
5✔
623
        else:
624
            caps_dict[key] = _extract_a_cap(caps_str, key)
5✔
625

626
    # Parse the input sources into a text list for readability
627
    input_source_cap = vcp_codes.input_select.value
5✔
628
    if input_source_cap in caps_dict["vcp"]:
5✔
629
        caps_dict["inputs"] = []
5✔
630
        input_val_list = list(caps_dict["vcp"][input_source_cap].keys())
5✔
631
        input_val_list.sort()
5✔
632

633
        for val in input_val_list:
5✔
634
            try:
5✔
635
                input_source = InputSource(val)
5✔
636
            except ValueError:
5✔
637
                input_source = val
5✔
638

639
            caps_dict["inputs"].append(input_source)
5✔
640

641
    # Parse the color presets into a text list for readability
642
    color_preset_cap = vcp_codes.image_color_preset.value
5✔
643
    if color_preset_cap in caps_dict["vcp"]:
5✔
644
        caps_dict["color_presets"] = []
5✔
645
        color_val_list = list(caps_dict["vcp"][color_preset_cap])
5✔
646
        color_val_list.sort()
5✔
647

648
        for val in color_val_list:
5✔
649
            try:
5✔
650
                color_source = ColorPreset(val)
5✔
UNCOV
651
            except ValueError:
×
UNCOV
652
                color_source = val
×
653

654
            caps_dict["color_presets"].append(color_source)
5✔
655

656
    return caps_dict
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc