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.
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]
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]
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:
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]
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]
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)
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
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
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
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
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
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
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
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
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
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
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]
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)
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
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]
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]
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
how long ddcutil should sleep between each DDC request (lower is shorter). See the ddcutil docs for more info.
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]
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]
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)
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'])