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

newAM / monitorcontrol / 15259035318

26 May 2025 05:10PM UTC coverage: 57.357% (-0.2%) from 57.508%
15259035318

push

github

newAM
Change integer enums from Enum to IntEnum

3 of 3 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

382 of 666 relevant lines covered (57.36%)

2.87 hits per line

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

91.43
/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 InputSourceValueError(ValueError):
5✔
67
    """
68
    Raised upon an invalid (out of spec) input source value.
69

70
    https://github.com/newAM/monitorcontrol/issues/93
71

72
    Attributes:
73
        value (int): The value of the input source that was invalid.
74
    """
75

76
    def __init__(self, message: str, value: int):
5✔
77
        super().__init__(message)
5✔
78
        self.value = value
5✔
79

80

81
class Monitor:
5✔
82
    """
83
    A physical monitor attached to a Virtual Control Panel (VCP).
84

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

88
    All class methods must be called from within a context manager unless
89
    otherwise stated.
90

91
    Args:
92
        vcp: Virtual control panel for the monitor.
93
    """
94

95
    def __init__(self, vcp: vcp.VCP):
5✔
96
        self.vcp = vcp
5✔
97
        self.code_maximum = {}
5✔
98
        self._in_ctx = False
5✔
99

100
    def __enter__(self):
5✔
101
        self.vcp.__enter__()
5✔
102
        self._in_ctx = True
5✔
103
        return self
5✔
104

105
    def __exit__(
5✔
106
        self,
107
        exception_type: Optional[Type[BaseException]],
108
        exception_value: Optional[BaseException],
109
        exception_traceback: Optional[TracebackType],
110
    ) -> Optional[bool]:
111
        try:
5✔
112
            return self.vcp.__exit__(
5✔
113
                exception_type, exception_value, exception_traceback
114
            )
115
        finally:
116
            self._in_ctx = False
5✔
117

118
    def _get_code_maximum(self, code: vcp.VCPCode) -> int:
5✔
119
        """
120
        Gets the maximum values for a given code, and caches in the
121
        class dictionary if not already found.
122

123
        Args:
124
            code: Feature code definition class.
125

126
        Returns:
127
            Maximum value for the given code.
128

129
        Raises:
130
            TypeError: Code is write only.
131
        """
132
        assert self._in_ctx, "This function must be run within the context manager"
5✔
133
        if not code.readable:
5✔
134
            raise TypeError(f"code is not readable: {code.name}")
5✔
135

136
        if code.value in self.code_maximum:
5✔
137
            return self.code_maximum[code.value]
5✔
138
        else:
139
            _, maximum = self.vcp.get_vcp_feature(code.value)
5✔
140
            self.code_maximum[code.value] = maximum
5✔
141
            return maximum
5✔
142

143
    def _set_vcp_feature(self, code: vcp.VCPCode, value: int):
5✔
144
        """
145
        Sets the value of a feature on the virtual control panel.
146

147
        Args:
148
            code: Feature code.
149
            value: Feature value.
150

151
        Raises:
152
            TypeError: Code is ready only.
153
            ValueError: Value is greater than the maximum allowable.
154
            VCPError: Failed to get VCP feature.
155
        """
156
        assert self._in_ctx, "This function must be run within the context manager"
5✔
157
        if code.type == "ro":
5✔
158
            raise TypeError(f"cannot write read-only code: {code.name}")
5✔
159
        elif code.type == "rw" and code.function == "c":
5✔
160
            maximum = self._get_code_maximum(code)
5✔
161
            if value > maximum:
5✔
162
                raise ValueError(f"value of {value} exceeds code maximum of {maximum}")
5✔
163

164
        self.vcp.set_vcp_feature(code.value, value)
5✔
165

166
    def _get_vcp_feature(self, code: vcp.VCPCode) -> int:
5✔
167
        """
168
        Gets the value of a feature from the virtual control panel.
169

170
        Args:
171
            code: Feature code.
172

173
        Returns:
174
            Current feature value.
175

176
        Raises:
177
            TypeError: Code is write only.
178
            VCPError: Failed to get VCP feature.
179
        """
180
        assert self._in_ctx, "This function must be run within the context manager"
5✔
181
        if code.type == "wo":
5✔
182
            raise TypeError(f"cannot read write-only code: {code.name}")
5✔
183

184
        current, maximum = self.vcp.get_vcp_feature(code.value)
5✔
185
        return current
5✔
186

187
    def get_vcp_capabilities(self) -> dict:
5✔
188
        """
189
        Gets the capabilities of the monitor
190

191
        Returns:
192
            Dictionary of capabilities in the following example format::
193

194
                {
195
                    "prot": "monitor",
196
                    "type": "LCD",
197
                    "cmds": {
198
                            1: [],
199
                            2: [],
200
                            96: [15, 17, 18],
201
                    },
202
                    "inputs": [
203
                        InputSource.DP1,
204
                        InputSource.HDMI1,
205
                        InputSource.HDMI2
206
                        # this may return integers for out-of-spec values,
207
                        # such as USB Type-C monitors
208
                    ],
209
                }
210
        """
211
        assert self._in_ctx, "This function must be run within the context manager"
5✔
212

213
        cap_str = self.vcp.get_vcp_capabilities()
5✔
214

215
        res = _parse_capabilities(cap_str)
5✔
216
        return res
5✔
217

218
    def get_luminance(self) -> int:
5✔
219
        """
220
        Gets the monitors back-light luminance.
221

222
        Returns:
223
            Current luminance value.
224

225
        Example:
226
            Basic Usage::
227

228
                from monitorcontrol import get_monitors
229

230
                for monitor in get_monitors():
231
                    with monitor:
232
                        print(monitor.get_luminance())
233

234
        Raises:
235
            VCPError: Failed to get luminance from the VCP.
236
        """
237
        return self._get_vcp_feature(vcp_codes.image_luminance)
5✔
238

239
    def set_luminance(self, value: int):
5✔
240
        """
241
        Sets the monitors back-light luminance.
242

243
        Args:
244
            value: New luminance value (typically 0-100).
245

246
        Example:
247
            Basic Usage::
248

249
                from monitorcontrol import get_monitors
250

251
                for monitor in get_monitors():
252
                    with monitor:
253
                        monitor.set_luminance(50)
254

255
        Raises:
256
            ValueError: Luminance outside of valid range.
257
            VCPError: Failed to set luminance in the VCP.
258
        """
259
        self._set_vcp_feature(vcp_codes.image_luminance, value)
5✔
260

261
    def get_color_preset(self) -> int:
5✔
262
        """
263
        Gets the monitors color preset.
264

265
        Returns:
266
            Current color preset.
267
            Valid values are enumerated in :py:class:`ColorPreset`.
268

269
        Example:
270
            Basic Usage::
271

272
                from monitorcontrol import get_monitors
273

274
                for monitor in get_monitors():
275
                    with monitor:
276
                        print(monitor.get_color_preset())
277

278
        Raises:
279
            VCPError: Failed to get color preset from the VCP.
280
        """
281
        return self._get_vcp_feature(vcp_codes.image_color_preset)
×
282

283
    def set_color_preset(self, value: Union[int, str, ColorPreset]):
5✔
284
        """
285
        Sets the monitors color preset.
286

287
        Args:
288
            value:
289
                An integer color preset,
290
                or a string representing the color preset,
291
                or a value from :py:class:`ColorPreset`.
292

293
        Example:
294
            Basic Usage::
295

296
                from monitorcontrol import get_monitors, ColorPreset
297

298
                for monitor in get_monitors():
299
                    with monitor:
300
                        monitor.set_color_preset(ColorPreset.COLOR_TEMP_5000K)
301

302
        Raises:
303
            VCPError: Failed to set color preset in the VCP.
304
            ValueError: Color preset outside valid range.
305
            AttributeError: Color preset string is invalid.
306
            TypeError: Unsupported value
307
        """
308
        if isinstance(value, str):
×
309
            mode_value = getattr(ColorPreset, value).value
×
310
        elif isinstance(value, int):
×
311
            mode_value = ColorPreset(value).value
×
312
        elif isinstance(value, ColorPreset):
×
313
            mode_value = value.value
×
314
        else:
315
            raise TypeError("unsupported color preset: " + repr(type(value)))
×
316

317
        self._set_vcp_feature(vcp_codes.image_color_preset, mode_value)
×
318

319
    def get_contrast(self) -> int:
5✔
320
        """
321
        Gets the monitors contrast.
322

323
        Returns:
324
            Current contrast value.
325

326
        Example:
327
            Basic Usage::
328

329
                from monitorcontrol import get_monitors
330

331
                for monitor in get_monitors():
332
                    with monitor:
333
                        print(monitor.get_contrast())
334

335
        Raises:
336
            VCPError: Failed to get contrast from the VCP.
337
        """
338
        return self._get_vcp_feature(vcp_codes.image_contrast)
5✔
339

340
    def set_contrast(self, value: int):
5✔
341
        """
342
        Sets the monitors back-light contrast.
343

344
        Args:
345
            value: New contrast value (typically 0-100).
346

347
        Example:
348
            Basic Usage::
349

350
                from monitorcontrol import get_monitors
351

352
                for monitor in get_monitors():
353
                    with monitor:
354
                        print(monitor.set_contrast(50))
355

356
        Raises:
357
            ValueError: Contrast outside of valid range.
358
            VCPError: Failed to set contrast in the VCP.
359
        """
360
        self._set_vcp_feature(vcp_codes.image_contrast, value)
5✔
361

362
    def get_power_mode(self) -> PowerMode:
5✔
363
        """
364
        Get the monitor power mode.
365

366
        Returns:
367
            Value from the :py:class:`PowerMode` enumeration.
368

369
        Example:
370
            Basic Usage::
371

372
                from monitorcontrol import get_monitors
373

374
                for monitor in get_monitors():
375
                    with monitor:
376
                        print(monitor.get_power_mode())
377

378
        Raises:
379
            VCPError: Failed to get the power mode.
380
            ValueError: Set power state outside of valid range.
381
            KeyError: Set power mode string is invalid.
382
        """
383
        value = self._get_vcp_feature(vcp_codes.display_power_mode)
5✔
384
        return PowerMode(value)
5✔
385

386
    def set_power_mode(self, value: Union[int, str, PowerMode]):
5✔
387
        """
388
        Set the monitor power mode.
389

390
        Args:
391
            value:
392
                An integer power mode,
393
                or a string representing the power mode,
394
                or a value from :py:class:`PowerMode`.
395

396
        Example:
397
            Basic Usage::
398

399
                from monitorcontrol import get_monitors
400

401
                for monitor in get_monitors():
402
                    with monitor:
403
                        monitor.set_power_mode("standby")
404

405
        Raises:
406
            VCPError: Failed to get or set the power mode
407
            ValueError: Power state outside of valid range.
408
            AttributeError: Power mode string is invalid.
409
        """
410
        if isinstance(value, str):
5✔
411
            mode_value = getattr(PowerMode, value).value
5✔
412
        elif isinstance(value, int):
5✔
413
            mode_value = PowerMode(value).value
5✔
414
        elif isinstance(value, PowerMode):
5✔
415
            mode_value = value.value
×
416
        else:
417
            raise TypeError("unsupported mode type: " + repr(type(value)))
5✔
418

419
        self._set_vcp_feature(vcp_codes.display_power_mode, mode_value)
5✔
420

421
    def get_input_source(self) -> InputSource:
5✔
422
        """
423
        Gets the monitors input source
424

425
        Returns:
426
            Current input source.
427

428
        Example:
429
            Basic Usage::
430

431
                from monitorcontrol import get_monitors
432

433
                for monitor in get_monitors():
434
                    with monitor:
435
                        print(monitor.get_input_source())
436

437
            Handling out-of-spec inputs (observed for USB type-C inputs)::
438

439
                from monitorcontrol import get_monitors, InputSourceValueError
440

441
                for monitor in get_monitors():
442
                    with monitor:
443
                        try:
444
                            print(monitor.get_input_source())
445
                        except InputSourceValueError as e:
446
                            print(e.value)
447

448
        Raises:
449
            VCPError: Failed to get input source from the VCP.
450
            InputSourceValueError:
451
                Input source value is not within the MCCS defined inputs.
452
        """
453
        value = self._get_vcp_feature(vcp_codes.input_select) & 0xFF
5✔
454
        try:
5✔
455
            return InputSource(value)
5✔
456
        except ValueError:
5✔
457
            raise InputSourceValueError(
5✔
458
                f"{value} is not a valid InputSource", value
459
            ) from None
460

461
    def set_input_source(self, value: Union[int, str, InputSource]):
5✔
462
        """
463
        Sets the monitors input source.
464

465
        Args:
466
            value: New input source
467

468
        Example:
469
            Basic Usage::
470

471
                from monitorcontrol import get_monitors
472

473
                for monitor in get_monitors():
474
                    with monitor:
475
                        print(monitor.set_input_source("DP1"))
476

477
        Raises:
478
            VCPError: Failed to get the input source.
479
            KeyError: Set input source string is invalid.
480
        """
481

482
        if isinstance(value, str):
5✔
483
            mode_value = getattr(InputSource, value.upper()).value
×
484
        elif isinstance(value, int):
5✔
485
            mode_value = value
5✔
UNCOV
486
        elif isinstance(value, InputSource):
×
UNCOV
487
            mode_value = value.value
×
488
        else:
489
            raise TypeError("unsupported input type: " + repr(type(value)))
×
490

491
        self._set_vcp_feature(vcp_codes.input_select, mode_value)
5✔
492

493

494
def get_vcps() -> List[Type[vcp.VCP]]:
5✔
495
    """
496
    Discovers virtual control panels.
497

498
    This function should not be used directly in most cases, use
499
    :py:func:`get_monitors` get monitors with VCPs.
500

501
    Returns:
502
        List of VCPs in a closed state.
503

504
    Raises:
505
        NotImplementedError: not implemented for your operating system
506
        VCPError: failed to list VCPs
507
    """
508
    if sys.platform == "win32" or sys.platform.startswith("linux"):
5✔
509
        return vcp.get_vcps()
5✔
510
    else:
511
        raise NotImplementedError(f"not implemented for {sys.platform}")
×
512

513

514
def get_monitors() -> List[Monitor]:
5✔
515
    """
516
    Creates a list of all monitors.
517

518
    Returns:
519
        List of monitors in a closed state.
520

521
    Raises:
522
        VCPError: Failed to list VCPs.
523

524
    Example:
525
        Setting the power mode of all monitors to standby::
526

527
            for monitor in get_monitors():
528
                with monitor:
529
                    monitor.set_power_mode("standby")
530

531
        Setting all monitors to the maximum brightness using the
532
        context manager::
533

534
            for monitor in get_monitors():
535
                with monitor:
536
                    monitor.set_luminance(100)
537
    """
538
    return [Monitor(v) for v in vcp.get_vcps()]
5✔
539

540

541
def _extract_a_cap(caps_str: str, key: str) -> str:
5✔
542
    """
543
    Splits the capabilities string into individual sets.
544

545
    Returns:
546
        Dict of all values for the capability
547
    """
548
    start_of_filter = caps_str.upper().find(key.upper())
5✔
549

550
    if start_of_filter == -1:
5✔
551
        # not all keys are returned by monitor.
552
        # Also, sometimes the string has errors.
553
        return ""
5✔
554

555
    start_of_filter += len(key)
5✔
556
    filtered_caps_str = caps_str[start_of_filter:]
5✔
557
    end_of_filter = 0
5✔
558
    for i in range(len(filtered_caps_str)):
5✔
559
        if filtered_caps_str[i] == "(":
5✔
560
            end_of_filter += 1
5✔
561
        if filtered_caps_str[i] == ")":
5✔
562
            end_of_filter -= 1
5✔
563
        if end_of_filter == 0:
5✔
564
            # don't change end_of_filter to remove the closing ")"
565
            break
5✔
566

567
    # 1:i to remove the first character "("
568
    return filtered_caps_str[1:i]
5✔
569

570

571
def _convert_to_dict(caps_str: str) -> dict:
5✔
572
    """
573
    Parses the VCP capabilities string to a dictionary.
574
    Non-continuous capabilities will include an array of
575
    all supported values.
576

577
    Returns:
578
        Dict with all capabilities in hex
579

580
    Example:
581
        Expected string "04 14(05 06) 16" is converted to::
582

583
            {
584
                0x04: {},
585
                0x14: {0x05: {}, 0x06: {}},
586
                0x16: {},
587
            }
588
    """
589

590
    if len(caps_str) == 0:
5✔
591
        # Sometimes the keys aren't found and the extracting of
592
        # capabilities returns an empty string.
593
        return {}
×
594

595
    result_dict = {}
5✔
596
    group = []
5✔
597
    prev_val = None
5✔
598
    for chunk in caps_str.replace("(", " ( ").replace(")", " ) ").split(" "):
5✔
599
        if chunk == "":
5✔
600
            continue
5✔
601
        elif chunk == "(":
5✔
602
            group.append(prev_val)
5✔
603
        elif chunk == ")":
5✔
604
            group.pop(-1)
5✔
605
        else:
606
            val = int(chunk, 16)
5✔
607
            if len(group) == 0:
5✔
608
                result_dict[val] = {}
5✔
609
            else:
610
                d = result_dict
5✔
611
                for g in group:
5✔
612
                    d = d[g]
5✔
613
                d[val] = {}
5✔
614
            prev_val = val
5✔
615

616
    return result_dict
5✔
617

618

619
def _parse_capabilities(caps_str: str) -> dict:
5✔
620
    """
621
    Converts the capabilities string into a nice dict
622
    """
623
    caps_dict = {
5✔
624
        # Used to specify the protocol class
625
        "prot": "",
626
        # Identifies the type of display
627
        "type": "",
628
        # The display model number
629
        "model": "",
630
        # A list of supported VCP codes. Somehow not the same as "vcp"
631
        "cmds": "",
632
        # A list of supported VCP codes with a list of supported values
633
        # for each nc code
634
        "vcp": "",
635
        # undocumented
636
        "mswhql": "",
637
        # undocumented
638
        "asset_eep": "",
639
        # MCCS version implemented
640
        "mccs_ver": "",
641
        # Specifies the window, window type (PIP or Zone) safe area size
642
        # (bounded safe area) maximum size of the window, minimum size of
643
        # the window, and window supports VCP codes for control/adjustment.
644
        "window": "",
645
        # Alternate name to be used for control
646
        "vcpname": "",
647
        # Parsed input sources into text. Not part of capabilities string.
648
        "inputs": "",
649
        # Parsed color presets into text. Not part of capabilities string.
650
        "color_presets": "",
651
    }
652

653
    for key in caps_dict:
5✔
654
        if key in ["cmds", "vcp"]:
5✔
655
            caps_dict[key] = _convert_to_dict(_extract_a_cap(caps_str, key))
5✔
656
        else:
657
            caps_dict[key] = _extract_a_cap(caps_str, key)
5✔
658

659
    # Parse the input sources into a text list for readability
660
    input_source_cap = vcp_codes.input_select.value
5✔
661
    if input_source_cap in caps_dict["vcp"]:
5✔
662
        caps_dict["inputs"] = []
5✔
663
        input_val_list = list(caps_dict["vcp"][input_source_cap].keys())
5✔
664
        input_val_list.sort()
5✔
665

666
        for val in input_val_list:
5✔
667
            try:
5✔
668
                input_source = InputSource(val)
5✔
669
            except ValueError:
5✔
670
                input_source = val
5✔
671

672
            caps_dict["inputs"].append(input_source)
5✔
673

674
    # Parse the color presets into a text list for readability
675
    color_preset_cap = vcp_codes.image_color_preset.value
5✔
676
    if color_preset_cap in caps_dict["vcp"]:
5✔
677
        caps_dict["color_presets"] = []
5✔
678
        color_val_list = list(caps_dict["vcp"][color_preset_cap])
5✔
679
        color_val_list.sort()
5✔
680

681
        for val in color_val_list:
5✔
682
            try:
5✔
683
                color_source = ColorPreset(val)
5✔
684
            except ValueError:
×
685
                color_source = val
×
686

687
            caps_dict["color_presets"].append(color_source)
5✔
688

689
    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