screen_brightness_control.linux

   1import fcntl
   2import functools
   3import glob
   4import logging
   5import operator
   6import os
   7import re
   8import time
   9from typing import List, Optional, Tuple, Union
  10
  11from . import filter_monitors, get_methods
  12from .exceptions import I2CValidationError, NoValidDisplayError, format_exc
  13from .helpers import (EDID, BrightnessMethod, BrightnessMethodAdv, __Cache,
  14                      _monitor_brand_lookup, check_output)
  15
  16__cache__ = __Cache()
  17logger = logging.getLogger(__name__)
  18
  19
  20class SysFiles(BrightnessMethod):
  21    '''
  22    A way of getting display information and adjusting the brightness
  23    that does not rely on any 3rd party software.
  24
  25    This class works with displays that show up in the `/sys/class/backlight`
  26    directory (so usually laptop displays).
  27
  28    To set the brightness, your user will need write permissions for
  29    `/sys/class/backlight/*/brightness` or you will need to run the program
  30    as root.
  31    '''
  32    logger = logger.getChild('SysFiles')
  33
  34    @classmethod
  35    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
  36        '''
  37        Returns information about detected displays by reading files from the
  38        `/sys/class/backlight` directory
  39
  40        Args:
  41            display (str or int): The display to return info about.
  42                Pass in the serial number, name, model, interface, edid or index.
  43                This is passed to `filter_monitors`
  44
  45        Returns:
  46            list: list of dicts
  47
  48        Example:
  49            ```python
  50            import screen_brightness_control as sbc
  51
  52            # get info about all displays
  53            info = sbc.linux.SysFiles.get_display_info()
  54            # EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]
  55
  56            # get info about the primary display
  57            primary_info = sbc.linux.SysFiles.get_display_info(0)[0]
  58
  59            # get info about a display called 'edp-backlight'
  60            edp_info = sbc.linux.SysFiles.get_display_info('edp-backlight')[0]
  61            ```
  62        '''
  63        subsystems = set()
  64        for folder in os.listdir('/sys/class/backlight'):
  65            if os.path.isdir(f'/sys/class/backlight/{folder}/subsystem'):
  66                subsystems.add(tuple(os.listdir(f'/sys/class/backlight/{folder}/subsystem')))
  67
  68        all_displays = {}
  69        index = 0
  70
  71        for subsystem in subsystems:
  72
  73            device = {
  74                'name': subsystem[0],
  75                'path': f'/sys/class/backlight/{subsystem[0]}',
  76                'method': cls,
  77                'index': index,
  78                'model': None,
  79                'serial': None,
  80                'manufacturer': None,
  81                'manufacturer_id': None,
  82                'edid': None,
  83                'scale': None
  84            }
  85
  86            for folder in subsystem:
  87                # subsystems like intel_backlight usually have an acpi_video0
  88                # counterpart, which we don't want so lets find the 'best' candidate
  89                try:
  90                    with open(os.path.join(f'/sys/class/backlight/{folder}/max_brightness')) as f:
  91                        scale = int(f.read().rstrip(' \n')) / 100
  92
  93                    # use the display with the highest resolution scale
  94                    if device['scale'] is None or scale > device['scale']:
  95                        device['name'] = folder
  96                        device['path'] = f'/sys/class/backlight/{folder}'
  97                        device['scale'] = scale
  98                except (FileNotFoundError, TypeError) as e:
  99                    cls.logger.error(
 100                        f'error getting highest resolution scale for {folder}'
 101                        f' - {format_exc(e)}'
 102                    )
 103                    continue
 104
 105            if os.path.isfile('%s/device/edid' % device['path']):
 106                device['edid'] = EDID.hexdump('%s/device/edid' % device['path'])
 107
 108                for key, value in zip(
 109                    ('manufacturer_id', 'manufacturer', 'model', 'name', 'serial'),
 110                    EDID.parse(device['edid'])
 111                ):
 112                    if value is None:
 113                        continue
 114                    device[key] = value
 115
 116            all_displays[device['edid']] = device
 117            index += 1
 118
 119        all_displays = list(all_displays.values())
 120        if display is not None:
 121            all_displays = filter_monitors(display=display, haystack=all_displays, include=['path'])
 122        return all_displays
 123
 124    @classmethod
 125    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
 126        '''
 127        Gets the brightness for a display by reading the brightness files
 128        stored in `/sys/class/backlight/*/brightness`
 129
 130        Args:
 131            display (int): The specific display you wish to query.
 132
 133        Returns:
 134            list: list of ints (0 to 100)
 135
 136        Example:
 137            ```python
 138            import screen_brightness_control as sbc
 139
 140            # get the current display brightness
 141            current_brightness = sbc.linux.SysFiles.get_brightness()
 142
 143            # get the brightness of the primary display
 144            primary_brightness = sbc.linux.SysFiles.get_brightness(display = 0)[0]
 145
 146            # get the brightness of the secondary display
 147            secondary_brightness = sbc.linux.SysFiles.get_brightness(display = 1)[0]
 148            ```
 149        '''
 150        info = cls.get_display_info()
 151        if display is not None:
 152            info = [info[display]]
 153
 154        results = []
 155        for device in info:
 156            with open(os.path.join(device['path'], 'brightness'), 'r') as f:
 157                brightness = int(f.read().rstrip('\n'))
 158            results.append(int(brightness / device['scale']))
 159
 160        return results
 161
 162    @classmethod
 163    def set_brightness(cls, value: int, display: Optional[int] = None):
 164        '''
 165        Sets the brightness for a display by writing to the brightness files
 166        stored in `/sys/class/backlight/*/brightness`.
 167        This function requires permission to write to these files which is
 168        usually provided when it's run as root.
 169
 170        Args:
 171            value (int): Sets the brightness to this value
 172            display (int): The specific display you wish to adjust.
 173
 174        Example:
 175            ```python
 176            import screen_brightness_control as sbc
 177
 178            # set the brightness to 50%
 179            sbc.linux.SysFiles.set_brightness(50)
 180
 181            # set the primary display brightness to 75%
 182            sbc.linux.SysFiles.set_brightness(75, display = 0)
 183
 184            # set the secondary display brightness to 25%
 185            sbc.linux.SysFiles.set_brightness(25, display = 1)
 186            ```
 187        '''
 188        info = cls.get_display_info()
 189        if display is not None:
 190            info = [info[display]]
 191
 192        for device in info:
 193            with open(os.path.join(device['path'], 'brightness'), 'w') as f:
 194                f.write(str(int(value * device['scale'])))
 195
 196
 197class I2C(BrightnessMethod):
 198    '''
 199    In the same spirit as `SysFiles`, this class serves as a way of getting
 200    display information and adjusting the brightness without relying on any
 201    3rd party software.
 202
 203    Usage of this class requires read and write permission for `/dev/i2c-*`.
 204
 205    This class works over the I2C bus, primarily with desktop monitors as I
 206    haven't tested any e-DP displays yet.
 207
 208    Massive thanks to [siemer](https://github.com/siemer) for
 209    his work on the [ddcci.py](https://github.com/siemer/ddcci) project,
 210    which served as a my main reference for this.
 211
 212    References:
 213        * [ddcci.py](https://github.com/siemer/ddcci)
 214        * [DDCCI Spec](https://milek7.pl/ddcbacklight/ddcci.pdf)
 215    '''
 216    logger = logger.getChild('I2C')
 217
 218    # vcp commands
 219    GET_VCP_CMD = 0x01
 220    '''VCP command to get the value of a feature (eg: brightness)'''
 221    GET_VCP_REPLY = 0x02
 222    '''VCP feature reply op code'''
 223    SET_VCP_CMD = 0x03
 224    '''VCP command to set the value of a feature (eg: brightness)'''
 225
 226    # addresses
 227    DDCCI_ADDR = 0x37
 228    '''DDC packets are transmittred using this I2C address'''
 229    HOST_ADDR_R = 0x50
 230    '''Packet source address (the computer) when reading data'''
 231    HOST_ADDR_W = 0x51
 232    '''Packet source address (the computer) when writing data'''
 233    DESTINATION_ADDR_W = 0x6e
 234    '''Packet destination address (the monitor) when writing data'''
 235    I2C_SLAVE = 0x0703
 236    '''The I2C slave address'''
 237
 238    # timings
 239    WAIT_TIME = 0.05
 240    '''How long to wait between I2C commands'''
 241
 242    _max_brightness_cache: dict = {}
 243
 244    class I2CDevice():
 245        '''
 246        Class to read and write data to an I2C bus,
 247        based on the `I2CDev` class from [ddcci.py](https://github.com/siemer/ddcci)
 248        '''
 249        def __init__(self, fname: str, slave_addr: int):
 250            '''
 251            Args:
 252                fname (str): the I2C path, eg: `/dev/i2c-2`
 253                slave_addr (int): not entirely sure what this is meant to be
 254            '''
 255            self.device = os.open(fname, os.O_RDWR)
 256            # I2C_SLAVE address setup
 257            fcntl.ioctl(self.device, I2C.I2C_SLAVE, slave_addr)
 258
 259        def read(self, length: int) -> bytes:
 260            '''
 261            Read a certain number of bytes from the I2C bus
 262
 263            Args:
 264                length (int): the number of bytes to read
 265
 266            Returns:
 267                bytes
 268            '''
 269            return os.read(self.device, length)
 270
 271        def write(self, data: bytes) -> int:
 272            '''
 273            Writes data to the I2C bus
 274
 275            Args:
 276                data (bytes): the data to write
 277
 278            Returns:
 279                int: the number of bytes written
 280            '''
 281            return os.write(self.device, data)
 282
 283    class DDCInterface(I2CDevice):
 284        '''
 285        Class to send DDC (Display Data Channel) commands to an I2C device,
 286        based on the `Ddcci` and `Mccs` classes from [ddcci.py](https://github.com/siemer/ddcci)
 287        '''
 288
 289        PROTOCOL_FLAG = 0x80
 290
 291        def __init__(self, i2c_path: str):
 292            '''
 293            Args:
 294                i2c_path (str): the path to the I2C device, eg: `/dev/i2c-2`
 295            '''
 296            self.logger = logger.getChild(self.__class__.__name__).getChild(i2c_path)
 297            super().__init__(i2c_path, I2C.DDCCI_ADDR)
 298
 299        def write(self, *args) -> int:
 300            '''
 301            Write some data to the I2C device.
 302
 303            It is recommended to use `setvcp` to set VCP values on the DDC device
 304            instead of using this function directly.
 305
 306            Args:
 307                *args: variable length list of arguments. This will be put
 308                    into a `bytearray` and wrapped up in various flags and
 309                    checksums before being written to the I2C device
 310
 311            Returns:
 312                int: the number of bytes that were written
 313            '''
 314            time.sleep(I2C.WAIT_TIME)
 315
 316            ba = bytearray(args)
 317            ba.insert(0, len(ba) | self.PROTOCOL_FLAG)  # add length info
 318            ba.insert(0, I2C.HOST_ADDR_W)  # insert source address
 319            ba.append(functools.reduce(operator.xor, ba, I2C.DESTINATION_ADDR_W))  # checksum
 320
 321            return super().write(ba)
 322
 323        def setvcp(self, vcp_code: int, value: int) -> int:
 324            '''
 325            Set a VCP value on the device
 326
 327            Args:
 328                vcp_code (int): the VCP command to send, eg: `0x10` is brightness
 329                value (int): what to set the value to
 330
 331            Returns:
 332                int: the number of bytes written to the device
 333            '''
 334            return self.write(I2C.SET_VCP_CMD, vcp_code, *value.to_bytes(2, 'big'))
 335
 336        def read(self, amount: int) -> bytes:
 337            '''
 338            Reads data from the DDC device.
 339
 340            It is recommended to use `getvcp` to retrieve VCP values from the
 341            DDC device instead of using this function directly.
 342
 343            Args:
 344                amount (int): the number of bytes to read
 345
 346            Returns:
 347                bytes
 348
 349            Raises:
 350                ValueError: if the read data is deemed invalid
 351            '''
 352            time.sleep(I2C.WAIT_TIME)
 353
 354            ba = super().read(amount + 3)
 355
 356            # check the bytes read
 357            checks = {
 358                'source address': ba[0] == I2C.DESTINATION_ADDR_W,
 359                'checksum': functools.reduce(operator.xor, ba) == I2C.HOST_ADDR_R,
 360                'length': len(ba) >= (ba[1] & ~self.PROTOCOL_FLAG) + 3
 361            }
 362            if False in checks.values():
 363                self.logger.error('i2c read check failed: ' + repr(checks))
 364                raise I2CValidationError('i2c read check failed: ' + repr(checks))
 365
 366            return ba[2:-1]
 367
 368        def getvcp(self, vcp_code: int) -> Tuple[int, int]:
 369            '''
 370            Retrieves a VCP value from the DDC device.
 371
 372            Args:
 373                vcp_code (int): the VCP value to read, eg: `0x10` is brightness
 374
 375            Returns:
 376                tuple[int, int]: the current and maximum value respectively
 377
 378            Raises:
 379                ValueError: if the read data is deemed invalid
 380            '''
 381            self.write(I2C.GET_VCP_CMD, vcp_code)
 382            ba = self.read(8)
 383
 384            checks = {
 385                'is feature reply': ba[0] == I2C.GET_VCP_REPLY,
 386                'supported VCP opcode': ba[1] == 0,
 387                'answer matches request': ba[2] == vcp_code
 388            }
 389            if False in checks.values():
 390                self.logger.error('i2c read check failed: ' + repr(checks))
 391                raise I2CValidationError('i2c read check failed: ' + repr(checks))
 392
 393            # current and max values
 394            return int.from_bytes(ba[6:8], 'big'), int.from_bytes(ba[4:6], 'big')
 395
 396    @classmethod
 397    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
 398        '''
 399        Returns information about detected displays by querying the various I2C buses
 400
 401        Args:
 402            display (str or int): The display to return info about.
 403                Pass in the serial number, name, model, interface, edid or index.
 404                This is passed to `filter_monitors`
 405
 406        Returns:
 407            list: list of dicts
 408
 409        Example:
 410            ```python
 411            import screen_brightness_control as sbc
 412
 413            # get info about all displays
 414            info = sbc.linux.I2C.get_display_info()
 415            # EG output: [{'name': 'Benq GL2450H', 'model': 'GL2450H', 'manufacturer': 'BenQ', 'edid': '00ffff...'}]
 416
 417            # get info about the primary display
 418            primary_info = sbc.linux.I2C.get_display_info(0)[0]
 419
 420            # get info about a display called 'Benq GL2450H'
 421            benq_info = sbc.linux.I2C.get_display_info('Benq GL2450H')[0]
 422            ```
 423        '''
 424        all_displays = __cache__.get('i2c_display_info')
 425        if all_displays is None:
 426            all_displays = []
 427            index = 0
 428
 429            for i2c_path in glob.glob('/dev/i2c-*'):
 430                if not os.path.exists(i2c_path):
 431                    continue
 432
 433                try:
 434                    # open the I2C device using the host read address
 435                    device = cls.I2CDevice(i2c_path, cls.HOST_ADDR_R)
 436                    # read some 512 bytes from the device
 437                    data = device.read(512)
 438                except IOError as e:
 439                    cls.logger.error(f'IOError reading from device {i2c_path}: {e}')
 440                    continue
 441
 442                # search for the EDID header within our 512 read bytes
 443                start = data.find(bytes.fromhex('00 FF FF FF FF FF FF 00'))
 444                if start < 0:
 445                    continue
 446
 447                # grab 128 bytes of the edid
 448                edid = data[start: start + 128]
 449                # parse the EDID
 450                manufacturer_id, manufacturer, model, name, serial = EDID.parse(edid)
 451                # convert edid to hex string
 452                edid = ''.join(f'{i:02x}' for i in edid)
 453
 454                all_displays.append(
 455                    {
 456                        'name': name,
 457                        'model': model,
 458                        'manufacturer': manufacturer,
 459                        'manufacturer_id': manufacturer_id,
 460                        'serial': serial,
 461                        'method': cls,
 462                        'index': index,
 463                        'edid': edid,
 464                        'i2c_bus': i2c_path
 465                    }
 466                )
 467                index += 1
 468
 469            if all_displays:
 470                __cache__.store('i2c_display_info', all_displays, expires=2)
 471
 472        if display is not None:
 473            return filter_monitors(display=display, haystack=all_displays, include=['i2c_bus'])
 474        return all_displays
 475
 476    @classmethod
 477    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
 478        '''
 479        Gets the brightness for a display by querying the I2C bus
 480
 481        Args:
 482            display (int): The specific display you wish to query.
 483
 484        Returns:
 485            list: list of ints (0 to 100)
 486
 487        Example:
 488            ```python
 489            import screen_brightness_control as sbc
 490
 491            # get the current display brightness
 492            current_brightness = sbc.linux.I2C.get_brightness()
 493
 494            # get the brightness of the primary display
 495            primary_brightness = sbc.linux.I2C.get_brightness(display = 0)[0]
 496
 497            # get the brightness of the secondary display
 498            secondary_brightness = sbc.linux.I2C.get_brightness(display = 1)[0]
 499            ```
 500        '''
 501        all_displays = cls.get_display_info()
 502        if display is not None:
 503            all_displays = [all_displays[display]]
 504
 505        results = []
 506        for device in all_displays:
 507            interface = cls.DDCInterface(device['i2c_bus'])
 508            value, max_value = interface.getvcp(0x10)
 509
 510            # make sure display's max brighness is cached
 511            cache_ident = '%s-%s-%s' % (device['name'], device['model'], device['serial'])
 512            if cache_ident not in cls._max_brightness_cache:
 513                cls._max_brightness_cache[cache_ident] = max_value
 514                cls.logger.info(f'{cache_ident} max brightness:{max_value} (current: {value})')
 515
 516            if max_value != 100:
 517                # if max value is not 100 then we have to adjust the scale to be
 518                # a percentage
 519                value = int((value / max_value) * 100)
 520
 521            results.append(value)
 522
 523        return results
 524
 525    @classmethod
 526    def set_brightness(cls, value: int, display: Optional[int] = None):
 527        '''
 528        Sets the brightness for a display by writing to the I2C bus
 529
 530        Args:
 531            value (int): Set the brightness to this value
 532            display (int): The specific display you wish to adjust.
 533
 534        Example:
 535            ```python
 536            import screen_brightness_control as sbc
 537
 538            # set the brightness to 50%
 539            sbc.linux.I2C.set_brightness(50)
 540
 541            # set the primary display brightness to 75%
 542            sbc.linux.I2C.set_brightness(75, display = 0)
 543
 544            # set the secondary display brightness to 25%
 545            sbc.linux.I2C.set_brightness(25, display = 1)
 546            ```
 547        '''
 548        all_displays = cls.get_display_info()
 549        if display is not None:
 550            all_displays = [all_displays[display]]
 551
 552        for device in all_displays:
 553            # make sure display brightness max value is cached
 554            cache_ident = '%s-%s-%s' % (device['name'], device['model'], device['serial'])
 555            if cache_ident not in cls._max_brightness_cache:
 556                cls.get_brightness(display=device['index'])
 557
 558            # scale the brightness value according to the max brightness
 559            max_value = cls._max_brightness_cache[cache_ident]
 560            if max_value != 100:
 561                value = int((value / 100) * max_value)
 562
 563            interface = cls.DDCInterface(device['i2c_bus'])
 564            interface.setvcp(0x10, value)
 565
 566
 567class Light(BrightnessMethod):
 568    '''collection of screen brightness related methods using the light executable'''
 569
 570    executable: str = 'light'
 571    '''the light executable to be called'''
 572
 573    @classmethod
 574    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
 575        '''
 576        Returns information about detected displays as reported by Light.
 577
 578        It works by taking the output of `SysFiles.get_display_info` and
 579        filtering out any displays that aren't supported by Light
 580
 581        Args:
 582            display (str or int): The display to return info about.
 583                Pass in the serial number, name, model, interface, edid or index.
 584                This is passed to `filter_monitors`
 585
 586        Returns:
 587            list: list of dicts
 588
 589        Example:
 590            ```python
 591            import screen_brightness_control as sbc
 592
 593            # get info about all displays
 594            info = sbc.linux.Light.get_display_info()
 595            # EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]
 596
 597            # get info about the primary display
 598            primary_info = sbc.linux.Light.get_display_info(0)[0]
 599
 600            # get info about a display called 'edp-backlight'
 601            edp_info = sbc.linux.Light.get_display_info('edp-backlight')[0]
 602            ```
 603        '''
 604        light_output = check_output([cls.executable, '-L']).decode()
 605        displays = []
 606        index = 0
 607        for device in SysFiles.get_display_info():
 608            # SysFiles scrapes info from the same place that Light used to
 609            # so it makes sense to use that output
 610            if device['path'].replace('/sys/class', 'sysfs') in light_output:
 611                del device['scale']
 612                device['light_path'] = device['path'].replace('/sys/class', 'sysfs')
 613                device['method'] = cls
 614                device['index'] = index
 615
 616                displays.append(device)
 617                index += 1
 618
 619        if display is not None:
 620            displays = filter_monitors(display=display, haystack=displays, include=['path', 'light_path'])
 621        return displays
 622
 623    @classmethod
 624    def set_brightness(cls, value: int, display: Optional[int] = None):
 625        '''
 626        Sets the brightness for a display using the light executable
 627
 628        Args:
 629            value (int): Sets the brightness to this value
 630            display (int): The specific display you wish to query.
 631
 632        Example:
 633            ```python
 634            import screen_brightness_control as sbc
 635
 636            # set the brightness to 50%
 637            sbc.linux.Light.set_brightness(50)
 638
 639            # set the primary display brightness to 75%
 640            sbc.linux.Light.set_brightness(75, display = 0)
 641
 642            # set the secondary display brightness to 25%
 643            sbc.linux.Light.set_brightness(25, display = 1)
 644            ```
 645        '''
 646        info = cls.get_display_info()
 647        if display is not None:
 648            info = [info[display]]
 649
 650        for i in info:
 651            check_output(f'{cls.executable} -S {value} -s {i["light_path"]}'.split(" "))
 652
 653    @classmethod
 654    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
 655        '''
 656        Gets the brightness for a display using the light executable
 657
 658        Args:
 659            display (int): The specific display you wish to query.
 660
 661        Returns:
 662            list: list of ints (0 to 100)
 663
 664        Example:
 665            ```python
 666            import screen_brightness_control as sbc
 667
 668            # get the current display brightness
 669            current_brightness = sbc.linux.Light.get_brightness()
 670
 671            # get the brightness of the primary display
 672            primary_brightness = sbc.linux.Light.get_brightness(display = 0)[0]
 673
 674            # get the brightness of the secondary display
 675            edp_brightness = sbc.linux.Light.get_brightness(display = 1)[0]
 676            ```
 677        '''
 678        info = cls.get_display_info()
 679        if display is not None:
 680            info = [info[display]]
 681
 682        results = []
 683        for i in info:
 684            results.append(
 685                check_output([cls.executable, '-G', '-s', i['light_path']])
 686            )
 687        results = [int(round(float(i.decode()), 0)) for i in results]
 688        return results
 689
 690
 691class XRandr(BrightnessMethodAdv):
 692    '''collection of screen brightness related methods using the xrandr executable'''
 693
 694    executable: str = 'xrandr'
 695    '''the xrandr executable to be called'''
 696
 697    @classmethod
 698    def _gdi(cls):
 699        '''
 700        .. warning:: Don't use this
 701           This function isn't final and I will probably make breaking changes to it.
 702           You have been warned
 703
 704        Gets all displays reported by XRandr even if they're not supported
 705        '''
 706        xrandr_output = check_output([cls.executable, '--verbose']).decode().split('\n')
 707
 708        display_count = 0
 709        tmp_display = {}
 710
 711        for line_index, line in enumerate(xrandr_output):
 712            if line == '':
 713                continue
 714
 715            if not line.startswith((' ', '\t')) and 'connected' in line and 'disconnected' not in line:
 716                if tmp_display:
 717                    yield tmp_display
 718
 719                tmp_display = {
 720                    'name': line.split(' ')[0],
 721                    'interface': line.split(' ')[0],
 722                    'method': cls,
 723                    'index': display_count,
 724                    'model': None,
 725                    'serial': None,
 726                    'manufacturer': None,
 727                    'manufacturer_id': None,
 728                    'edid': None,
 729                    'unsupported': line.startswith('XWAYLAND')
 730                }
 731                display_count += 1
 732
 733            elif 'EDID:' in line:
 734                # extract the edid from the chunk of the output that will contain the edid
 735                edid = ''.join(
 736                    i.replace('\t', '') for i in xrandr_output[line_index + 1: line_index + 9]
 737                )
 738                tmp_display['edid'] = edid
 739
 740                for key, value in zip(
 741                    ('manufacturer_id', 'manufacturer', 'model', 'name', 'serial'),
 742                    EDID.parse(tmp_display['edid'])
 743                ):
 744                    if value is None:
 745                        continue
 746                    tmp_display[key] = value
 747
 748            elif 'Brightness:' in line:
 749                tmp_display['brightness'] = int(float(line.replace('Brightness:', '')) * 100)
 750
 751        if tmp_display:
 752            yield tmp_display
 753
 754    @classmethod
 755    def get_display_info(cls, display: Optional[Union[int, str]] = None, brightness: bool = False) -> List[dict]:
 756        '''
 757        Returns info about all detected displays as reported by xrandr
 758
 759        Args:
 760            display (str or int): The display to return info about.
 761                Pass in the serial number, name, model, interface, edid or index.
 762                This is passed to `filter_monitors`
 763            brightness (bool): whether to include the current brightness
 764                in the returned info
 765
 766        Returns:
 767            list: list of dicts
 768
 769        Example:
 770            ```python
 771            import screen_brightness_control as sbc
 772
 773            info = sbc.linux.XRandr.get_display_info()
 774            for i in info:
 775                print('================')
 776                for key, value in i.items():
 777                    print(key, ':', value)
 778
 779            # get information about the first XRandr addressable display
 780            primary_info = sbc.linux.XRandr.get_display_info(0)[0]
 781
 782            # get information about a display with a specific name
 783            benq_info = sbc.linux.XRandr.get_display_info('BenQ GL2450HM')[0]
 784            ```
 785        '''
 786        valid_displays = []
 787        for item in cls._gdi():
 788            if item['unsupported']:
 789                continue
 790            if not brightness:
 791                del item['brightness']
 792            del item['unsupported']
 793            valid_displays.append(item)
 794        if display is not None:
 795            valid_displays = filter_monitors(display=display, haystack=valid_displays, include=['interface'])
 796        return valid_displays
 797
 798    @classmethod
 799    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
 800        '''
 801        Returns the brightness for a display using the xrandr executable
 802
 803        Args:
 804            display (int): The specific display you wish to query.
 805
 806        Returns:
 807            list: list of integers (from 0 to 100)
 808
 809        Example:
 810            ```python
 811            import screen_brightness_control as sbc
 812
 813            # get the current brightness
 814            current_brightness = sbc.linux.XRandr.get_brightness()
 815
 816            # get the current brightness for the primary display
 817            primary_brightness = sbc.linux.XRandr.get_brightness(display=0)[0]
 818            ```
 819        '''
 820        monitors = cls.get_display_info(brightness=True)
 821        if display is not None:
 822            monitors = [monitors[display]]
 823        brightness = [i['brightness'] for i in monitors]
 824
 825        return brightness
 826
 827    @classmethod
 828    def set_brightness(cls, value: int, display: Optional[int] = None):
 829        '''
 830        Sets the brightness for a display using the xrandr executable
 831
 832        Args:
 833            value (int): Sets the brightness to this value
 834            display (int): The specific display you wish to query.
 835
 836        Example:
 837            ```python
 838            import screen_brightness_control as sbc
 839
 840            # set the brightness to 50
 841            sbc.linux.XRandr.set_brightness(50)
 842
 843            # set the brightness of the primary display to 75
 844            sbc.linux.XRandr.set_brightness(75, display=0)
 845            ```
 846        '''
 847        value = str(float(value) / 100)
 848        info = cls.get_display_info()
 849        if display is not None:
 850            info = [info[display]]
 851
 852        for i in info:
 853            check_output([cls.executable, '--output', i['interface'], '--brightness', value])
 854
 855
 856class DDCUtil(BrightnessMethodAdv):
 857    '''collection of screen brightness related methods using the ddcutil executable'''
 858    logger = logger.getChild('DDCUtil')
 859
 860    executable: str = 'ddcutil'
 861    '''the ddcutil executable to be called'''
 862    sleep_multiplier: float = 0.5
 863    '''how long ddcutil should sleep between each DDC request (lower is shorter).
 864    See [the ddcutil docs](https://www.ddcutil.com/performance_options/) for more info.'''
 865    cmd_max_tries: int = 10
 866    '''max number of retries when calling the ddcutil'''
 867    _max_brightness_cache: dict = {}
 868    '''Cache for displays and their maximum brightness values'''
 869
 870    @classmethod
 871    def _gdi(cls):
 872        '''
 873        .. warning:: Don't use this
 874           This function isn't final and I will probably make breaking changes to it.
 875           You have been warned
 876
 877        Gets all displays reported by DDCUtil even if they're not supported
 878        '''
 879        raw_ddcutil_output = str(
 880            check_output(
 881                [
 882                    cls.executable, 'detect', '-v', '--async',
 883                    f'--sleep-multiplier={cls.sleep_multiplier}'
 884                ], max_tries=cls.cmd_max_tries
 885            )
 886        )[2:-1].split('\\n')
 887        # Use -v to get EDID string but this means output cannot be decoded.
 888        # Or maybe it can. I don't know the encoding though, so let's assume it cannot be decoded.
 889        # Use str()[2:-1] workaround
 890
 891        # include "Invalid display" sections because they tell us where one displays metadata ends
 892        # and another begins. We filter out invalid displays later on
 893        ddcutil_output = [i for i in raw_ddcutil_output if i.startswith(('Invalid display', 'Display', '\t', ' '))]
 894        tmp_display = {}
 895        display_count = 0
 896
 897        for line_index, line in enumerate(ddcutil_output):
 898            if not line.startswith(('\t', ' ')):
 899                if tmp_display:
 900                    yield tmp_display
 901
 902                tmp_display = {
 903                    'method': cls,
 904                    'index': display_count,
 905                    'model': None,
 906                    'serial': None,
 907                    'bin_serial': None,
 908                    'manufacturer': None,
 909                    'manufacturer_id': None,
 910                    'edid': None,
 911                    'unsupported': 'invalid display' in line.lower()
 912                }
 913                display_count += 1
 914
 915            elif 'I2C bus' in line:
 916                tmp_display['i2c_bus'] = line[line.index('/'):]
 917                tmp_display['bus_number'] = int(tmp_display['i2c_bus'].replace('/dev/i2c-', ''))
 918
 919            elif 'Mfg id' in line:
 920                # Recently ddcutil has started reporting manufacturer IDs like
 921                # 'BNQ - UNK' or 'MSI - Microstep' so we have to split the line
 922                # into chunks of alpha chars and check for a valid mfg id
 923                for code in re.split(r'[^A-Za-z]', line.replace('Mfg id:', '').replace(' ', '')):
 924                    if len(code) != 3:
 925                        # all mfg ids are 3 chars long
 926                        continue
 927
 928                    try:
 929                        (
 930                            tmp_display['manufacturer_id'],
 931                            tmp_display['manufacturer']
 932                        ) = _monitor_brand_lookup(code)
 933                    except TypeError:
 934                        continue
 935                    else:
 936                        break
 937
 938            elif 'Model' in line:
 939                # the split() removes extra spaces
 940                name = line.replace('Model:', '').split()
 941                try:
 942                    tmp_display['model'] = name[1]
 943                except IndexError:
 944                    pass
 945                tmp_display['name'] = ' '.join(name)
 946
 947            elif 'Serial number' in line:
 948                tmp_display['serial'] = line.replace('Serial number:', '').replace(' ', '') or None
 949
 950            elif 'Binary serial number:' in line:
 951                tmp_display['bin_serial'] = line.split(' ')[-1][3:-1]
 952
 953            elif 'EDID hex dump:' in line:
 954                try:
 955                    tmp_display['edid'] = ''.join(
 956                        ''.join(i.split()[1:17]) for i in ddcutil_output[line_index + 2: line_index + 10]
 957                    )
 958                except Exception:
 959                    pass
 960
 961        if tmp_display:
 962            yield tmp_display
 963
 964    @classmethod
 965    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
 966        '''
 967        Returns information about all DDC compatible displays shown by DDCUtil
 968        Works by calling the command 'ddcutil detect' and parsing the output.
 969
 970        Args:
 971            display (int or str): The display to return info about.
 972                Pass in the serial number, name, model, i2c bus, edid or index.
 973                This is passed to `filter_monitors`
 974
 975        Returns:
 976            list: list of dicts
 977
 978        Example:
 979            ```python
 980            import screen_brightness_control as sbc
 981
 982            info = sbc.linux.DDCUtil.get_display_info()
 983            for i in info:
 984                print('================')
 985                for key, value in i.items():
 986                    print(key, ':', value)
 987
 988            # get information about the first DDCUtil addressable display
 989            primary_info = sbc.linux.DDCUtil.get_display_info(0)[0]
 990
 991            # get information about a display with a specific name
 992            benq_info = sbc.linux.DDCUtil.get_display_info('BenQ GL2450HM')[0]
 993            ```
 994        '''
 995        valid_displays = __cache__.get('ddcutil_monitors_info')
 996        if valid_displays is None:
 997            valid_displays = []
 998            for item in cls._gdi():
 999                if item['unsupported']:
1000                    continue
1001                del item['unsupported']
1002                valid_displays.append(item)
1003
1004            if valid_displays:
1005                __cache__.store('ddcutil_monitors_info', valid_displays)
1006
1007        if display is not None:
1008            valid_displays = filter_monitors(display=display, haystack=valid_displays, include=['i2c_bus'])
1009        return valid_displays
1010
1011    @classmethod
1012    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
1013        '''
1014        Returns the brightness for a display using the ddcutil executable
1015
1016        Args:
1017            display (int): The specific display you wish to query.
1018
1019        Returns:
1020            list: list of ints (0 to 100)
1021
1022        Example:
1023            ```python
1024            import screen_brightness_control as sbc
1025
1026            # get the current brightness
1027            current_brightness = sbc.linux.DDCUtil.get_brightness()
1028
1029            # get the current brightness for the primary display
1030            primary_brightness = sbc.linux.DDCUtil.get_brightness(display=0)[0]
1031            ```
1032        '''
1033        monitors = cls.get_display_info()
1034        if display is not None:
1035            monitors = [monitors[display]]
1036
1037        res = []
1038        for monitor in monitors:
1039            value = __cache__.get(f'ddcutil_brightness_{monitor["index"]}')
1040            if value is None:
1041                cmd_out = check_output(
1042                    [
1043                        cls.executable,
1044                        'getvcp', '10', '-t',
1045                        '-b', str(monitor['bus_number']),
1046                        f'--sleep-multiplier={cls.sleep_multiplier}'
1047                    ], max_tries=cls.cmd_max_tries
1048                ).decode().split(' ')
1049
1050                value = int(cmd_out[-2])
1051                max_value = int(cmd_out[-1])
1052                if max_value != 100:
1053                    # if the max brightness is not 100 then the number is not a percentage
1054                    # and will need to be scaled
1055                    value = int((value / max_value) * 100)
1056
1057                # now make sure max brightness is recorded so set_brightness can use it
1058                cache_ident = '%s-%s-%s' % (monitor['name'], monitor['serial'], monitor['bin_serial'])
1059                if cache_ident not in cls._max_brightness_cache:
1060                    cls._max_brightness_cache[cache_ident] = max_value
1061                    cls.logger.debug(f'{cache_ident} max brightness:{max_value} (current: {value})')
1062
1063                __cache__.store(f'ddcutil_brightness_{monitor["index"]}', value, expires=0.5)
1064            res.append(value)
1065        return res
1066
1067    @classmethod
1068    def set_brightness(cls, value: int, display: Optional[int] = None):
1069        '''
1070        Sets the brightness for a display using the ddcutil executable
1071
1072        Args:
1073            value (int): Sets the brightness to this value
1074            display (int): The specific display you wish to query.
1075
1076        Example:
1077            ```python
1078            import screen_brightness_control as sbc
1079
1080            # set the brightness to 50
1081            sbc.linux.DDCUtil.set_brightness(50)
1082
1083            # set the brightness of the primary display to 75
1084            sbc.linux.DDCUtil.set_brightness(75, display=0)
1085            ```
1086        '''
1087        monitors = cls.get_display_info()
1088        if display is not None:
1089            monitors = [monitors[display]]
1090
1091        __cache__.expire(startswith='ddcutil_brightness_')
1092        for monitor in monitors:
1093            # check if monitor has a max brightness that requires us to scale this value
1094            cache_ident = '%s-%s-%s' % (monitor['name'], monitor['serial'], monitor['bin_serial'])
1095            if cache_ident not in cls._max_brightness_cache:
1096                cls.get_brightness(display=monitor['index'])
1097
1098            if cls._max_brightness_cache[cache_ident] != 100:
1099                value = int((value / 100) * cls._max_brightness_cache[cache_ident])
1100
1101            check_output(
1102                [
1103                    cls.executable, 'setvcp', '10', str(value),
1104                    '-b', str(monitor['bus_number']),
1105                    f'--sleep-multiplier={cls.sleep_multiplier}'
1106                ], max_tries=cls.cmd_max_tries
1107            )
1108
1109
1110def list_monitors_info(
1111    method: Optional[str] = None, allow_duplicates: bool = False, unsupported: bool = False
1112) -> List[dict]:
1113    '''
1114    Lists detailed information about all detected displays
1115
1116    Args:
1117        method (str): the method the display can be addressed by. See `screen_brightness_control.get_methods`
1118            for more info on available methods
1119        allow_duplicates (bool): whether to filter out duplicate displays (displays with the same EDID) or not
1120        unsupported (bool): include detected displays that are invalid or unsupported
1121
1122    Returns:
1123        list: list of dicts
1124
1125    Example:
1126        ```python
1127        import screen_brightness_control as sbc
1128
1129        displays = sbc.linux.list_monitors_info()
1130        for display in displays:
1131            print('=======================')
1132            # the manufacturer name plus the model OR a generic name for the display, depending on the method
1133            print('Name:', display['name'])
1134            # the general model of the display
1135            print('Model:', display['model'])
1136            # the serial of the display
1137            print('Serial:', display['serial'])
1138            # the name of the brand of the display
1139            print('Manufacturer:', display['manufacturer'])
1140            # the 3 letter code corresponding to the brand name, EG: BNQ -> BenQ
1141            print('Manufacturer ID:', display['manufacturer_id'])
1142            # the index of that display FOR THE SPECIFIC METHOD THE DISPLAY USES
1143            print('Index:', display['index'])
1144            # the method this display can be addressed by
1145            print('Method:', display['method'])
1146        ```
1147    '''
1148    all_methods = get_methods(method).values()
1149    haystack = []
1150    for method_class in all_methods:
1151        try:
1152            if unsupported and issubclass(method_class, BrightnessMethodAdv):
1153                haystack += method_class._gdi()
1154            else:
1155                haystack += method_class.get_display_info()
1156        except Exception as e:
1157            logger.warning(f'error grabbing display info from {method_class} - {format_exc(e)}')
1158            pass
1159
1160    if allow_duplicates:
1161        return haystack
1162
1163    try:
1164        # use filter_monitors to remove duplicates
1165        return filter_monitors(haystack=haystack)
1166    except NoValidDisplayError:
1167        return []

 21class SysFiles(BrightnessMethod):
 22    '''
 23    A way of getting display information and adjusting the brightness
 24    that does not rely on any 3rd party software.
 25
 26    This class works with displays that show up in the `/sys/class/backlight`
 27    directory (so usually laptop displays).
 28
 29    To set the brightness, your user will need write permissions for
 30    `/sys/class/backlight/*/brightness` or you will need to run the program
 31    as root.
 32    '''
 33    logger = logger.getChild('SysFiles')
 34
 35    @classmethod
 36    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
 37        '''
 38        Returns information about detected displays by reading files from the
 39        `/sys/class/backlight` directory
 40
 41        Args:
 42            display (str or int): The display to return info about.
 43                Pass in the serial number, name, model, interface, edid or index.
 44                This is passed to `filter_monitors`
 45
 46        Returns:
 47            list: list of dicts
 48
 49        Example:
 50            ```python
 51            import screen_brightness_control as sbc
 52
 53            # get info about all displays
 54            info = sbc.linux.SysFiles.get_display_info()
 55            # EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]
 56
 57            # get info about the primary display
 58            primary_info = sbc.linux.SysFiles.get_display_info(0)[0]
 59
 60            # get info about a display called 'edp-backlight'
 61            edp_info = sbc.linux.SysFiles.get_display_info('edp-backlight')[0]
 62            ```
 63        '''
 64        subsystems = set()
 65        for folder in os.listdir('/sys/class/backlight'):
 66            if os.path.isdir(f'/sys/class/backlight/{folder}/subsystem'):
 67                subsystems.add(tuple(os.listdir(f'/sys/class/backlight/{folder}/subsystem')))
 68
 69        all_displays = {}
 70        index = 0
 71
 72        for subsystem in subsystems:
 73
 74            device = {
 75                'name': subsystem[0],
 76                'path': f'/sys/class/backlight/{subsystem[0]}',
 77                'method': cls,
 78                'index': index,
 79                'model': None,
 80                'serial': None,
 81                'manufacturer': None,
 82                'manufacturer_id': None,
 83                'edid': None,
 84                'scale': None
 85            }
 86
 87            for folder in subsystem:
 88                # subsystems like intel_backlight usually have an acpi_video0
 89                # counterpart, which we don't want so lets find the 'best' candidate
 90                try:
 91                    with open(os.path.join(f'/sys/class/backlight/{folder}/max_brightness')) as f:
 92                        scale = int(f.read().rstrip(' \n')) / 100
 93
 94                    # use the display with the highest resolution scale
 95                    if device['scale'] is None or scale > device['scale']:
 96                        device['name'] = folder
 97                        device['path'] = f'/sys/class/backlight/{folder}'
 98                        device['scale'] = scale
 99                except (FileNotFoundError, TypeError) as e:
100                    cls.logger.error(
101                        f'error getting highest resolution scale for {folder}'
102                        f' - {format_exc(e)}'
103                    )
104                    continue
105
106            if os.path.isfile('%s/device/edid' % device['path']):
107                device['edid'] = EDID.hexdump('%s/device/edid' % device['path'])
108
109                for key, value in zip(
110                    ('manufacturer_id', 'manufacturer', 'model', 'name', 'serial'),
111                    EDID.parse(device['edid'])
112                ):
113                    if value is None:
114                        continue
115                    device[key] = value
116
117            all_displays[device['edid']] = device
118            index += 1
119
120        all_displays = list(all_displays.values())
121        if display is not None:
122            all_displays = filter_monitors(display=display, haystack=all_displays, include=['path'])
123        return all_displays
124
125    @classmethod
126    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
127        '''
128        Gets the brightness for a display by reading the brightness files
129        stored in `/sys/class/backlight/*/brightness`
130
131        Args:
132            display (int): The specific display you wish to query.
133
134        Returns:
135            list: list of ints (0 to 100)
136
137        Example:
138            ```python
139            import screen_brightness_control as sbc
140
141            # get the current display brightness
142            current_brightness = sbc.linux.SysFiles.get_brightness()
143
144            # get the brightness of the primary display
145            primary_brightness = sbc.linux.SysFiles.get_brightness(display = 0)[0]
146
147            # get the brightness of the secondary display
148            secondary_brightness = sbc.linux.SysFiles.get_brightness(display = 1)[0]
149            ```
150        '''
151        info = cls.get_display_info()
152        if display is not None:
153            info = [info[display]]
154
155        results = []
156        for device in info:
157            with open(os.path.join(device['path'], 'brightness'), 'r') as f:
158                brightness = int(f.read().rstrip('\n'))
159            results.append(int(brightness / device['scale']))
160
161        return results
162
163    @classmethod
164    def set_brightness(cls, value: int, display: Optional[int] = None):
165        '''
166        Sets the brightness for a display by writing to the brightness files
167        stored in `/sys/class/backlight/*/brightness`.
168        This function requires permission to write to these files which is
169        usually provided when it's run as root.
170
171        Args:
172            value (int): Sets the brightness to this value
173            display (int): The specific display you wish to adjust.
174
175        Example:
176            ```python
177            import screen_brightness_control as sbc
178
179            # set the brightness to 50%
180            sbc.linux.SysFiles.set_brightness(50)
181
182            # set the primary display brightness to 75%
183            sbc.linux.SysFiles.set_brightness(75, display = 0)
184
185            # set the secondary display brightness to 25%
186            sbc.linux.SysFiles.set_brightness(25, display = 1)
187            ```
188        '''
189        info = cls.get_display_info()
190        if display is not None:
191            info = [info[display]]
192
193        for device in info:
194            with open(os.path.join(device['path'], 'brightness'), 'w') as f:
195                f.write(str(int(value * device['scale'])))

A way of getting display information and adjusting the brightness that does not rely on any 3rd party software.

This class works with displays that show up in the /sys/class/backlight directory (so usually laptop displays).

To set the brightness, your user will need write permissions for /sys/class/backlight/*/brightness or you will need to run the program as root.

@classmethod
def get_display_info(cls, display: Union[str, int, NoneType] = None) -> List[dict]:
 35    @classmethod
 36    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
 37        '''
 38        Returns information about detected displays by reading files from the
 39        `/sys/class/backlight` directory
 40
 41        Args:
 42            display (str or int): The display to return info about.
 43                Pass in the serial number, name, model, interface, edid or index.
 44                This is passed to `filter_monitors`
 45
 46        Returns:
 47            list: list of dicts
 48
 49        Example:
 50            ```python
 51            import screen_brightness_control as sbc
 52
 53            # get info about all displays
 54            info = sbc.linux.SysFiles.get_display_info()
 55            # EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]
 56
 57            # get info about the primary display
 58            primary_info = sbc.linux.SysFiles.get_display_info(0)[0]
 59
 60            # get info about a display called 'edp-backlight'
 61            edp_info = sbc.linux.SysFiles.get_display_info('edp-backlight')[0]
 62            ```
 63        '''
 64        subsystems = set()
 65        for folder in os.listdir('/sys/class/backlight'):
 66            if os.path.isdir(f'/sys/class/backlight/{folder}/subsystem'):
 67                subsystems.add(tuple(os.listdir(f'/sys/class/backlight/{folder}/subsystem')))
 68
 69        all_displays = {}
 70        index = 0
 71
 72        for subsystem in subsystems:
 73
 74            device = {
 75                'name': subsystem[0],
 76                'path': f'/sys/class/backlight/{subsystem[0]}',
 77                'method': cls,
 78                'index': index,
 79                'model': None,
 80                'serial': None,
 81                'manufacturer': None,
 82                'manufacturer_id': None,
 83                'edid': None,
 84                'scale': None
 85            }
 86
 87            for folder in subsystem:
 88                # subsystems like intel_backlight usually have an acpi_video0
 89                # counterpart, which we don't want so lets find the 'best' candidate
 90                try:
 91                    with open(os.path.join(f'/sys/class/backlight/{folder}/max_brightness')) as f:
 92                        scale = int(f.read().rstrip(' \n')) / 100
 93
 94                    # use the display with the highest resolution scale
 95                    if device['scale'] is None or scale > device['scale']:
 96                        device['name'] = folder
 97                        device['path'] = f'/sys/class/backlight/{folder}'
 98                        device['scale'] = scale
 99                except (FileNotFoundError, TypeError) as e:
100                    cls.logger.error(
101                        f'error getting highest resolution scale for {folder}'
102                        f' - {format_exc(e)}'
103                    )
104                    continue
105
106            if os.path.isfile('%s/device/edid' % device['path']):
107                device['edid'] = EDID.hexdump('%s/device/edid' % device['path'])
108
109                for key, value in zip(
110                    ('manufacturer_id', 'manufacturer', 'model', 'name', 'serial'),
111                    EDID.parse(device['edid'])
112                ):
113                    if value is None:
114                        continue
115                    device[key] = value
116
117            all_displays[device['edid']] = device
118            index += 1
119
120        all_displays = list(all_displays.values())
121        if display is not None:
122            all_displays = filter_monitors(display=display, haystack=all_displays, include=['path'])
123        return all_displays

Returns information about detected displays by reading files from the /sys/class/backlight directory

Arguments:
  • display (str or int): The display to return info about. Pass in the serial number, name, model, interface, edid or index. This is passed to filter_monitors
Returns:
  • list: list of dicts
Example:
import screen_brightness_control as sbc

# get info about all displays
info = sbc.linux.SysFiles.get_display_info()
# EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]

# get info about the primary display
primary_info = sbc.linux.SysFiles.get_display_info(0)[0]

# get info about a display called 'edp-backlight'
edp_info = sbc.linux.SysFiles.get_display_info('edp-backlight')[0]
@classmethod
def get_brightness(cls, display: Optional[int] = None) -> List[int]:
125    @classmethod
126    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
127        '''
128        Gets the brightness for a display by reading the brightness files
129        stored in `/sys/class/backlight/*/brightness`
130
131        Args:
132            display (int): The specific display you wish to query.
133
134        Returns:
135            list: list of ints (0 to 100)
136
137        Example:
138            ```python
139            import screen_brightness_control as sbc
140
141            # get the current display brightness
142            current_brightness = sbc.linux.SysFiles.get_brightness()
143
144            # get the brightness of the primary display
145            primary_brightness = sbc.linux.SysFiles.get_brightness(display = 0)[0]
146
147            # get the brightness of the secondary display
148            secondary_brightness = sbc.linux.SysFiles.get_brightness(display = 1)[0]
149            ```
150        '''
151        info = cls.get_display_info()
152        if display is not None:
153            info = [info[display]]
154
155        results = []
156        for device in info:
157            with open(os.path.join(device['path'], 'brightness'), 'r') as f:
158                brightness = int(f.read().rstrip('\n'))
159            results.append(int(brightness / device['scale']))
160
161        return results

Gets the brightness for a display by reading the brightness files stored in /sys/class/backlight/*/brightness

Arguments:
  • display (int): The specific display you wish to query.
Returns:
  • list: list of ints (0 to 100)
Example:
import screen_brightness_control as sbc

# get the current display brightness
current_brightness = sbc.linux.SysFiles.get_brightness()

# get the brightness of the primary display
primary_brightness = sbc.linux.SysFiles.get_brightness(display = 0)[0]

# get the brightness of the secondary display
secondary_brightness = sbc.linux.SysFiles.get_brightness(display = 1)[0]
@classmethod
def set_brightness(cls, value: int, display: Optional[int] = None):
163    @classmethod
164    def set_brightness(cls, value: int, display: Optional[int] = None):
165        '''
166        Sets the brightness for a display by writing to the brightness files
167        stored in `/sys/class/backlight/*/brightness`.
168        This function requires permission to write to these files which is
169        usually provided when it's run as root.
170
171        Args:
172            value (int): Sets the brightness to this value
173            display (int): The specific display you wish to adjust.
174
175        Example:
176            ```python
177            import screen_brightness_control as sbc
178
179            # set the brightness to 50%
180            sbc.linux.SysFiles.set_brightness(50)
181
182            # set the primary display brightness to 75%
183            sbc.linux.SysFiles.set_brightness(75, display = 0)
184
185            # set the secondary display brightness to 25%
186            sbc.linux.SysFiles.set_brightness(25, display = 1)
187            ```
188        '''
189        info = cls.get_display_info()
190        if display is not None:
191            info = [info[display]]
192
193        for device in info:
194            with open(os.path.join(device['path'], 'brightness'), 'w') as f:
195                f.write(str(int(value * device['scale'])))

Sets the brightness for a display by writing to the brightness files stored in /sys/class/backlight/*/brightness. This function requires permission to write to these files which is usually provided when it's run as root.

Arguments:
  • value (int): Sets the brightness to this value
  • display (int): The specific display you wish to adjust.
Example:
import screen_brightness_control as sbc

# set the brightness to 50%
sbc.linux.SysFiles.set_brightness(50)

# set the primary display brightness to 75%
sbc.linux.SysFiles.set_brightness(75, display = 0)

# set the secondary display brightness to 25%
sbc.linux.SysFiles.set_brightness(25, display = 1)
198class I2C(BrightnessMethod):
199    '''
200    In the same spirit as `SysFiles`, this class serves as a way of getting
201    display information and adjusting the brightness without relying on any
202    3rd party software.
203
204    Usage of this class requires read and write permission for `/dev/i2c-*`.
205
206    This class works over the I2C bus, primarily with desktop monitors as I
207    haven't tested any e-DP displays yet.
208
209    Massive thanks to [siemer](https://github.com/siemer) for
210    his work on the [ddcci.py](https://github.com/siemer/ddcci) project,
211    which served as a my main reference for this.
212
213    References:
214        * [ddcci.py](https://github.com/siemer/ddcci)
215        * [DDCCI Spec](https://milek7.pl/ddcbacklight/ddcci.pdf)
216    '''
217    logger = logger.getChild('I2C')
218
219    # vcp commands
220    GET_VCP_CMD = 0x01
221    '''VCP command to get the value of a feature (eg: brightness)'''
222    GET_VCP_REPLY = 0x02
223    '''VCP feature reply op code'''
224    SET_VCP_CMD = 0x03
225    '''VCP command to set the value of a feature (eg: brightness)'''
226
227    # addresses
228    DDCCI_ADDR = 0x37
229    '''DDC packets are transmittred using this I2C address'''
230    HOST_ADDR_R = 0x50
231    '''Packet source address (the computer) when reading data'''
232    HOST_ADDR_W = 0x51
233    '''Packet source address (the computer) when writing data'''
234    DESTINATION_ADDR_W = 0x6e
235    '''Packet destination address (the monitor) when writing data'''
236    I2C_SLAVE = 0x0703
237    '''The I2C slave address'''
238
239    # timings
240    WAIT_TIME = 0.05
241    '''How long to wait between I2C commands'''
242
243    _max_brightness_cache: dict = {}
244
245    class I2CDevice():
246        '''
247        Class to read and write data to an I2C bus,
248        based on the `I2CDev` class from [ddcci.py](https://github.com/siemer/ddcci)
249        '''
250        def __init__(self, fname: str, slave_addr: int):
251            '''
252            Args:
253                fname (str): the I2C path, eg: `/dev/i2c-2`
254                slave_addr (int): not entirely sure what this is meant to be
255            '''
256            self.device = os.open(fname, os.O_RDWR)
257            # I2C_SLAVE address setup
258            fcntl.ioctl(self.device, I2C.I2C_SLAVE, slave_addr)
259
260        def read(self, length: int) -> bytes:
261            '''
262            Read a certain number of bytes from the I2C bus
263
264            Args:
265                length (int): the number of bytes to read
266
267            Returns:
268                bytes
269            '''
270            return os.read(self.device, length)
271
272        def write(self, data: bytes) -> int:
273            '''
274            Writes data to the I2C bus
275
276            Args:
277                data (bytes): the data to write
278
279            Returns:
280                int: the number of bytes written
281            '''
282            return os.write(self.device, data)
283
284    class DDCInterface(I2CDevice):
285        '''
286        Class to send DDC (Display Data Channel) commands to an I2C device,
287        based on the `Ddcci` and `Mccs` classes from [ddcci.py](https://github.com/siemer/ddcci)
288        '''
289
290        PROTOCOL_FLAG = 0x80
291
292        def __init__(self, i2c_path: str):
293            '''
294            Args:
295                i2c_path (str): the path to the I2C device, eg: `/dev/i2c-2`
296            '''
297            self.logger = logger.getChild(self.__class__.__name__).getChild(i2c_path)
298            super().__init__(i2c_path, I2C.DDCCI_ADDR)
299
300        def write(self, *args) -> int:
301            '''
302            Write some data to the I2C device.
303
304            It is recommended to use `setvcp` to set VCP values on the DDC device
305            instead of using this function directly.
306
307            Args:
308                *args: variable length list of arguments. This will be put
309                    into a `bytearray` and wrapped up in various flags and
310                    checksums before being written to the I2C device
311
312            Returns:
313                int: the number of bytes that were written
314            '''
315            time.sleep(I2C.WAIT_TIME)
316
317            ba = bytearray(args)
318            ba.insert(0, len(ba) | self.PROTOCOL_FLAG)  # add length info
319            ba.insert(0, I2C.HOST_ADDR_W)  # insert source address
320            ba.append(functools.reduce(operator.xor, ba, I2C.DESTINATION_ADDR_W))  # checksum
321
322            return super().write(ba)
323
324        def setvcp(self, vcp_code: int, value: int) -> int:
325            '''
326            Set a VCP value on the device
327
328            Args:
329                vcp_code (int): the VCP command to send, eg: `0x10` is brightness
330                value (int): what to set the value to
331
332            Returns:
333                int: the number of bytes written to the device
334            '''
335            return self.write(I2C.SET_VCP_CMD, vcp_code, *value.to_bytes(2, 'big'))
336
337        def read(self, amount: int) -> bytes:
338            '''
339            Reads data from the DDC device.
340
341            It is recommended to use `getvcp` to retrieve VCP values from the
342            DDC device instead of using this function directly.
343
344            Args:
345                amount (int): the number of bytes to read
346
347            Returns:
348                bytes
349
350            Raises:
351                ValueError: if the read data is deemed invalid
352            '''
353            time.sleep(I2C.WAIT_TIME)
354
355            ba = super().read(amount + 3)
356
357            # check the bytes read
358            checks = {
359                'source address': ba[0] == I2C.DESTINATION_ADDR_W,
360                'checksum': functools.reduce(operator.xor, ba) == I2C.HOST_ADDR_R,
361                'length': len(ba) >= (ba[1] & ~self.PROTOCOL_FLAG) + 3
362            }
363            if False in checks.values():
364                self.logger.error('i2c read check failed: ' + repr(checks))
365                raise I2CValidationError('i2c read check failed: ' + repr(checks))
366
367            return ba[2:-1]
368
369        def getvcp(self, vcp_code: int) -> Tuple[int, int]:
370            '''
371            Retrieves a VCP value from the DDC device.
372
373            Args:
374                vcp_code (int): the VCP value to read, eg: `0x10` is brightness
375
376            Returns:
377                tuple[int, int]: the current and maximum value respectively
378
379            Raises:
380                ValueError: if the read data is deemed invalid
381            '''
382            self.write(I2C.GET_VCP_CMD, vcp_code)
383            ba = self.read(8)
384
385            checks = {
386                'is feature reply': ba[0] == I2C.GET_VCP_REPLY,
387                'supported VCP opcode': ba[1] == 0,
388                'answer matches request': ba[2] == vcp_code
389            }
390            if False in checks.values():
391                self.logger.error('i2c read check failed: ' + repr(checks))
392                raise I2CValidationError('i2c read check failed: ' + repr(checks))
393
394            # current and max values
395            return int.from_bytes(ba[6:8], 'big'), int.from_bytes(ba[4:6], 'big')
396
397    @classmethod
398    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
399        '''
400        Returns information about detected displays by querying the various I2C buses
401
402        Args:
403            display (str or int): The display to return info about.
404                Pass in the serial number, name, model, interface, edid or index.
405                This is passed to `filter_monitors`
406
407        Returns:
408            list: list of dicts
409
410        Example:
411            ```python
412            import screen_brightness_control as sbc
413
414            # get info about all displays
415            info = sbc.linux.I2C.get_display_info()
416            # EG output: [{'name': 'Benq GL2450H', 'model': 'GL2450H', 'manufacturer': 'BenQ', 'edid': '00ffff...'}]
417
418            # get info about the primary display
419            primary_info = sbc.linux.I2C.get_display_info(0)[0]
420
421            # get info about a display called 'Benq GL2450H'
422            benq_info = sbc.linux.I2C.get_display_info('Benq GL2450H')[0]
423            ```
424        '''
425        all_displays = __cache__.get('i2c_display_info')
426        if all_displays is None:
427            all_displays = []
428            index = 0
429
430            for i2c_path in glob.glob('/dev/i2c-*'):
431                if not os.path.exists(i2c_path):
432                    continue
433
434                try:
435                    # open the I2C device using the host read address
436                    device = cls.I2CDevice(i2c_path, cls.HOST_ADDR_R)
437                    # read some 512 bytes from the device
438                    data = device.read(512)
439                except IOError as e:
440                    cls.logger.error(f'IOError reading from device {i2c_path}: {e}')
441                    continue
442
443                # search for the EDID header within our 512 read bytes
444                start = data.find(bytes.fromhex('00 FF FF FF FF FF FF 00'))
445                if start < 0:
446                    continue
447
448                # grab 128 bytes of the edid
449                edid = data[start: start + 128]
450                # parse the EDID
451                manufacturer_id, manufacturer, model, name, serial = EDID.parse(edid)
452                # convert edid to hex string
453                edid = ''.join(f'{i:02x}' for i in edid)
454
455                all_displays.append(
456                    {
457                        'name': name,
458                        'model': model,
459                        'manufacturer': manufacturer,
460                        'manufacturer_id': manufacturer_id,
461                        'serial': serial,
462                        'method': cls,
463                        'index': index,
464                        'edid': edid,
465                        'i2c_bus': i2c_path
466                    }
467                )
468                index += 1
469
470            if all_displays:
471                __cache__.store('i2c_display_info', all_displays, expires=2)
472
473        if display is not None:
474            return filter_monitors(display=display, haystack=all_displays, include=['i2c_bus'])
475        return all_displays
476
477    @classmethod
478    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
479        '''
480        Gets the brightness for a display by querying the I2C bus
481
482        Args:
483            display (int): The specific display you wish to query.
484
485        Returns:
486            list: list of ints (0 to 100)
487
488        Example:
489            ```python
490            import screen_brightness_control as sbc
491
492            # get the current display brightness
493            current_brightness = sbc.linux.I2C.get_brightness()
494
495            # get the brightness of the primary display
496            primary_brightness = sbc.linux.I2C.get_brightness(display = 0)[0]
497
498            # get the brightness of the secondary display
499            secondary_brightness = sbc.linux.I2C.get_brightness(display = 1)[0]
500            ```
501        '''
502        all_displays = cls.get_display_info()
503        if display is not None:
504            all_displays = [all_displays[display]]
505
506        results = []
507        for device in all_displays:
508            interface = cls.DDCInterface(device['i2c_bus'])
509            value, max_value = interface.getvcp(0x10)
510
511            # make sure display's max brighness is cached
512            cache_ident = '%s-%s-%s' % (device['name'], device['model'], device['serial'])
513            if cache_ident not in cls._max_brightness_cache:
514                cls._max_brightness_cache[cache_ident] = max_value
515                cls.logger.info(f'{cache_ident} max brightness:{max_value} (current: {value})')
516
517            if max_value != 100:
518                # if max value is not 100 then we have to adjust the scale to be
519                # a percentage
520                value = int((value / max_value) * 100)
521
522            results.append(value)
523
524        return results
525
526    @classmethod
527    def set_brightness(cls, value: int, display: Optional[int] = None):
528        '''
529        Sets the brightness for a display by writing to the I2C bus
530
531        Args:
532            value (int): Set the brightness to this value
533            display (int): The specific display you wish to adjust.
534
535        Example:
536            ```python
537            import screen_brightness_control as sbc
538
539            # set the brightness to 50%
540            sbc.linux.I2C.set_brightness(50)
541
542            # set the primary display brightness to 75%
543            sbc.linux.I2C.set_brightness(75, display = 0)
544
545            # set the secondary display brightness to 25%
546            sbc.linux.I2C.set_brightness(25, display = 1)
547            ```
548        '''
549        all_displays = cls.get_display_info()
550        if display is not None:
551            all_displays = [all_displays[display]]
552
553        for device in all_displays:
554            # make sure display brightness max value is cached
555            cache_ident = '%s-%s-%s' % (device['name'], device['model'], device['serial'])
556            if cache_ident not in cls._max_brightness_cache:
557                cls.get_brightness(display=device['index'])
558
559            # scale the brightness value according to the max brightness
560            max_value = cls._max_brightness_cache[cache_ident]
561            if max_value != 100:
562                value = int((value / 100) * max_value)
563
564            interface = cls.DDCInterface(device['i2c_bus'])
565            interface.setvcp(0x10, value)

In the same spirit as SysFiles, this class serves as a way of getting display information and adjusting the brightness without relying on any 3rd party software.

Usage of this class requires read and write permission for /dev/i2c-*.

This class works over the I2C bus, primarily with desktop monitors as I haven't tested any e-DP displays yet.

Massive thanks to siemer for his work on the ddcci.py project, which served as a my main reference for this.

References:
GET_VCP_CMD = 1

VCP command to get the value of a feature (eg: brightness)

GET_VCP_REPLY = 2

VCP feature reply op code

SET_VCP_CMD = 3

VCP command to set the value of a feature (eg: brightness)

DDCCI_ADDR = 55

DDC packets are transmittred using this I2C address

HOST_ADDR_R = 80

Packet source address (the computer) when reading data

HOST_ADDR_W = 81

Packet source address (the computer) when writing data

DESTINATION_ADDR_W = 110

Packet destination address (the monitor) when writing data

I2C_SLAVE = 1795

The I2C slave address

WAIT_TIME = 0.05

How long to wait between I2C commands

@classmethod
def get_display_info(cls, display: Union[str, int, NoneType] = None) -> List[dict]:
397    @classmethod
398    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
399        '''
400        Returns information about detected displays by querying the various I2C buses
401
402        Args:
403            display (str or int): The display to return info about.
404                Pass in the serial number, name, model, interface, edid or index.
405                This is passed to `filter_monitors`
406
407        Returns:
408            list: list of dicts
409
410        Example:
411            ```python
412            import screen_brightness_control as sbc
413
414            # get info about all displays
415            info = sbc.linux.I2C.get_display_info()
416            # EG output: [{'name': 'Benq GL2450H', 'model': 'GL2450H', 'manufacturer': 'BenQ', 'edid': '00ffff...'}]
417
418            # get info about the primary display
419            primary_info = sbc.linux.I2C.get_display_info(0)[0]
420
421            # get info about a display called 'Benq GL2450H'
422            benq_info = sbc.linux.I2C.get_display_info('Benq GL2450H')[0]
423            ```
424        '''
425        all_displays = __cache__.get('i2c_display_info')
426        if all_displays is None:
427            all_displays = []
428            index = 0
429
430            for i2c_path in glob.glob('/dev/i2c-*'):
431                if not os.path.exists(i2c_path):
432                    continue
433
434                try:
435                    # open the I2C device using the host read address
436                    device = cls.I2CDevice(i2c_path, cls.HOST_ADDR_R)
437                    # read some 512 bytes from the device
438                    data = device.read(512)
439                except IOError as e:
440                    cls.logger.error(f'IOError reading from device {i2c_path}: {e}')
441                    continue
442
443                # search for the EDID header within our 512 read bytes
444                start = data.find(bytes.fromhex('00 FF FF FF FF FF FF 00'))
445                if start < 0:
446                    continue
447
448                # grab 128 bytes of the edid
449                edid = data[start: start + 128]
450                # parse the EDID
451                manufacturer_id, manufacturer, model, name, serial = EDID.parse(edid)
452                # convert edid to hex string
453                edid = ''.join(f'{i:02x}' for i in edid)
454
455                all_displays.append(
456                    {
457                        'name': name,
458                        'model': model,
459                        'manufacturer': manufacturer,
460                        'manufacturer_id': manufacturer_id,
461                        'serial': serial,
462                        'method': cls,
463                        'index': index,
464                        'edid': edid,
465                        'i2c_bus': i2c_path
466                    }
467                )
468                index += 1
469
470            if all_displays:
471                __cache__.store('i2c_display_info', all_displays, expires=2)
472
473        if display is not None:
474            return filter_monitors(display=display, haystack=all_displays, include=['i2c_bus'])
475        return all_displays

Returns information about detected displays by querying the various I2C buses

Arguments:
  • display (str or int): The display to return info about. Pass in the serial number, name, model, interface, edid or index. This is passed to filter_monitors
Returns:
  • list: list of dicts
Example:
import screen_brightness_control as sbc

# get info about all displays
info = sbc.linux.I2C.get_display_info()
# EG output: [{'name': 'Benq GL2450H', 'model': 'GL2450H', 'manufacturer': 'BenQ', 'edid': '00ffff...'}]

# get info about the primary display
primary_info = sbc.linux.I2C.get_display_info(0)[0]

# get info about a display called 'Benq GL2450H'
benq_info = sbc.linux.I2C.get_display_info('Benq GL2450H')[0]
@classmethod
def get_brightness(cls, display: Optional[int] = None) -> List[int]:
477    @classmethod
478    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
479        '''
480        Gets the brightness for a display by querying the I2C bus
481
482        Args:
483            display (int): The specific display you wish to query.
484
485        Returns:
486            list: list of ints (0 to 100)
487
488        Example:
489            ```python
490            import screen_brightness_control as sbc
491
492            # get the current display brightness
493            current_brightness = sbc.linux.I2C.get_brightness()
494
495            # get the brightness of the primary display
496            primary_brightness = sbc.linux.I2C.get_brightness(display = 0)[0]
497
498            # get the brightness of the secondary display
499            secondary_brightness = sbc.linux.I2C.get_brightness(display = 1)[0]
500            ```
501        '''
502        all_displays = cls.get_display_info()
503        if display is not None:
504            all_displays = [all_displays[display]]
505
506        results = []
507        for device in all_displays:
508            interface = cls.DDCInterface(device['i2c_bus'])
509            value, max_value = interface.getvcp(0x10)
510
511            # make sure display's max brighness is cached
512            cache_ident = '%s-%s-%s' % (device['name'], device['model'], device['serial'])
513            if cache_ident not in cls._max_brightness_cache:
514                cls._max_brightness_cache[cache_ident] = max_value
515                cls.logger.info(f'{cache_ident} max brightness:{max_value} (current: {value})')
516
517            if max_value != 100:
518                # if max value is not 100 then we have to adjust the scale to be
519                # a percentage
520                value = int((value / max_value) * 100)
521
522            results.append(value)
523
524        return results

Gets the brightness for a display by querying the I2C bus

Arguments:
  • display (int): The specific display you wish to query.
Returns:
  • list: list of ints (0 to 100)
Example:
import screen_brightness_control as sbc

# get the current display brightness
current_brightness = sbc.linux.I2C.get_brightness()

# get the brightness of the primary display
primary_brightness = sbc.linux.I2C.get_brightness(display = 0)[0]

# get the brightness of the secondary display
secondary_brightness = sbc.linux.I2C.get_brightness(display = 1)[0]
@classmethod
def set_brightness(cls, value: int, display: Optional[int] = None):
526    @classmethod
527    def set_brightness(cls, value: int, display: Optional[int] = None):
528        '''
529        Sets the brightness for a display by writing to the I2C bus
530
531        Args:
532            value (int): Set the brightness to this value
533            display (int): The specific display you wish to adjust.
534
535        Example:
536            ```python
537            import screen_brightness_control as sbc
538
539            # set the brightness to 50%
540            sbc.linux.I2C.set_brightness(50)
541
542            # set the primary display brightness to 75%
543            sbc.linux.I2C.set_brightness(75, display = 0)
544
545            # set the secondary display brightness to 25%
546            sbc.linux.I2C.set_brightness(25, display = 1)
547            ```
548        '''
549        all_displays = cls.get_display_info()
550        if display is not None:
551            all_displays = [all_displays[display]]
552
553        for device in all_displays:
554            # make sure display brightness max value is cached
555            cache_ident = '%s-%s-%s' % (device['name'], device['model'], device['serial'])
556            if cache_ident not in cls._max_brightness_cache:
557                cls.get_brightness(display=device['index'])
558
559            # scale the brightness value according to the max brightness
560            max_value = cls._max_brightness_cache[cache_ident]
561            if max_value != 100:
562                value = int((value / 100) * max_value)
563
564            interface = cls.DDCInterface(device['i2c_bus'])
565            interface.setvcp(0x10, value)

Sets the brightness for a display by writing to the I2C bus

Arguments:
  • value (int): Set the brightness to this value
  • display (int): The specific display you wish to adjust.
Example:
import screen_brightness_control as sbc

# set the brightness to 50%
sbc.linux.I2C.set_brightness(50)

# set the primary display brightness to 75%
sbc.linux.I2C.set_brightness(75, display = 0)

# set the secondary display brightness to 25%
sbc.linux.I2C.set_brightness(25, display = 1)
class I2C.I2CDevice:
245    class I2CDevice():
246        '''
247        Class to read and write data to an I2C bus,
248        based on the `I2CDev` class from [ddcci.py](https://github.com/siemer/ddcci)
249        '''
250        def __init__(self, fname: str, slave_addr: int):
251            '''
252            Args:
253                fname (str): the I2C path, eg: `/dev/i2c-2`
254                slave_addr (int): not entirely sure what this is meant to be
255            '''
256            self.device = os.open(fname, os.O_RDWR)
257            # I2C_SLAVE address setup
258            fcntl.ioctl(self.device, I2C.I2C_SLAVE, slave_addr)
259
260        def read(self, length: int) -> bytes:
261            '''
262            Read a certain number of bytes from the I2C bus
263
264            Args:
265                length (int): the number of bytes to read
266
267            Returns:
268                bytes
269            '''
270            return os.read(self.device, length)
271
272        def write(self, data: bytes) -> int:
273            '''
274            Writes data to the I2C bus
275
276            Args:
277                data (bytes): the data to write
278
279            Returns:
280                int: the number of bytes written
281            '''
282            return os.write(self.device, data)

Class to read and write data to an I2C bus, based on the I2CDev class from ddcci.py

I2C.I2CDevice(fname: str, slave_addr: int)
250        def __init__(self, fname: str, slave_addr: int):
251            '''
252            Args:
253                fname (str): the I2C path, eg: `/dev/i2c-2`
254                slave_addr (int): not entirely sure what this is meant to be
255            '''
256            self.device = os.open(fname, os.O_RDWR)
257            # I2C_SLAVE address setup
258            fcntl.ioctl(self.device, I2C.I2C_SLAVE, slave_addr)
Arguments:
  • fname (str): the I2C path, eg: /dev/i2c-2
  • slave_addr (int): not entirely sure what this is meant to be
def read(self, length: int) -> bytes:
260        def read(self, length: int) -> bytes:
261            '''
262            Read a certain number of bytes from the I2C bus
263
264            Args:
265                length (int): the number of bytes to read
266
267            Returns:
268                bytes
269            '''
270            return os.read(self.device, length)

Read a certain number of bytes from the I2C bus

Arguments:
  • length (int): the number of bytes to read
Returns:
  • bytes
def write(self, data: bytes) -> int:
272        def write(self, data: bytes) -> int:
273            '''
274            Writes data to the I2C bus
275
276            Args:
277                data (bytes): the data to write
278
279            Returns:
280                int: the number of bytes written
281            '''
282            return os.write(self.device, data)

Writes data to the I2C bus

Arguments:
  • data (bytes): the data to write
Returns:
  • int: the number of bytes written
class I2C.DDCInterface(I2C.I2CDevice):
284    class DDCInterface(I2CDevice):
285        '''
286        Class to send DDC (Display Data Channel) commands to an I2C device,
287        based on the `Ddcci` and `Mccs` classes from [ddcci.py](https://github.com/siemer/ddcci)
288        '''
289
290        PROTOCOL_FLAG = 0x80
291
292        def __init__(self, i2c_path: str):
293            '''
294            Args:
295                i2c_path (str): the path to the I2C device, eg: `/dev/i2c-2`
296            '''
297            self.logger = logger.getChild(self.__class__.__name__).getChild(i2c_path)
298            super().__init__(i2c_path, I2C.DDCCI_ADDR)
299
300        def write(self, *args) -> int:
301            '''
302            Write some data to the I2C device.
303
304            It is recommended to use `setvcp` to set VCP values on the DDC device
305            instead of using this function directly.
306
307            Args:
308                *args: variable length list of arguments. This will be put
309                    into a `bytearray` and wrapped up in various flags and
310                    checksums before being written to the I2C device
311
312            Returns:
313                int: the number of bytes that were written
314            '''
315            time.sleep(I2C.WAIT_TIME)
316
317            ba = bytearray(args)
318            ba.insert(0, len(ba) | self.PROTOCOL_FLAG)  # add length info
319            ba.insert(0, I2C.HOST_ADDR_W)  # insert source address
320            ba.append(functools.reduce(operator.xor, ba, I2C.DESTINATION_ADDR_W))  # checksum
321
322            return super().write(ba)
323
324        def setvcp(self, vcp_code: int, value: int) -> int:
325            '''
326            Set a VCP value on the device
327
328            Args:
329                vcp_code (int): the VCP command to send, eg: `0x10` is brightness
330                value (int): what to set the value to
331
332            Returns:
333                int: the number of bytes written to the device
334            '''
335            return self.write(I2C.SET_VCP_CMD, vcp_code, *value.to_bytes(2, 'big'))
336
337        def read(self, amount: int) -> bytes:
338            '''
339            Reads data from the DDC device.
340
341            It is recommended to use `getvcp` to retrieve VCP values from the
342            DDC device instead of using this function directly.
343
344            Args:
345                amount (int): the number of bytes to read
346
347            Returns:
348                bytes
349
350            Raises:
351                ValueError: if the read data is deemed invalid
352            '''
353            time.sleep(I2C.WAIT_TIME)
354
355            ba = super().read(amount + 3)
356
357            # check the bytes read
358            checks = {
359                'source address': ba[0] == I2C.DESTINATION_ADDR_W,
360                'checksum': functools.reduce(operator.xor, ba) == I2C.HOST_ADDR_R,
361                'length': len(ba) >= (ba[1] & ~self.PROTOCOL_FLAG) + 3
362            }
363            if False in checks.values():
364                self.logger.error('i2c read check failed: ' + repr(checks))
365                raise I2CValidationError('i2c read check failed: ' + repr(checks))
366
367            return ba[2:-1]
368
369        def getvcp(self, vcp_code: int) -> Tuple[int, int]:
370            '''
371            Retrieves a VCP value from the DDC device.
372
373            Args:
374                vcp_code (int): the VCP value to read, eg: `0x10` is brightness
375
376            Returns:
377                tuple[int, int]: the current and maximum value respectively
378
379            Raises:
380                ValueError: if the read data is deemed invalid
381            '''
382            self.write(I2C.GET_VCP_CMD, vcp_code)
383            ba = self.read(8)
384
385            checks = {
386                'is feature reply': ba[0] == I2C.GET_VCP_REPLY,
387                'supported VCP opcode': ba[1] == 0,
388                'answer matches request': ba[2] == vcp_code
389            }
390            if False in checks.values():
391                self.logger.error('i2c read check failed: ' + repr(checks))
392                raise I2CValidationError('i2c read check failed: ' + repr(checks))
393
394            # current and max values
395            return int.from_bytes(ba[6:8], 'big'), int.from_bytes(ba[4:6], 'big')

Class to send DDC (Display Data Channel) commands to an I2C device, based on the Ddcci and Mccs classes from ddcci.py

I2C.DDCInterface(i2c_path: str)
292        def __init__(self, i2c_path: str):
293            '''
294            Args:
295                i2c_path (str): the path to the I2C device, eg: `/dev/i2c-2`
296            '''
297            self.logger = logger.getChild(self.__class__.__name__).getChild(i2c_path)
298            super().__init__(i2c_path, I2C.DDCCI_ADDR)
Arguments:
  • i2c_path (str): the path to the I2C device, eg: /dev/i2c-2
def write(self, *args) -> int:
300        def write(self, *args) -> int:
301            '''
302            Write some data to the I2C device.
303
304            It is recommended to use `setvcp` to set VCP values on the DDC device
305            instead of using this function directly.
306
307            Args:
308                *args: variable length list of arguments. This will be put
309                    into a `bytearray` and wrapped up in various flags and
310                    checksums before being written to the I2C device
311
312            Returns:
313                int: the number of bytes that were written
314            '''
315            time.sleep(I2C.WAIT_TIME)
316
317            ba = bytearray(args)
318            ba.insert(0, len(ba) | self.PROTOCOL_FLAG)  # add length info
319            ba.insert(0, I2C.HOST_ADDR_W)  # insert source address
320            ba.append(functools.reduce(operator.xor, ba, I2C.DESTINATION_ADDR_W))  # checksum
321
322            return super().write(ba)

Write some data to the I2C device.

It is recommended to use setvcp to set VCP values on the DDC device instead of using this function directly.

Arguments:
  • *args: variable length list of arguments. This will be put into a bytearray and wrapped up in various flags and checksums before being written to the I2C device
Returns:
  • int: the number of bytes that were written
def setvcp(self, vcp_code: int, value: int) -> int:
324        def setvcp(self, vcp_code: int, value: int) -> int:
325            '''
326            Set a VCP value on the device
327
328            Args:
329                vcp_code (int): the VCP command to send, eg: `0x10` is brightness
330                value (int): what to set the value to
331
332            Returns:
333                int: the number of bytes written to the device
334            '''
335            return self.write(I2C.SET_VCP_CMD, vcp_code, *value.to_bytes(2, 'big'))

Set a VCP value on the device

Arguments:
  • vcp_code (int): the VCP command to send, eg: 0x10 is brightness
  • value (int): what to set the value to
Returns:
  • int: the number of bytes written to the device
def read(self, amount: int) -> bytes:
337        def read(self, amount: int) -> bytes:
338            '''
339            Reads data from the DDC device.
340
341            It is recommended to use `getvcp` to retrieve VCP values from the
342            DDC device instead of using this function directly.
343
344            Args:
345                amount (int): the number of bytes to read
346
347            Returns:
348                bytes
349
350            Raises:
351                ValueError: if the read data is deemed invalid
352            '''
353            time.sleep(I2C.WAIT_TIME)
354
355            ba = super().read(amount + 3)
356
357            # check the bytes read
358            checks = {
359                'source address': ba[0] == I2C.DESTINATION_ADDR_W,
360                'checksum': functools.reduce(operator.xor, ba) == I2C.HOST_ADDR_R,
361                'length': len(ba) >= (ba[1] & ~self.PROTOCOL_FLAG) + 3
362            }
363            if False in checks.values():
364                self.logger.error('i2c read check failed: ' + repr(checks))
365                raise I2CValidationError('i2c read check failed: ' + repr(checks))
366
367            return ba[2:-1]

Reads data from the DDC device.

It is recommended to use getvcp to retrieve VCP values from the DDC device instead of using this function directly.

Arguments:
  • amount (int): the number of bytes to read
Returns:
  • bytes
Raises:
  • ValueError: if the read data is deemed invalid
def getvcp(self, vcp_code: int) -> Tuple[int, int]:
369        def getvcp(self, vcp_code: int) -> Tuple[int, int]:
370            '''
371            Retrieves a VCP value from the DDC device.
372
373            Args:
374                vcp_code (int): the VCP value to read, eg: `0x10` is brightness
375
376            Returns:
377                tuple[int, int]: the current and maximum value respectively
378
379            Raises:
380                ValueError: if the read data is deemed invalid
381            '''
382            self.write(I2C.GET_VCP_CMD, vcp_code)
383            ba = self.read(8)
384
385            checks = {
386                'is feature reply': ba[0] == I2C.GET_VCP_REPLY,
387                'supported VCP opcode': ba[1] == 0,
388                'answer matches request': ba[2] == vcp_code
389            }
390            if False in checks.values():
391                self.logger.error('i2c read check failed: ' + repr(checks))
392                raise I2CValidationError('i2c read check failed: ' + repr(checks))
393
394            # current and max values
395            return int.from_bytes(ba[6:8], 'big'), int.from_bytes(ba[4:6], 'big')

Retrieves a VCP value from the DDC device.

Arguments:
  • vcp_code (int): the VCP value to read, eg: 0x10 is brightness
Returns:
  • tuple[int, int]: the current and maximum value respectively
Raises:
  • ValueError: if the read data is deemed invalid
568class Light(BrightnessMethod):
569    '''collection of screen brightness related methods using the light executable'''
570
571    executable: str = 'light'
572    '''the light executable to be called'''
573
574    @classmethod
575    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
576        '''
577        Returns information about detected displays as reported by Light.
578
579        It works by taking the output of `SysFiles.get_display_info` and
580        filtering out any displays that aren't supported by Light
581
582        Args:
583            display (str or int): The display to return info about.
584                Pass in the serial number, name, model, interface, edid or index.
585                This is passed to `filter_monitors`
586
587        Returns:
588            list: list of dicts
589
590        Example:
591            ```python
592            import screen_brightness_control as sbc
593
594            # get info about all displays
595            info = sbc.linux.Light.get_display_info()
596            # EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]
597
598            # get info about the primary display
599            primary_info = sbc.linux.Light.get_display_info(0)[0]
600
601            # get info about a display called 'edp-backlight'
602            edp_info = sbc.linux.Light.get_display_info('edp-backlight')[0]
603            ```
604        '''
605        light_output = check_output([cls.executable, '-L']).decode()
606        displays = []
607        index = 0
608        for device in SysFiles.get_display_info():
609            # SysFiles scrapes info from the same place that Light used to
610            # so it makes sense to use that output
611            if device['path'].replace('/sys/class', 'sysfs') in light_output:
612                del device['scale']
613                device['light_path'] = device['path'].replace('/sys/class', 'sysfs')
614                device['method'] = cls
615                device['index'] = index
616
617                displays.append(device)
618                index += 1
619
620        if display is not None:
621            displays = filter_monitors(display=display, haystack=displays, include=['path', 'light_path'])
622        return displays
623
624    @classmethod
625    def set_brightness(cls, value: int, display: Optional[int] = None):
626        '''
627        Sets the brightness for a display using the light executable
628
629        Args:
630            value (int): Sets the brightness to this value
631            display (int): The specific display you wish to query.
632
633        Example:
634            ```python
635            import screen_brightness_control as sbc
636
637            # set the brightness to 50%
638            sbc.linux.Light.set_brightness(50)
639
640            # set the primary display brightness to 75%
641            sbc.linux.Light.set_brightness(75, display = 0)
642
643            # set the secondary display brightness to 25%
644            sbc.linux.Light.set_brightness(25, display = 1)
645            ```
646        '''
647        info = cls.get_display_info()
648        if display is not None:
649            info = [info[display]]
650
651        for i in info:
652            check_output(f'{cls.executable} -S {value} -s {i["light_path"]}'.split(" "))
653
654    @classmethod
655    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
656        '''
657        Gets the brightness for a display using the light executable
658
659        Args:
660            display (int): The specific display you wish to query.
661
662        Returns:
663            list: list of ints (0 to 100)
664
665        Example:
666            ```python
667            import screen_brightness_control as sbc
668
669            # get the current display brightness
670            current_brightness = sbc.linux.Light.get_brightness()
671
672            # get the brightness of the primary display
673            primary_brightness = sbc.linux.Light.get_brightness(display = 0)[0]
674
675            # get the brightness of the secondary display
676            edp_brightness = sbc.linux.Light.get_brightness(display = 1)[0]
677            ```
678        '''
679        info = cls.get_display_info()
680        if display is not None:
681            info = [info[display]]
682
683        results = []
684        for i in info:
685            results.append(
686                check_output([cls.executable, '-G', '-s', i['light_path']])
687            )
688        results = [int(round(float(i.decode()), 0)) for i in results]
689        return results

collection of screen brightness related methods using the light executable

executable: str = 'light'

the light executable to be called

@classmethod
def get_display_info(cls, display: Union[str, int, NoneType] = None) -> List[dict]:
574    @classmethod
575    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
576        '''
577        Returns information about detected displays as reported by Light.
578
579        It works by taking the output of `SysFiles.get_display_info` and
580        filtering out any displays that aren't supported by Light
581
582        Args:
583            display (str or int): The display to return info about.
584                Pass in the serial number, name, model, interface, edid or index.
585                This is passed to `filter_monitors`
586
587        Returns:
588            list: list of dicts
589
590        Example:
591            ```python
592            import screen_brightness_control as sbc
593
594            # get info about all displays
595            info = sbc.linux.Light.get_display_info()
596            # EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]
597
598            # get info about the primary display
599            primary_info = sbc.linux.Light.get_display_info(0)[0]
600
601            # get info about a display called 'edp-backlight'
602            edp_info = sbc.linux.Light.get_display_info('edp-backlight')[0]
603            ```
604        '''
605        light_output = check_output([cls.executable, '-L']).decode()
606        displays = []
607        index = 0
608        for device in SysFiles.get_display_info():
609            # SysFiles scrapes info from the same place that Light used to
610            # so it makes sense to use that output
611            if device['path'].replace('/sys/class', 'sysfs') in light_output:
612                del device['scale']
613                device['light_path'] = device['path'].replace('/sys/class', 'sysfs')
614                device['method'] = cls
615                device['index'] = index
616
617                displays.append(device)
618                index += 1
619
620        if display is not None:
621            displays = filter_monitors(display=display, haystack=displays, include=['path', 'light_path'])
622        return displays

Returns information about detected displays as reported by Light.

It works by taking the output of SysFiles.get_display_info and filtering out any displays that aren't supported by Light

Arguments:
  • display (str or int): The display to return info about. Pass in the serial number, name, model, interface, edid or index. This is passed to filter_monitors
Returns:
  • list: list of dicts
Example:
import screen_brightness_control as sbc

# get info about all displays
info = sbc.linux.Light.get_display_info()
# EG output: [{'name': 'edp-backlight', 'path': '/sys/class/backlight/edp-backlight', edid': '00ffff...'}]

# get info about the primary display
primary_info = sbc.linux.Light.get_display_info(0)[0]

# get info about a display called 'edp-backlight'
edp_info = sbc.linux.Light.get_display_info('edp-backlight')[0]
@classmethod
def set_brightness(cls, value: int, display: Optional[int] = None):
624    @classmethod
625    def set_brightness(cls, value: int, display: Optional[int] = None):
626        '''
627        Sets the brightness for a display using the light executable
628
629        Args:
630            value (int): Sets the brightness to this value
631            display (int): The specific display you wish to query.
632
633        Example:
634            ```python
635            import screen_brightness_control as sbc
636
637            # set the brightness to 50%
638            sbc.linux.Light.set_brightness(50)
639
640            # set the primary display brightness to 75%
641            sbc.linux.Light.set_brightness(75, display = 0)
642
643            # set the secondary display brightness to 25%
644            sbc.linux.Light.set_brightness(25, display = 1)
645            ```
646        '''
647        info = cls.get_display_info()
648        if display is not None:
649            info = [info[display]]
650
651        for i in info:
652            check_output(f'{cls.executable} -S {value} -s {i["light_path"]}'.split(" "))

Sets the brightness for a display using the light executable

Arguments:
  • value (int): Sets the brightness to this value
  • display (int): The specific display you wish to query.
Example:
import screen_brightness_control as sbc

# set the brightness to 50%
sbc.linux.Light.set_brightness(50)

# set the primary display brightness to 75%
sbc.linux.Light.set_brightness(75, display = 0)

# set the secondary display brightness to 25%
sbc.linux.Light.set_brightness(25, display = 1)
@classmethod
def get_brightness(cls, display: Optional[int] = None) -> List[int]:
654    @classmethod
655    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
656        '''
657        Gets the brightness for a display using the light executable
658
659        Args:
660            display (int): The specific display you wish to query.
661
662        Returns:
663            list: list of ints (0 to 100)
664
665        Example:
666            ```python
667            import screen_brightness_control as sbc
668
669            # get the current display brightness
670            current_brightness = sbc.linux.Light.get_brightness()
671
672            # get the brightness of the primary display
673            primary_brightness = sbc.linux.Light.get_brightness(display = 0)[0]
674
675            # get the brightness of the secondary display
676            edp_brightness = sbc.linux.Light.get_brightness(display = 1)[0]
677            ```
678        '''
679        info = cls.get_display_info()
680        if display is not None:
681            info = [info[display]]
682
683        results = []
684        for i in info:
685            results.append(
686                check_output([cls.executable, '-G', '-s', i['light_path']])
687            )
688        results = [int(round(float(i.decode()), 0)) for i in results]
689        return results

Gets the brightness for a display using the light executable

Arguments:
  • display (int): The specific display you wish to query.
Returns:
  • list: list of ints (0 to 100)
Example:
import screen_brightness_control as sbc

# get the current display brightness
current_brightness = sbc.linux.Light.get_brightness()

# get the brightness of the primary display
primary_brightness = sbc.linux.Light.get_brightness(display = 0)[0]

# get the brightness of the secondary display
edp_brightness = sbc.linux.Light.get_brightness(display = 1)[0]
692class XRandr(BrightnessMethodAdv):
693    '''collection of screen brightness related methods using the xrandr executable'''
694
695    executable: str = 'xrandr'
696    '''the xrandr executable to be called'''
697
698    @classmethod
699    def _gdi(cls):
700        '''
701        .. warning:: Don't use this
702           This function isn't final and I will probably make breaking changes to it.
703           You have been warned
704
705        Gets all displays reported by XRandr even if they're not supported
706        '''
707        xrandr_output = check_output([cls.executable, '--verbose']).decode().split('\n')
708
709        display_count = 0
710        tmp_display = {}
711
712        for line_index, line in enumerate(xrandr_output):
713            if line == '':
714                continue
715
716            if not line.startswith((' ', '\t')) and 'connected' in line and 'disconnected' not in line:
717                if tmp_display:
718                    yield tmp_display
719
720                tmp_display = {
721                    'name': line.split(' ')[0],
722                    'interface': line.split(' ')[0],
723                    'method': cls,
724                    'index': display_count,
725                    'model': None,
726                    'serial': None,
727                    'manufacturer': None,
728                    'manufacturer_id': None,
729                    'edid': None,
730                    'unsupported': line.startswith('XWAYLAND')
731                }
732                display_count += 1
733
734            elif 'EDID:' in line:
735                # extract the edid from the chunk of the output that will contain the edid
736                edid = ''.join(
737                    i.replace('\t', '') for i in xrandr_output[line_index + 1: line_index + 9]
738                )
739                tmp_display['edid'] = edid
740
741                for key, value in zip(
742                    ('manufacturer_id', 'manufacturer', 'model', 'name', 'serial'),
743                    EDID.parse(tmp_display['edid'])
744                ):
745                    if value is None:
746                        continue
747                    tmp_display[key] = value
748
749            elif 'Brightness:' in line:
750                tmp_display['brightness'] = int(float(line.replace('Brightness:', '')) * 100)
751
752        if tmp_display:
753            yield tmp_display
754
755    @classmethod
756    def get_display_info(cls, display: Optional[Union[int, str]] = None, brightness: bool = False) -> List[dict]:
757        '''
758        Returns info about all detected displays as reported by xrandr
759
760        Args:
761            display (str or int): The display to return info about.
762                Pass in the serial number, name, model, interface, edid or index.
763                This is passed to `filter_monitors`
764            brightness (bool): whether to include the current brightness
765                in the returned info
766
767        Returns:
768            list: list of dicts
769
770        Example:
771            ```python
772            import screen_brightness_control as sbc
773
774            info = sbc.linux.XRandr.get_display_info()
775            for i in info:
776                print('================')
777                for key, value in i.items():
778                    print(key, ':', value)
779
780            # get information about the first XRandr addressable display
781            primary_info = sbc.linux.XRandr.get_display_info(0)[0]
782
783            # get information about a display with a specific name
784            benq_info = sbc.linux.XRandr.get_display_info('BenQ GL2450HM')[0]
785            ```
786        '''
787        valid_displays = []
788        for item in cls._gdi():
789            if item['unsupported']:
790                continue
791            if not brightness:
792                del item['brightness']
793            del item['unsupported']
794            valid_displays.append(item)
795        if display is not None:
796            valid_displays = filter_monitors(display=display, haystack=valid_displays, include=['interface'])
797        return valid_displays
798
799    @classmethod
800    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
801        '''
802        Returns the brightness for a display using the xrandr executable
803
804        Args:
805            display (int): The specific display you wish to query.
806
807        Returns:
808            list: list of integers (from 0 to 100)
809
810        Example:
811            ```python
812            import screen_brightness_control as sbc
813
814            # get the current brightness
815            current_brightness = sbc.linux.XRandr.get_brightness()
816
817            # get the current brightness for the primary display
818            primary_brightness = sbc.linux.XRandr.get_brightness(display=0)[0]
819            ```
820        '''
821        monitors = cls.get_display_info(brightness=True)
822        if display is not None:
823            monitors = [monitors[display]]
824        brightness = [i['brightness'] for i in monitors]
825
826        return brightness
827
828    @classmethod
829    def set_brightness(cls, value: int, display: Optional[int] = None):
830        '''
831        Sets the brightness for a display using the xrandr executable
832
833        Args:
834            value (int): Sets the brightness to this value
835            display (int): The specific display you wish to query.
836
837        Example:
838            ```python
839            import screen_brightness_control as sbc
840
841            # set the brightness to 50
842            sbc.linux.XRandr.set_brightness(50)
843
844            # set the brightness of the primary display to 75
845            sbc.linux.XRandr.set_brightness(75, display=0)
846            ```
847        '''
848        value = str(float(value) / 100)
849        info = cls.get_display_info()
850        if display is not None:
851            info = [info[display]]
852
853        for i in info:
854            check_output([cls.executable, '--output', i['interface'], '--brightness', value])

collection of screen brightness related methods using the xrandr executable

executable: str = 'xrandr'

the xrandr executable to be called

@classmethod
def get_display_info( cls, display: Union[str, int, NoneType] = None, brightness: bool = False) -> List[dict]:
755    @classmethod
756    def get_display_info(cls, display: Optional[Union[int, str]] = None, brightness: bool = False) -> List[dict]:
757        '''
758        Returns info about all detected displays as reported by xrandr
759
760        Args:
761            display (str or int): The display to return info about.
762                Pass in the serial number, name, model, interface, edid or index.
763                This is passed to `filter_monitors`
764            brightness (bool): whether to include the current brightness
765                in the returned info
766
767        Returns:
768            list: list of dicts
769
770        Example:
771            ```python
772            import screen_brightness_control as sbc
773
774            info = sbc.linux.XRandr.get_display_info()
775            for i in info:
776                print('================')
777                for key, value in i.items():
778                    print(key, ':', value)
779
780            # get information about the first XRandr addressable display
781            primary_info = sbc.linux.XRandr.get_display_info(0)[0]
782
783            # get information about a display with a specific name
784            benq_info = sbc.linux.XRandr.get_display_info('BenQ GL2450HM')[0]
785            ```
786        '''
787        valid_displays = []
788        for item in cls._gdi():
789            if item['unsupported']:
790                continue
791            if not brightness:
792                del item['brightness']
793            del item['unsupported']
794            valid_displays.append(item)
795        if display is not None:
796            valid_displays = filter_monitors(display=display, haystack=valid_displays, include=['interface'])
797        return valid_displays

Returns info about all detected displays as reported by xrandr

Arguments:
  • display (str or int): The display to return info about. Pass in the serial number, name, model, interface, edid or index. This is passed to filter_monitors
  • brightness (bool): whether to include the current brightness in the returned info
Returns:
  • list: list of dicts
Example:
import screen_brightness_control as sbc

info = sbc.linux.XRandr.get_display_info()
for i in info:
    print('================')
    for key, value in i.items():
        print(key, ':', value)

# get information about the first XRandr addressable display
primary_info = sbc.linux.XRandr.get_display_info(0)[0]

# get information about a display with a specific name
benq_info = sbc.linux.XRandr.get_display_info('BenQ GL2450HM')[0]
@classmethod
def get_brightness(cls, display: Optional[int] = None) -> List[int]:
799    @classmethod
800    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
801        '''
802        Returns the brightness for a display using the xrandr executable
803
804        Args:
805            display (int): The specific display you wish to query.
806
807        Returns:
808            list: list of integers (from 0 to 100)
809
810        Example:
811            ```python
812            import screen_brightness_control as sbc
813
814            # get the current brightness
815            current_brightness = sbc.linux.XRandr.get_brightness()
816
817            # get the current brightness for the primary display
818            primary_brightness = sbc.linux.XRandr.get_brightness(display=0)[0]
819            ```
820        '''
821        monitors = cls.get_display_info(brightness=True)
822        if display is not None:
823            monitors = [monitors[display]]
824        brightness = [i['brightness'] for i in monitors]
825
826        return brightness

Returns the brightness for a display using the xrandr executable

Arguments:
  • display (int): The specific display you wish to query.
Returns:
  • list: list of integers (from 0 to 100)
Example:
import screen_brightness_control as sbc

# get the current brightness
current_brightness = sbc.linux.XRandr.get_brightness()

# get the current brightness for the primary display
primary_brightness = sbc.linux.XRandr.get_brightness(display=0)[0]
@classmethod
def set_brightness(cls, value: int, display: Optional[int] = None):
828    @classmethod
829    def set_brightness(cls, value: int, display: Optional[int] = None):
830        '''
831        Sets the brightness for a display using the xrandr executable
832
833        Args:
834            value (int): Sets the brightness to this value
835            display (int): The specific display you wish to query.
836
837        Example:
838            ```python
839            import screen_brightness_control as sbc
840
841            # set the brightness to 50
842            sbc.linux.XRandr.set_brightness(50)
843
844            # set the brightness of the primary display to 75
845            sbc.linux.XRandr.set_brightness(75, display=0)
846            ```
847        '''
848        value = str(float(value) / 100)
849        info = cls.get_display_info()
850        if display is not None:
851            info = [info[display]]
852
853        for i in info:
854            check_output([cls.executable, '--output', i['interface'], '--brightness', value])

Sets the brightness for a display using the xrandr executable

Arguments:
  • value (int): Sets the brightness to this value
  • display (int): The specific display you wish to query.
Example:
import screen_brightness_control as sbc

# set the brightness to 50
sbc.linux.XRandr.set_brightness(50)

# set the brightness of the primary display to 75
sbc.linux.XRandr.set_brightness(75, display=0)
 857class DDCUtil(BrightnessMethodAdv):
 858    '''collection of screen brightness related methods using the ddcutil executable'''
 859    logger = logger.getChild('DDCUtil')
 860
 861    executable: str = 'ddcutil'
 862    '''the ddcutil executable to be called'''
 863    sleep_multiplier: float = 0.5
 864    '''how long ddcutil should sleep between each DDC request (lower is shorter).
 865    See [the ddcutil docs](https://www.ddcutil.com/performance_options/) for more info.'''
 866    cmd_max_tries: int = 10
 867    '''max number of retries when calling the ddcutil'''
 868    _max_brightness_cache: dict = {}
 869    '''Cache for displays and their maximum brightness values'''
 870
 871    @classmethod
 872    def _gdi(cls):
 873        '''
 874        .. warning:: Don't use this
 875           This function isn't final and I will probably make breaking changes to it.
 876           You have been warned
 877
 878        Gets all displays reported by DDCUtil even if they're not supported
 879        '''
 880        raw_ddcutil_output = str(
 881            check_output(
 882                [
 883                    cls.executable, 'detect', '-v', '--async',
 884                    f'--sleep-multiplier={cls.sleep_multiplier}'
 885                ], max_tries=cls.cmd_max_tries
 886            )
 887        )[2:-1].split('\\n')
 888        # Use -v to get EDID string but this means output cannot be decoded.
 889        # Or maybe it can. I don't know the encoding though, so let's assume it cannot be decoded.
 890        # Use str()[2:-1] workaround
 891
 892        # include "Invalid display" sections because they tell us where one displays metadata ends
 893        # and another begins. We filter out invalid displays later on
 894        ddcutil_output = [i for i in raw_ddcutil_output if i.startswith(('Invalid display', 'Display', '\t', ' '))]
 895        tmp_display = {}
 896        display_count = 0
 897
 898        for line_index, line in enumerate(ddcutil_output):
 899            if not line.startswith(('\t', ' ')):
 900                if tmp_display:
 901                    yield tmp_display
 902
 903                tmp_display = {
 904                    'method': cls,
 905                    'index': display_count,
 906                    'model': None,
 907                    'serial': None,
 908                    'bin_serial': None,
 909                    'manufacturer': None,
 910                    'manufacturer_id': None,
 911                    'edid': None,
 912                    'unsupported': 'invalid display' in line.lower()
 913                }
 914                display_count += 1
 915
 916            elif 'I2C bus' in line:
 917                tmp_display['i2c_bus'] = line[line.index('/'):]
 918                tmp_display['bus_number'] = int(tmp_display['i2c_bus'].replace('/dev/i2c-', ''))
 919
 920            elif 'Mfg id' in line:
 921                # Recently ddcutil has started reporting manufacturer IDs like
 922                # 'BNQ - UNK' or 'MSI - Microstep' so we have to split the line
 923                # into chunks of alpha chars and check for a valid mfg id
 924                for code in re.split(r'[^A-Za-z]', line.replace('Mfg id:', '').replace(' ', '')):
 925                    if len(code) != 3:
 926                        # all mfg ids are 3 chars long
 927                        continue
 928
 929                    try:
 930                        (
 931                            tmp_display['manufacturer_id'],
 932                            tmp_display['manufacturer']
 933                        ) = _monitor_brand_lookup(code)
 934                    except TypeError:
 935                        continue
 936                    else:
 937                        break
 938
 939            elif 'Model' in line:
 940                # the split() removes extra spaces
 941                name = line.replace('Model:', '').split()
 942                try:
 943                    tmp_display['model'] = name[1]
 944                except IndexError:
 945                    pass
 946                tmp_display['name'] = ' '.join(name)
 947
 948            elif 'Serial number' in line:
 949                tmp_display['serial'] = line.replace('Serial number:', '').replace(' ', '') or None
 950
 951            elif 'Binary serial number:' in line:
 952                tmp_display['bin_serial'] = line.split(' ')[-1][3:-1]
 953
 954            elif 'EDID hex dump:' in line:
 955                try:
 956                    tmp_display['edid'] = ''.join(
 957                        ''.join(i.split()[1:17]) for i in ddcutil_output[line_index + 2: line_index + 10]
 958                    )
 959                except Exception:
 960                    pass
 961
 962        if tmp_display:
 963            yield tmp_display
 964
 965    @classmethod
 966    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
 967        '''
 968        Returns information about all DDC compatible displays shown by DDCUtil
 969        Works by calling the command 'ddcutil detect' and parsing the output.
 970
 971        Args:
 972            display (int or str): The display to return info about.
 973                Pass in the serial number, name, model, i2c bus, edid or index.
 974                This is passed to `filter_monitors`
 975
 976        Returns:
 977            list: list of dicts
 978
 979        Example:
 980            ```python
 981            import screen_brightness_control as sbc
 982
 983            info = sbc.linux.DDCUtil.get_display_info()
 984            for i in info:
 985                print('================')
 986                for key, value in i.items():
 987                    print(key, ':', value)
 988
 989            # get information about the first DDCUtil addressable display
 990            primary_info = sbc.linux.DDCUtil.get_display_info(0)[0]
 991
 992            # get information about a display with a specific name
 993            benq_info = sbc.linux.DDCUtil.get_display_info('BenQ GL2450HM')[0]
 994            ```
 995        '''
 996        valid_displays = __cache__.get('ddcutil_monitors_info')
 997        if valid_displays is None:
 998            valid_displays = []
 999            for item in cls._gdi():
1000                if item['unsupported']:
1001                    continue
1002                del item['unsupported']
1003                valid_displays.append(item)
1004
1005            if valid_displays:
1006                __cache__.store('ddcutil_monitors_info', valid_displays)
1007
1008        if display is not None:
1009            valid_displays = filter_monitors(display=display, haystack=valid_displays, include=['i2c_bus'])
1010        return valid_displays
1011
1012    @classmethod
1013    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
1014        '''
1015        Returns the brightness for a display using the ddcutil executable
1016
1017        Args:
1018            display (int): The specific display you wish to query.
1019
1020        Returns:
1021            list: list of ints (0 to 100)
1022
1023        Example:
1024            ```python
1025            import screen_brightness_control as sbc
1026
1027            # get the current brightness
1028            current_brightness = sbc.linux.DDCUtil.get_brightness()
1029
1030            # get the current brightness for the primary display
1031            primary_brightness = sbc.linux.DDCUtil.get_brightness(display=0)[0]
1032            ```
1033        '''
1034        monitors = cls.get_display_info()
1035        if display is not None:
1036            monitors = [monitors[display]]
1037
1038        res = []
1039        for monitor in monitors:
1040            value = __cache__.get(f'ddcutil_brightness_{monitor["index"]}')
1041            if value is None:
1042                cmd_out = check_output(
1043                    [
1044                        cls.executable,
1045                        'getvcp', '10', '-t',
1046                        '-b', str(monitor['bus_number']),
1047                        f'--sleep-multiplier={cls.sleep_multiplier}'
1048                    ], max_tries=cls.cmd_max_tries
1049                ).decode().split(' ')
1050
1051                value = int(cmd_out[-2])
1052                max_value = int(cmd_out[-1])
1053                if max_value != 100:
1054                    # if the max brightness is not 100 then the number is not a percentage
1055                    # and will need to be scaled
1056                    value = int((value / max_value) * 100)
1057
1058                # now make sure max brightness is recorded so set_brightness can use it
1059                cache_ident = '%s-%s-%s' % (monitor['name'], monitor['serial'], monitor['bin_serial'])
1060                if cache_ident not in cls._max_brightness_cache:
1061                    cls._max_brightness_cache[cache_ident] = max_value
1062                    cls.logger.debug(f'{cache_ident} max brightness:{max_value} (current: {value})')
1063
1064                __cache__.store(f'ddcutil_brightness_{monitor["index"]}', value, expires=0.5)
1065            res.append(value)
1066        return res
1067
1068    @classmethod
1069    def set_brightness(cls, value: int, display: Optional[int] = None):
1070        '''
1071        Sets the brightness for a display using the ddcutil executable
1072
1073        Args:
1074            value (int): Sets the brightness to this value
1075            display (int): The specific display you wish to query.
1076
1077        Example:
1078            ```python
1079            import screen_brightness_control as sbc
1080
1081            # set the brightness to 50
1082            sbc.linux.DDCUtil.set_brightness(50)
1083
1084            # set the brightness of the primary display to 75
1085            sbc.linux.DDCUtil.set_brightness(75, display=0)
1086            ```
1087        '''
1088        monitors = cls.get_display_info()
1089        if display is not None:
1090            monitors = [monitors[display]]
1091
1092        __cache__.expire(startswith='ddcutil_brightness_')
1093        for monitor in monitors:
1094            # check if monitor has a max brightness that requires us to scale this value
1095            cache_ident = '%s-%s-%s' % (monitor['name'], monitor['serial'], monitor['bin_serial'])
1096            if cache_ident not in cls._max_brightness_cache:
1097                cls.get_brightness(display=monitor['index'])
1098
1099            if cls._max_brightness_cache[cache_ident] != 100:
1100                value = int((value / 100) * cls._max_brightness_cache[cache_ident])
1101
1102            check_output(
1103                [
1104                    cls.executable, 'setvcp', '10', str(value),
1105                    '-b', str(monitor['bus_number']),
1106                    f'--sleep-multiplier={cls.sleep_multiplier}'
1107                ], max_tries=cls.cmd_max_tries
1108            )

collection of screen brightness related methods using the ddcutil executable

executable: str = 'ddcutil'

the ddcutil executable to be called

sleep_multiplier: float = 0.5

how long ddcutil should sleep between each DDC request (lower is shorter). See the ddcutil docs for more info.

cmd_max_tries: int = 10

max number of retries when calling the ddcutil

@classmethod
def get_display_info(cls, display: Union[str, int, NoneType] = None) -> List[dict]:
 965    @classmethod
 966    def get_display_info(cls, display: Optional[Union[int, str]] = None) -> List[dict]:
 967        '''
 968        Returns information about all DDC compatible displays shown by DDCUtil
 969        Works by calling the command 'ddcutil detect' and parsing the output.
 970
 971        Args:
 972            display (int or str): The display to return info about.
 973                Pass in the serial number, name, model, i2c bus, edid or index.
 974                This is passed to `filter_monitors`
 975
 976        Returns:
 977            list: list of dicts
 978
 979        Example:
 980            ```python
 981            import screen_brightness_control as sbc
 982
 983            info = sbc.linux.DDCUtil.get_display_info()
 984            for i in info:
 985                print('================')
 986                for key, value in i.items():
 987                    print(key, ':', value)
 988
 989            # get information about the first DDCUtil addressable display
 990            primary_info = sbc.linux.DDCUtil.get_display_info(0)[0]
 991
 992            # get information about a display with a specific name
 993            benq_info = sbc.linux.DDCUtil.get_display_info('BenQ GL2450HM')[0]
 994            ```
 995        '''
 996        valid_displays = __cache__.get('ddcutil_monitors_info')
 997        if valid_displays is None:
 998            valid_displays = []
 999            for item in cls._gdi():
1000                if item['unsupported']:
1001                    continue
1002                del item['unsupported']
1003                valid_displays.append(item)
1004
1005            if valid_displays:
1006                __cache__.store('ddcutil_monitors_info', valid_displays)
1007
1008        if display is not None:
1009            valid_displays = filter_monitors(display=display, haystack=valid_displays, include=['i2c_bus'])
1010        return valid_displays

Returns information about all DDC compatible displays shown by DDCUtil Works by calling the command 'ddcutil detect' and parsing the output.

Arguments:
  • display (int or str): The display to return info about. Pass in the serial number, name, model, i2c bus, edid or index. This is passed to filter_monitors
Returns:
  • list: list of dicts
Example:
import screen_brightness_control as sbc

info = sbc.linux.DDCUtil.get_display_info()
for i in info:
    print('================')
    for key, value in i.items():
        print(key, ':', value)

# get information about the first DDCUtil addressable display
primary_info = sbc.linux.DDCUtil.get_display_info(0)[0]

# get information about a display with a specific name
benq_info = sbc.linux.DDCUtil.get_display_info('BenQ GL2450HM')[0]
@classmethod
def get_brightness(cls, display: Optional[int] = None) -> List[int]:
1012    @classmethod
1013    def get_brightness(cls, display: Optional[int] = None) -> List[int]:
1014        '''
1015        Returns the brightness for a display using the ddcutil executable
1016
1017        Args:
1018            display (int): The specific display you wish to query.
1019
1020        Returns:
1021            list: list of ints (0 to 100)
1022
1023        Example:
1024            ```python
1025            import screen_brightness_control as sbc
1026
1027            # get the current brightness
1028            current_brightness = sbc.linux.DDCUtil.get_brightness()
1029
1030            # get the current brightness for the primary display
1031            primary_brightness = sbc.linux.DDCUtil.get_brightness(display=0)[0]
1032            ```
1033        '''
1034        monitors = cls.get_display_info()
1035        if display is not None:
1036            monitors = [monitors[display]]
1037
1038        res = []
1039        for monitor in monitors:
1040            value = __cache__.get(f'ddcutil_brightness_{monitor["index"]}')
1041            if value is None:
1042                cmd_out = check_output(
1043                    [
1044                        cls.executable,
1045                        'getvcp', '10', '-t',
1046                        '-b', str(monitor['bus_number']),
1047                        f'--sleep-multiplier={cls.sleep_multiplier}'
1048                    ], max_tries=cls.cmd_max_tries
1049                ).decode().split(' ')
1050
1051                value = int(cmd_out[-2])
1052                max_value = int(cmd_out[-1])
1053                if max_value != 100:
1054                    # if the max brightness is not 100 then the number is not a percentage
1055                    # and will need to be scaled
1056                    value = int((value / max_value) * 100)
1057
1058                # now make sure max brightness is recorded so set_brightness can use it
1059                cache_ident = '%s-%s-%s' % (monitor['name'], monitor['serial'], monitor['bin_serial'])
1060                if cache_ident not in cls._max_brightness_cache:
1061                    cls._max_brightness_cache[cache_ident] = max_value
1062                    cls.logger.debug(f'{cache_ident} max brightness:{max_value} (current: {value})')
1063
1064                __cache__.store(f'ddcutil_brightness_{monitor["index"]}', value, expires=0.5)
1065            res.append(value)
1066        return res

Returns the brightness for a display using the ddcutil executable

Arguments:
  • display (int): The specific display you wish to query.
Returns:
  • list: list of ints (0 to 100)
Example:
import screen_brightness_control as sbc

# get the current brightness
current_brightness = sbc.linux.DDCUtil.get_brightness()

# get the current brightness for the primary display
primary_brightness = sbc.linux.DDCUtil.get_brightness(display=0)[0]
@classmethod
def set_brightness(cls, value: int, display: Optional[int] = None):
1068    @classmethod
1069    def set_brightness(cls, value: int, display: Optional[int] = None):
1070        '''
1071        Sets the brightness for a display using the ddcutil executable
1072
1073        Args:
1074            value (int): Sets the brightness to this value
1075            display (int): The specific display you wish to query.
1076
1077        Example:
1078            ```python
1079            import screen_brightness_control as sbc
1080
1081            # set the brightness to 50
1082            sbc.linux.DDCUtil.set_brightness(50)
1083
1084            # set the brightness of the primary display to 75
1085            sbc.linux.DDCUtil.set_brightness(75, display=0)
1086            ```
1087        '''
1088        monitors = cls.get_display_info()
1089        if display is not None:
1090            monitors = [monitors[display]]
1091
1092        __cache__.expire(startswith='ddcutil_brightness_')
1093        for monitor in monitors:
1094            # check if monitor has a max brightness that requires us to scale this value
1095            cache_ident = '%s-%s-%s' % (monitor['name'], monitor['serial'], monitor['bin_serial'])
1096            if cache_ident not in cls._max_brightness_cache:
1097                cls.get_brightness(display=monitor['index'])
1098
1099            if cls._max_brightness_cache[cache_ident] != 100:
1100                value = int((value / 100) * cls._max_brightness_cache[cache_ident])
1101
1102            check_output(
1103                [
1104                    cls.executable, 'setvcp', '10', str(value),
1105                    '-b', str(monitor['bus_number']),
1106                    f'--sleep-multiplier={cls.sleep_multiplier}'
1107                ], max_tries=cls.cmd_max_tries
1108            )

Sets the brightness for a display using the ddcutil executable

Arguments:
  • value (int): Sets the brightness to this value
  • display (int): The specific display you wish to query.
Example:
import screen_brightness_control as sbc

# set the brightness to 50
sbc.linux.DDCUtil.set_brightness(50)

# set the brightness of the primary display to 75
sbc.linux.DDCUtil.set_brightness(75, display=0)
def list_monitors_info( method: Optional[str] = None, allow_duplicates: bool = False, unsupported: bool = False) -> List[dict]:
1111def list_monitors_info(
1112    method: Optional[str] = None, allow_duplicates: bool = False, unsupported: bool = False
1113) -> List[dict]:
1114    '''
1115    Lists detailed information about all detected displays
1116
1117    Args:
1118        method (str): the method the display can be addressed by. See `screen_brightness_control.get_methods`
1119            for more info on available methods
1120        allow_duplicates (bool): whether to filter out duplicate displays (displays with the same EDID) or not
1121        unsupported (bool): include detected displays that are invalid or unsupported
1122
1123    Returns:
1124        list: list of dicts
1125
1126    Example:
1127        ```python
1128        import screen_brightness_control as sbc
1129
1130        displays = sbc.linux.list_monitors_info()
1131        for display in displays:
1132            print('=======================')
1133            # the manufacturer name plus the model OR a generic name for the display, depending on the method
1134            print('Name:', display['name'])
1135            # the general model of the display
1136            print('Model:', display['model'])
1137            # the serial of the display
1138            print('Serial:', display['serial'])
1139            # the name of the brand of the display
1140            print('Manufacturer:', display['manufacturer'])
1141            # the 3 letter code corresponding to the brand name, EG: BNQ -> BenQ
1142            print('Manufacturer ID:', display['manufacturer_id'])
1143            # the index of that display FOR THE SPECIFIC METHOD THE DISPLAY USES
1144            print('Index:', display['index'])
1145            # the method this display can be addressed by
1146            print('Method:', display['method'])
1147        ```
1148    '''
1149    all_methods = get_methods(method).values()
1150    haystack = []
1151    for method_class in all_methods:
1152        try:
1153            if unsupported and issubclass(method_class, BrightnessMethodAdv):
1154                haystack += method_class._gdi()
1155            else:
1156                haystack += method_class.get_display_info()
1157        except Exception as e:
1158            logger.warning(f'error grabbing display info from {method_class} - {format_exc(e)}')
1159            pass
1160
1161    if allow_duplicates:
1162        return haystack
1163
1164    try:
1165        # use filter_monitors to remove duplicates
1166        return filter_monitors(haystack=haystack)
1167    except NoValidDisplayError:
1168        return []

Lists detailed information about all detected displays

Arguments:
  • method (str): the method the display can be addressed by. See screen_brightness_control.get_methods for more info on available methods
  • allow_duplicates (bool): whether to filter out duplicate displays (displays with the same EDID) or not
  • unsupported (bool): include detected displays that are invalid or unsupported
Returns:
  • list: list of dicts
Example:
import screen_brightness_control as sbc

displays = sbc.linux.list_monitors_info()
for display in displays:
    print('=======================')
    # the manufacturer name plus the model OR a generic name for the display, depending on the method
    print('Name:', display['name'])
    # the general model of the display
    print('Model:', display['model'])
    # the serial of the display
    print('Serial:', display['serial'])
    # the name of the brand of the display
    print('Manufacturer:', display['manufacturer'])
    # the 3 letter code corresponding to the brand name, EG: BNQ -> BenQ
    print('Manufacturer ID:', display['manufacturer_id'])
    # the index of that display FOR THE SPECIFIC METHOD THE DISPLAY USES
    print('Index:', display['index'])
    # the method this display can be addressed by
    print('Method:', display['method'])