1importlogging
+ 2importplatform
+ 3importthreading
+ 4importtime
+ 5importtraceback
+ 6fromdataclassesimportdataclass,field
+ 7fromtypingimportCallable,Any,Dict,List,Optional,Tuple,Type,Union,FrozenSet,ClassVar
+ 8from._versionimport__author__,__version__# noqa: F401
+ 9from.exceptionsimportNoValidDisplayError,format_exc
+ 10from.helpersimport(BrightnessMethod,ScreenBrightnessError,
+ 11logarithmic_range,percentage)
+ 12from.typesimportDisplayIdentifier,IntPercentage,Percentage
+ 13from.importconfig
+ 14
+ 15
+ 16_logger=logging.getLogger(__name__)
+ 17_logger.addHandler(logging.NullHandler())
+ 18
+ 19
+ 20@config.default_params
+ 21defget_brightness(
+ 22display:Optional[DisplayIdentifier]=None,
+ 23method:Optional[str]=None,
+ 24allow_duplicates:Optional[bool]=None,
+ 25verbose_error:bool=False
+ 26)->List[Union[IntPercentage,None]]:
+ 27'''
+ 28 Returns the current brightness of one or more displays
+ 29
+ 30 Args:
+ 31 display (.types.DisplayIdentifier): the specific display to query
+ 32 method: the method to use to get the brightness. See `get_methods` for
+ 33 more info on available methods
+ 34 allow_duplicates: controls whether to filter out duplicate displays or not.
+ 35 verbose_error: controls the level of detail in the error messages
+ 36
+ 37 Returns:
+ 38 A list of `.types.IntPercentage` values, each being the brightness of an
+ 39 individual display. Invalid displays may return None.
+ 40
+ 41 Example:
+ 42 ```python
+ 43 import screen_brightness_control as sbc
+ 44
+ 45 # get the current screen brightness (for all detected displays)
+ 46 current_brightness = sbc.get_brightness()
+ 47
+ 48 # get the brightness of the primary display
+ 49 primary_brightness = sbc.get_brightness(display=0)
+ 50
+ 51 # get the brightness of the secondary display (if connected)
+ 52 secondary_brightness = sbc.get_brightness(display=1)
+ 53 ```
+ 54 '''
+ 55result=__brightness(
+ 56display=display,
+ 57method=method,
+ 58meta_method='get',
+ 59allow_duplicates=allow_duplicates,
+ 60verbose_error=verbose_error
+ 61)
+ 62# __brightness can return None depending on the `no_return` kwarg. That obviously would never happen here
+ 63# but the type checker doesn't see it that way.
+ 64return[]ifresultisNoneelseresult
+ 65
+ 66
+ 67@config.default_params
+ 68defset_brightness(
+ 69value:Percentage,
+ 70display:Optional[DisplayIdentifier]=None,
+ 71method:Optional[str]=None,
+ 72force:bool=False,
+ 73allow_duplicates:Optional[bool]=None,
+ 74verbose_error:bool=False,
+ 75no_return:bool=True
+ 76)->Optional[List[Union[IntPercentage,None]]]:
+ 77'''
+ 78 Sets the brightness level of one or more displays to a given value.
+ 79
+ 80 Args:
+ 81 value (.types.Percentage): the new brightness level
+ 82 display (.types.DisplayIdentifier): the specific display to adjust
+ 83 method: the method to use to set the brightness. See `get_methods` for
+ 84 more info on available methods
+ 85 force: [*Linux Only*] if False the brightness will never be set lower than 1.
+ 86 This is because on most displays a brightness of 0 will turn off the backlight.
+ 87 If True, this check is bypassed
+ 88 allow_duplicates: controls whether to filter out duplicate displays or not.
+ 89 verbose_error: boolean value controls the amount of detail error messages will contain
+ 90 no_return: don't return the new brightness level(s)
+ 91
+ 92 Returns:
+ 93 If `no_return` is set to `True` (the default) then this function returns nothing.
+ 94 Otherwise, a list of `.types.IntPercentage` is returned, each item being the new
+ 95 brightness of each adjusted display (invalid displays may return None)
+ 96
+ 97 Example:
+ 98 ```python
+ 99 import screen_brightness_control as sbc
+100
+101 # set brightness to 50%
+102 sbc.set_brightness(50)
+103
+104 # set brightness to 0%
+105 sbc.set_brightness(0, force=True)
+106
+107 # increase brightness by 25%
+108 sbc.set_brightness('+25')
+109
+110 # decrease brightness by 30%
+111 sbc.set_brightness('-30')
+112
+113 # set the brightness of display 0 to 50%
+114 sbc.set_brightness(50, display=0)
+115 ```
+116 '''
+117ifisinstance(value,str)and('+'invalueor'-'invalue):
+118output:List[Union[IntPercentage,None]]=[]
+119formonitorinfilter_monitors(display=display,method=method,allow_duplicates=allow_duplicates):
+120# `filter_monitors()` will raise an error if no valid displays are found
+121display_instance=Display.from_dict(monitor)
+122display_instance.set_brightness(value=value,force=force)
+123output.append(Noneifno_returnelsedisplay_instance.get_brightness())
+124
+125returnNoneifno_returnelseoutput
+126
+127ifplatform.system()=='Linux'andnotforce:
+128lower_bound=1
+129else:
+130lower_bound=0
+131
+132value=percentage(value,lower_bound=lower_bound)
+133
+134return__brightness(
+135value,display=display,method=method,
+136meta_method='set',no_return=no_return,
+137allow_duplicates=allow_duplicates,
+138verbose_error=verbose_error
+139)
+140
+141
+142@config.default_params
+143deffade_brightness(
+144finish:Percentage,
+145start:Optional[Percentage]=None,
+146interval:float=0.01,
+147increment:int=1,
+148blocking:bool=True,
+149force:bool=False,
+150logarithmic:bool=True,
+151stoppable:bool=True,
+152**kwargs
+153)->Union[List[threading.Thread],List[Union[IntPercentage,None]]]:
+154'''
+155 Gradually change the brightness of one or more displays
+156
+157 Args:
+158 finish (.types.Percentage): fade to this brightness level
+159 start (.types.Percentage): where the brightness should fade from.
+160 If this arg is not specified, the fade will be started from the
+161 current brightness.
+162 interval: the time delay between each step in brightness
+163 increment: the amount to change the brightness by per step
+164 blocking: whether this should occur in the main thread (`True`) or a new daemonic thread (`False`)
+165 force: [*Linux Only*] if False the brightness will never be set lower than 1.
+166 This is because on most displays a brightness of 0 will turn off the backlight.
+167 If True, this check is bypassed
+168 logarithmic: follow a logarithmic brightness curve when adjusting the brightness
+169 stoppable: whether the fade can be stopped by starting a new fade on the same display
+170 **kwargs: passed through to `filter_monitors` for display selection.
+171 Will also be passed to `get_brightness` if `blocking is True`
+172
+173 Returns:
+174 By default, this function calls `get_brightness()` to return the new
+175 brightness of any adjusted displays.
+176
+177 If `blocking` is set to `False`, then a list of threads are
+178 returned, one for each display being faded.
+179
+180 Example:
+181 ```python
+182 import screen_brightness_control as sbc
+183
+184 # fade brightness from the current brightness to 50%
+185 sbc.fade_brightness(50)
+186
+187 # fade the brightness from 25% to 75%
+188 sbc.fade_brightness(75, start=25)
+189
+190 # fade the brightness from the current value to 100% in steps of 10%
+191 sbc.fade_brightness(100, increment=10)
+192
+193 # fade the brightness from 100% to 90% with time intervals of 0.1 seconds
+194 sbc.fade_brightness(90, start=100, interval=0.1)
+195
+196 # fade the brightness to 100% in a new thread
+197 sbc.fade_brightness(100, blocking=False)
+198 ```
+199 '''
+200# make sure only compatible kwargs are passed to filter_monitors
+201available_monitors=filter_monitors(
+202**{k:vfork,vinkwargs.items()ifkin(
+203'display','haystack','method','include','allow_duplicates'
+204)}
+205)
+206
+207threads=[]
+208foriinavailable_monitors:
+209display=Display.from_dict(i)
+210
+211thread=threading.Thread(target=display._fade_brightness,args=(finish,),kwargs={
+212'start':start,
+213'interval':interval,
+214'increment':increment,
+215'force':force,
+216'logarithmic':logarithmic,
+217'stoppable':stoppable
+218})
+219thread.start()
+220threads.append(thread)
+221
+222ifnotblocking:
+223returnthreads
+224
+225fortinthreads:
+226t.join()
+227returnget_brightness(**kwargs)
+228
+229
+230@config.default_params
+231deflist_monitors_info(
+232method:Optional[str]=None,allow_duplicates:Optional[bool]=None,unsupported:bool=False
+233)->List[dict]:
+234'''
+235 List detailed information about all displays that are controllable by this library
+236
+237 Args:
+238 method: the method to use to list the available displays. See `get_methods` for
+239 more info on available methods
+240 allow_duplicates: controls whether to filter out duplicate displays or not.
+241 unsupported: include detected displays that are invalid or unsupported
+242
+243 Returns:
+244 list: list of dictionaries containing information about the detected displays
+245
+246 Example:
+247 ```python
+248 import screen_brightness_control as sbc
+249 displays = sbc.list_monitors_info()
+250 for display in displays:
+251 print('=======================')
+252 # the manufacturer name plus the model
+253 print('Name:', display['name'])
+254 # the general model of the display
+255 print('Model:', display['model'])
+256 # the serial of the display
+257 print('Serial:', display['serial'])
+258 # the name of the brand of the display
+259 print('Manufacturer:', display['manufacturer'])
+260 # the 3 letter code corresponding to the brand name, EG: BNQ -> BenQ
+261 print('Manufacturer ID:', display['manufacturer_id'])
+262 # the index of that display FOR THE SPECIFIC METHOD THE DISPLAY USES
+263 print('Index:', display['index'])
+264 # the method this display can be addressed by
+265 print('Method:', display['method'])
+266 # the EDID string associated with that display
+267 print('EDID:', display['edid'])
+268 # The UID of the display
+269 print('UID:', display['uid'])
+270 ```
+271 '''
+272return_OS_MODULE.list_monitors_info(
+273method=method,allow_duplicates=allow_duplicates,unsupported=unsupported
+274)
+275
+276
+277@config.default_params
+278deflist_monitors(method:Optional[str]=None,allow_duplicates:Optional[bool]=None)->List[str]:
+279'''
+280 List the names of all detected displays
+281
+282 Args:
+283 method: the method to use to list the available displays. See `get_methods` for
+284 more info on available methods
+285 allow_duplicates: controls whether to filter out duplicate displays or not.
+286
+287 Example:
+288 ```python
+289 import screen_brightness_control as sbc
+290 display_names = sbc.list_monitors()
+291 # eg: ['BenQ GL2450H', 'Dell U2211H']
+292 ```
+293 '''
+294return[i['name']foriinlist_monitors_info(method=method,allow_duplicates=allow_duplicates)]
+295
+296
+297defget_methods(name:Optional[str]=None)->Dict[str,Type[BrightnessMethod]]:
+298'''
+299 Returns all available brightness method names and their associated classes.
+300
+301 Args:
+302 name: if specified, return the method corresponding to this name
+303
+304 Raises:
+305 ValueError: if the given name is incorrect
+306
+307 Example:
+308 ```python
+309 import screen_brightness_control as sbc
+310
+311 all_methods = sbc.get_methods()
+312
+313 for method_name, method_class in all_methods.items():
+314 print('Method:', method_name)
+315 print('Class:', method_class)
+316 print('Associated monitors:', sbc.list_monitors(method=method_name))
+317 ```
+318 '''
+319methods:Dict[str,type[BrightnessMethod]]={i.__name__.lower():iforiin_OS_MODULE.METHODS}
+320
+321ifnameisNone:
+322returnmethods
+323
+324ifnotisinstance(name,str):
+325raiseTypeError(f'name must be of type str, not {type(name)!r}')
+326
+327name=name.lower()
+328ifnameinmethods:
+329return{name:methods[name]}
+330
+331_logger.debug(f'requested method {name!r} invalid')
+332raiseValueError(
+333f'invalid method {name!r}, must be one of: {list(methods)}')
+334
+335
+336@dataclass
+337classDisplay():
+338'''
+339 Represents a single connected display.
+340 '''
+341index:int
+342'''The index of the display relative to the method it uses.
+343 So if the index is 0 and the method is `windows.VCP`, then this is the 1st
+344 display reported by `windows.VCP`, not the first display overall.'''
+345method:Type[BrightnessMethod]
+346'''The method by which this monitor can be addressed.
+347 This will be a class from either the windows or linux sub-module'''
+348
+349uid:Optional[str]=None
+350'''A unique identifier for the display. This is usually inferred from the display's connection to the machine.'''
+351edid:Optional[str]=None
+352'''A 256 character hex string containing information about a display and its capabilities'''
+353manufacturer:Optional[str]=None
+354'''Name of the display's manufacturer'''
+355manufacturer_id:Optional[str]=None
+356'''3 letter code corresponding to the manufacturer name'''
+357model:Optional[str]=None
+358'''Model name of the display'''
+359name:Optional[str]=None
+360'''The name of the display, often the manufacturer name plus the model name'''
+361serial:Optional[str]=None
+362'''The serial number of the display or (if serial is not available) an ID assigned by the OS'''
+363
+364_logger:logging.Logger=field(init=False,repr=False)
+365_fade_thread_dict:ClassVar[Dict[FrozenSet[Any],threading.Thread]]={}
+366'''A dictionary mapping display identifiers to latest fade threads for stopping fades.'''
+367
+368def__post_init__(self):
+369self._logger=_logger.getChild(self.__class__.__name__).getChild(
+370str(self.get_identifier()[1])[:20])
+371
+372deffade_brightness(
+373self,
+374finish:Percentage,
+375start:Optional[Percentage]=None,
+376interval:float=0.01,
+377increment:int=1,
+378force:bool=False,
+379logarithmic:bool=True,
+380blocking:bool=True,
+381stoppable:bool=True
+382)->Optional[threading.Thread]:
+383'''
+384 Gradually change the brightness of this display to a set value.
+385 Can execute in the current thread, blocking until completion,
+386 or in a separate thread, allowing concurrent operations.
+387 When set as non-blocking and stoppable, a new fade can halt the this operation.
+388
+389 Args:
+390 finish (.types.Percentage): the brightness level to end up on
+391 start (.types.Percentage): where the fade should start from. Defaults
+392 to whatever the current brightness level for the display is
+393 interval: time delay between each change in brightness
+394 increment: amount to change the brightness by each time (as a percentage)
+395 force: [*Linux only*] allow the brightness to be set to 0. By default,
+396 brightness values will never be set lower than 1, since setting them to 0
+397 often turns off the backlight
+398 logarithmic: follow a logarithmic curve when setting brightness values.
+399 See `logarithmic_range` for rationale
+400 blocking: run this function in the current thread and block until it completes
+401 stoppable: whether this fade will be stopped by starting a new fade on the same display
+402
+403 Returns:
+404 If `blocking` is `False`, returns a `threading.Thread` object representing the
+405 thread in which the fade operation is running.
+406 Otherwise, it returns None.
+407 '''
+408thread=threading.Thread(target=self._fade_brightness,args=(finish,),kwargs={
+409'start':start,
+410'interval':interval,
+411'increment':increment,
+412'force':force,
+413'logarithmic':logarithmic,
+414'stoppable':stoppable
+415})
+416thread.start()
+417
+418ifnotblocking:
+419returnthread
+420else:
+421thread.join()
+422returnNone
+423
+424def_fade_brightness(
+425self,
+426finish:Percentage,
+427start:Optional[Percentage]=None,
+428interval:float=0.01,
+429increment:int=1,
+430force:bool=False,
+431logarithmic:bool=True,
+432stoppable:bool=True
+433)->None:
+434# Record the latest thread for this display so that other stoppable threads can be stopped
+435display_key=frozenset((self.method,self.index))
+436self._fade_thread_dict[display_key]=threading.current_thread()
+437# minimum brightness value
+438ifplatform.system()=='Linux'andnotforce:
+439lower_bound=1
+440else:
+441lower_bound=0
+442
+443current=self.get_brightness()
+444
+445finish=percentage(finish,current,lower_bound)
+446start=percentage(
+447currentifstartisNoneelsestart,current,lower_bound)
+448
+449# mypy says "object is not callable" but range is. Ignore this
+450range_func:Callable=logarithmic_rangeiflogarithmicelserange# type: ignore[assignment]
+451increment=abs(increment)
+452ifstart>finish:
+453increment=-increment
+454
+455self._logger.debug(
+456f'fade {start}->{finish}:{increment}:logarithmic={logarithmic}')
+457
+458# Record the time when the next brightness change should start
+459next_change_start_time=time.time()
+460forvalueinrange_func(start,finish,increment):
+461ifstoppableandthreading.current_thread()!=self._fade_thread_dict[display_key]:
+462# If the current thread is stoppable and it's not the latest thread, stop fading
+463break
+464# `value` is ensured not to hit `finish` in loop, this will be handled in the final step.
+465self.set_brightness(value,force=force)
+466
+467# `interval` is the intended time between the start of each brightness change.
+468next_change_start_time+=interval
+469sleep_time=next_change_start_time-time.time()
+470# Skip sleep if the scheduled time has already passed
+471ifsleep_time>0:
+472time.sleep(sleep_time)
+473else:
+474# As `value` doesn't hit `finish` in loop, we explicitly set brightness to `finish`.
+475# This also avoids an unnecessary sleep in the last iteration.
+476ifnotstoppableorthreading.current_thread()==self._fade_thread_dict[display_key]:
+477self.set_brightness(finish,force=force)
+478
+479@classmethod
+480deffrom_dict(cls,display:dict)->'Display':
+481'''
+482 Initialise an instance of the class from a dictionary, ignoring
+483 any unwanted keys
+484 '''
+485returncls(
+486index=display['index'],
+487method=display['method'],
+488edid=display['edid'],
+489manufacturer=display['manufacturer'],
+490manufacturer_id=display['manufacturer_id'],
+491model=display['model'],
+492name=display['name'],
+493serial=display['serial'],
+494uid=display.get('uid')
+495)
+496
+497defget_brightness(self)->IntPercentage:
+498'''
+499 Returns the brightness of this display.
+500
+501 Returns:
+502 The brightness value of the display, as a percentage.
+503 See `.types.IntPercentage`
+504 '''
+505returnself.method.get_brightness(display=self.index)[0]
+506
+507defget_identifier(self)->Tuple[str,DisplayIdentifier]:
+508'''
+509 Returns the `.types.DisplayIdentifier` for this display.
+510 Will iterate through the UID, EDID, serial, name and index and return the first
+511 value that is not equal to None
+512
+513 Returns:
+514 The name of the property returned and the value of said property.
+515 EG: `('serial', '123abc...')` or `('name', 'BenQ GL2450H')`
+516 '''
+517forkeyin('uid','edid','serial','name'):
+518value=getattr(self,key,None)
+519ifvalueisnotNone:
+520returnkey,value
+521# the index should surely never be `None`
+522return'index',self.index
+523
+524defis_active(self)->bool:
+525'''
+526 Attempts to retrieve the brightness for this display. If it works the display is deemed active
+527 '''
+528try:
+529self.get_brightness()
+530returnTrue
+531exceptExceptionase:
+532self._logger.error(
+533f'Display.is_active: {self.get_identifier()} failed get_brightness call'
+534f' - {format_exc(e)}'
+535)
+536returnFalse
+537
+538defset_brightness(self,value:Percentage,force:bool=False):
+539'''
+540 Sets the brightness for this display. See `set_brightness` for the full docs
+541
+542 Args:
+543 value (.types.Percentage): the brightness percentage to set the display to
+544 force: allow the brightness to be set to 0 on Linux. This is disabled by default
+545 because setting the brightness of 0 will often turn off the backlight
+546 '''
+547# convert brightness value to percentage
+548ifplatform.system()=='Linux'andnotforce:
+549lower_bound=1
+550else:
+551lower_bound=0
+552
+553value=percentage(
+554value,
+555current=self.get_brightness,
+556lower_bound=lower_bound
+557)
+558
+559self.method.set_brightness(value,display=self.index)
+560
+561
+562@config.default_params
+563deffilter_monitors(
+564display:Optional[DisplayIdentifier]=None,
+565haystack:Optional[List[dict]]=None,
+566method:Optional[str]=None,
+567include:List[str]=[],
+568allow_duplicates:Optional[bool]=None
+569)->List[dict]:
+570'''
+571 Searches through the information for all detected displays
+572 and attempts to return the info matching the value given.
+573 Will attempt to match against index, name, edid, method and serial
+574
+575 Args:
+576 display (.types.DisplayIdentifier): the display you are searching for
+577 haystack: the information to filter from.
+578 If this isn't set it defaults to the return of `list_monitors_info`
+579 method: the method the monitors use. See `get_methods` for
+580 more info on available methods
+581 include: extra fields of information to sort by
+582 allow_duplicates: controls whether to filter out duplicate displays or not
+583
+584 Raises:
+585 NoValidDisplayError: if the display does not have a match
+586 TypeError: if the `display` kwarg is not `int` or `str`
+587
+588 Example:
+589 ```python
+590 import screen_brightness_control as sbc
+591
+592 search = 'GL2450H'
+593 match = sbc.filter_displays(search)
+594 print(match)
+595 # EG output: [{'name': 'BenQ GL2450H', 'model': 'GL2450H', ... }]
+596 ```
+597 '''
+598ifdisplayisnotNoneandtype(display)notin(str,int):
+599raiseTypeError(
+600f'display kwarg must be int or str, not "{type(display).__name__}"')
+601
+602defget_monitor_list():
+603# if we have been provided with a list of monitors to sift through then use that
+604# otherwise, get the info ourselves
+605ifhaystackisnotNone:
+606monitors_with_duplicates=haystack
+607ifmethodisnotNone:
+608method_class=next(iter(get_methods(method).values()))
+609monitors_with_duplicates=[
+610iforiinhaystackifi['method']==method_class]
+611else:
+612monitors_with_duplicates=list_monitors_info(
+613method=method,allow_duplicates=True)
+614
+615returnmonitors_with_duplicates
+616
+617deffilter_monitor_list(to_filter):
+618# This loop does two things:
+619# 1. Filters out duplicate monitors
+620# 2. Matches the display kwarg (if applicable)
+621
+622# When duplicates are allowed, the logic is straightforward:
+623ifallow_duplicates:
+624ifdisplayisNone:
+625# no monitor should be filtered out
+626returnto_filter
+627elifisinstance(display,int):
+628# 'display' variable should be the index of the monitor
+629# return a list with the monitor at the index or an empty list if the index is out of range
+630returnto_filter[display:display+1]
+631elifisinstance(display,str):
+632# 'display' variable should be an identifier of the monitor
+633# multiple monitors with the same identifier are allowed here, so return all of them
+634monitors=[]
+635formonitorinto_filter:
+636foridentifierin['uid','edid','serial','name']+include:
+637ifdisplay==monitor.get(identifier,None):
+638monitors.append(monitor)
+639break
+640returnmonitors
+641
+642filtered_displays={}
+643formonitorinto_filter:
+644# find a valid identifier for a monitor, excluding any which are equal to None
+645added=False
+646foridentifierin['uid','edid','serial','name']+include:
+647ifmonitor.get(identifier,None)isNone:
+648continue
+649
+650m_id=monitor[identifier]
+651ifm_idinfiltered_displays:
+652break
+653
+654ifisinstance(display,str)andm_id!=display:
+655continue
+656
+657# check we haven't already added the monitor
+658ifnotadded:
+659filtered_displays[m_id]=monitor
+660added=True
+661
+662# if the display kwarg is an integer and we are currently at that index
+663ifisinstance(display,int)andlen(filtered_displays)-1==display:
+664return[monitor]
+665
+666ifadded:
+667break
+668returnlist(filtered_displays.values())
+669
+670duplicates=[]
+671for_inrange(3):
+672duplicates=get_monitor_list()
+673ifduplicates:
+674break
+675time.sleep(0.4)
+676else:
+677msg='no displays detected'
+678ifmethodisnotNone:
+679msg+=f' with method: {method!r}'
+680raiseNoValidDisplayError(msg)
+681
+682monitors=filter_monitor_list(duplicates)
+683ifnotmonitors:
+684# if no displays matched the query
+685msg='no displays found'
+686ifdisplayisnotNone:
+687msg+=f' with name/serial/edid/index of {display!r}'
+688ifmethodisnotNone:
+689msg+=f' with method of {method!r}'
+690raiseNoValidDisplayError(msg)
+691
+692returnmonitors
+693
+694
+695def__brightness(
+696*args:Any,
+697display:Optional[DisplayIdentifier]=None,
+698method:Optional[str]=None,
+699meta_method:str='get',
+700no_return:bool=False,
+701allow_duplicates:bool=False,
+702verbose_error:bool=False,
+703**kwargs:Any
+704)->Optional[List[Union[IntPercentage,None]]]:
+705'''Internal function used to get/set brightness'''
+706_logger.debug(
+707f"brightness {meta_method} request display {display} with method {method}")
+708
+709output:List[Union[int,None]]=[]
+710errors=[]
+711
+712formonitorinfilter_monitors(display=display,method=method,allow_duplicates=allow_duplicates):
+713try:
+714ifmeta_method=='set':
+715monitor['method'].set_brightness(
+716*args,display=monitor['index'],**kwargs)
+717ifno_return:
+718output.append(None)
+719continue
+720
+721output+=monitor['method'].get_brightness(
+722display=monitor['index'],**kwargs)
+723exceptExceptionase:
+724output.append(None)
+725errors.append((
+726monitor,e.__class__.__name__,
+727traceback.format_exc()ifverbose_errorelsee
+728))
+729
+730ifoutput:
+731output_is_none=set(output)=={None}
+732if(
+733# can't have None output if we are trying to get the brightness
+734(meta_method=='get'andnotoutput_is_none)
+735or(
+736# if we are setting the brightness then we CAN have a None output
+737# but only if no_return is True.
+738meta_method=='set'
+739and((no_returnandoutput_is_none)ornotoutput_is_none)
+740)
+741):
+742returnNoneifno_returnelseoutput
+743
+744# if the function hasn't returned then it has failed
+745msg='\n'
+746iferrors:
+747formonitor,exc_name,excinerrors:
+748ifisinstance(monitor,str):
+749msg+=f'\t{monitor}'
+750else:
+751msg+=f'\t{monitor["name"]} ({monitor["serial"]})'
+752msg+=f' -> {exc_name}: '
+753msg+=str(exc).replace('\n','\n\t\t')+'\n'
+754else:
+755msg+='\tno valid output was received from brightness methods'
+756
+757raiseScreenBrightnessError(msg)
+758
+759
+760ifplatform.system()=='Windows':
+761from.importwindows
+762_OS_MODULE=windows
+763elifplatform.system()=='Linux':
+764from.importlinux
+765_OS_MODULE=linux
+766else:
+767_logger.warning(
+768f'package imported on unsupported platform ({platform.system()})')
+
21@config.default_params
+22defget_brightness(
+23display:Optional[DisplayIdentifier]=None,
+24method:Optional[str]=None,
+25allow_duplicates:Optional[bool]=None,
+26verbose_error:bool=False
+27)->List[Union[IntPercentage,None]]:
+28'''
+29 Returns the current brightness of one or more displays
+30
+31 Args:
+32 display (.types.DisplayIdentifier): the specific display to query
+33 method: the method to use to get the brightness. See `get_methods` for
+34 more info on available methods
+35 allow_duplicates: controls whether to filter out duplicate displays or not.
+36 verbose_error: controls the level of detail in the error messages
+37
+38 Returns:
+39 A list of `.types.IntPercentage` values, each being the brightness of an
+40 individual display. Invalid displays may return None.
+41
+42 Example:
+43 ```python
+44 import screen_brightness_control as sbc
+45
+46 # get the current screen brightness (for all detected displays)
+47 current_brightness = sbc.get_brightness()
+48
+49 # get the brightness of the primary display
+50 primary_brightness = sbc.get_brightness(display=0)
+51
+52 # get the brightness of the secondary display (if connected)
+53 secondary_brightness = sbc.get_brightness(display=1)
+54 ```
+55 '''
+56result=__brightness(
+57display=display,
+58method=method,
+59meta_method='get',
+60allow_duplicates=allow_duplicates,
+61verbose_error=verbose_error
+62)
+63# __brightness can return None depending on the `no_return` kwarg. That obviously would never happen here
+64# but the type checker doesn't see it that way.
+65return[]ifresultisNoneelseresult
+
+
+
+
Returns the current brightness of one or more displays
importscreen_brightness_controlassbc
+
+# get the current screen brightness (for all detected displays)
+current_brightness=sbc.get_brightness()
+
+# get the brightness of the primary display
+primary_brightness=sbc.get_brightness(display=0)
+
+# get the brightness of the secondary display (if connected)
+secondary_brightness=sbc.get_brightness(display=1)
+
68@config.default_params
+ 69defset_brightness(
+ 70value:Percentage,
+ 71display:Optional[DisplayIdentifier]=None,
+ 72method:Optional[str]=None,
+ 73force:bool=False,
+ 74allow_duplicates:Optional[bool]=None,
+ 75verbose_error:bool=False,
+ 76no_return:bool=True
+ 77)->Optional[List[Union[IntPercentage,None]]]:
+ 78'''
+ 79 Sets the brightness level of one or more displays to a given value.
+ 80
+ 81 Args:
+ 82 value (.types.Percentage): the new brightness level
+ 83 display (.types.DisplayIdentifier): the specific display to adjust
+ 84 method: the method to use to set the brightness. See `get_methods` for
+ 85 more info on available methods
+ 86 force: [*Linux Only*] if False the brightness will never be set lower than 1.
+ 87 This is because on most displays a brightness of 0 will turn off the backlight.
+ 88 If True, this check is bypassed
+ 89 allow_duplicates: controls whether to filter out duplicate displays or not.
+ 90 verbose_error: boolean value controls the amount of detail error messages will contain
+ 91 no_return: don't return the new brightness level(s)
+ 92
+ 93 Returns:
+ 94 If `no_return` is set to `True` (the default) then this function returns nothing.
+ 95 Otherwise, a list of `.types.IntPercentage` is returned, each item being the new
+ 96 brightness of each adjusted display (invalid displays may return None)
+ 97
+ 98 Example:
+ 99 ```python
+100 import screen_brightness_control as sbc
+101
+102 # set brightness to 50%
+103 sbc.set_brightness(50)
+104
+105 # set brightness to 0%
+106 sbc.set_brightness(0, force=True)
+107
+108 # increase brightness by 25%
+109 sbc.set_brightness('+25')
+110
+111 # decrease brightness by 30%
+112 sbc.set_brightness('-30')
+113
+114 # set the brightness of display 0 to 50%
+115 sbc.set_brightness(50, display=0)
+116 ```
+117 '''
+118ifisinstance(value,str)and('+'invalueor'-'invalue):
+119output:List[Union[IntPercentage,None]]=[]
+120formonitorinfilter_monitors(display=display,method=method,allow_duplicates=allow_duplicates):
+121# `filter_monitors()` will raise an error if no valid displays are found
+122display_instance=Display.from_dict(monitor)
+123display_instance.set_brightness(value=value,force=force)
+124output.append(Noneifno_returnelsedisplay_instance.get_brightness())
+125
+126returnNoneifno_returnelseoutput
+127
+128ifplatform.system()=='Linux'andnotforce:
+129lower_bound=1
+130else:
+131lower_bound=0
+132
+133value=percentage(value,lower_bound=lower_bound)
+134
+135return__brightness(
+136value,display=display,method=method,
+137meta_method='set',no_return=no_return,
+138allow_duplicates=allow_duplicates,
+139verbose_error=verbose_error
+140)
+
+
+
+
Sets the brightness level of one or more displays to a given value.
method: the method to use to set the brightness. See get_methods for
+more info on available methods
+
force: [Linux Only] if False the brightness will never be set lower than 1.
+This is because on most displays a brightness of 0 will turn off the backlight.
+If True, this check is bypassed
+
allow_duplicates: controls whether to filter out duplicate displays or not.
+
verbose_error: boolean value controls the amount of detail error messages will contain
+
no_return: don't return the new brightness level(s)
+
+
+
Returns:
+
+
+
If no_return is set to True (the default) then this function returns nothing.
+ Otherwise, a list of screen_brightness_control.types.IntPercentage is returned, each item being the new
+ brightness of each adjusted display (invalid displays may return None)
+
+
+
Example:
+
+
+
+
importscreen_brightness_controlassbc
+
+# set brightness to 50%
+sbc.set_brightness(50)
+
+# set brightness to 0%
+sbc.set_brightness(0,force=True)
+
+# increase brightness by 25%
+sbc.set_brightness('+25')
+
+# decrease brightness by 30%
+sbc.set_brightness('-30')
+
+# set the brightness of display 0 to 50%
+sbc.set_brightness(50,display=0)
+
143@config.default_params
+144deffade_brightness(
+145finish:Percentage,
+146start:Optional[Percentage]=None,
+147interval:float=0.01,
+148increment:int=1,
+149blocking:bool=True,
+150force:bool=False,
+151logarithmic:bool=True,
+152stoppable:bool=True,
+153**kwargs
+154)->Union[List[threading.Thread],List[Union[IntPercentage,None]]]:
+155'''
+156 Gradually change the brightness of one or more displays
+157
+158 Args:
+159 finish (.types.Percentage): fade to this brightness level
+160 start (.types.Percentage): where the brightness should fade from.
+161 If this arg is not specified, the fade will be started from the
+162 current brightness.
+163 interval: the time delay between each step in brightness
+164 increment: the amount to change the brightness by per step
+165 blocking: whether this should occur in the main thread (`True`) or a new daemonic thread (`False`)
+166 force: [*Linux Only*] if False the brightness will never be set lower than 1.
+167 This is because on most displays a brightness of 0 will turn off the backlight.
+168 If True, this check is bypassed
+169 logarithmic: follow a logarithmic brightness curve when adjusting the brightness
+170 stoppable: whether the fade can be stopped by starting a new fade on the same display
+171 **kwargs: passed through to `filter_monitors` for display selection.
+172 Will also be passed to `get_brightness` if `blocking is True`
+173
+174 Returns:
+175 By default, this function calls `get_brightness()` to return the new
+176 brightness of any adjusted displays.
+177
+178 If `blocking` is set to `False`, then a list of threads are
+179 returned, one for each display being faded.
+180
+181 Example:
+182 ```python
+183 import screen_brightness_control as sbc
+184
+185 # fade brightness from the current brightness to 50%
+186 sbc.fade_brightness(50)
+187
+188 # fade the brightness from 25% to 75%
+189 sbc.fade_brightness(75, start=25)
+190
+191 # fade the brightness from the current value to 100% in steps of 10%
+192 sbc.fade_brightness(100, increment=10)
+193
+194 # fade the brightness from 100% to 90% with time intervals of 0.1 seconds
+195 sbc.fade_brightness(90, start=100, interval=0.1)
+196
+197 # fade the brightness to 100% in a new thread
+198 sbc.fade_brightness(100, blocking=False)
+199 ```
+200 '''
+201# make sure only compatible kwargs are passed to filter_monitors
+202available_monitors=filter_monitors(
+203**{k:vfork,vinkwargs.items()ifkin(
+204'display','haystack','method','include','allow_duplicates'
+205)}
+206)
+207
+208threads=[]
+209foriinavailable_monitors:
+210display=Display.from_dict(i)
+211
+212thread=threading.Thread(target=display._fade_brightness,args=(finish,),kwargs={
+213'start':start,
+214'interval':interval,
+215'increment':increment,
+216'force':force,
+217'logarithmic':logarithmic,
+218'stoppable':stoppable
+219})
+220thread.start()
+221threads.append(thread)
+222
+223ifnotblocking:
+224returnthreads
+225
+226fortinthreads:
+227t.join()
+228returnget_brightness(**kwargs)
+
+
+
+
Gradually change the brightness of one or more displays
start (screen_brightness_control.types.Percentage): where the brightness should fade from.
+If this arg is not specified, the fade will be started from the
+current brightness.
+
interval: the time delay between each step in brightness
+
increment: the amount to change the brightness by per step
+
blocking: whether this should occur in the main thread (True) or a new daemonic thread (False)
+
force: [Linux Only] if False the brightness will never be set lower than 1.
+This is because on most displays a brightness of 0 will turn off the backlight.
+If True, this check is bypassed
+
logarithmic: follow a logarithmic brightness curve when adjusting the brightness
+
stoppable: whether the fade can be stopped by starting a new fade on the same display
+
**kwargs: passed through to filter_monitors for display selection.
+Will also be passed to get_brightness if blocking is True
+
+
+
Returns:
+
+
+
By default, this function calls get_brightness() to return the new
+ brightness of any adjusted displays.
+
+
If blocking is set to False, then a list of threads are
+ returned, one for each display being faded.
+
+
+
Example:
+
+
+
+
importscreen_brightness_controlassbc
+
+# fade brightness from the current brightness to 50%
+sbc.fade_brightness(50)
+
+# fade the brightness from 25% to 75%
+sbc.fade_brightness(75,start=25)
+
+# fade the brightness from the current value to 100% in steps of 10%
+sbc.fade_brightness(100,increment=10)
+
+# fade the brightness from 100% to 90% with time intervals of 0.1 seconds
+sbc.fade_brightness(90,start=100,interval=0.1)
+
+# fade the brightness to 100% in a new thread
+sbc.fade_brightness(100,blocking=False)
+
231@config.default_params
+232deflist_monitors_info(
+233method:Optional[str]=None,allow_duplicates:Optional[bool]=None,unsupported:bool=False
+234)->List[dict]:
+235'''
+236 List detailed information about all displays that are controllable by this library
+237
+238 Args:
+239 method: the method to use to list the available displays. See `get_methods` for
+240 more info on available methods
+241 allow_duplicates: controls whether to filter out duplicate displays or not.
+242 unsupported: include detected displays that are invalid or unsupported
+243
+244 Returns:
+245 list: list of dictionaries containing information about the detected displays
+246
+247 Example:
+248 ```python
+249 import screen_brightness_control as sbc
+250 displays = sbc.list_monitors_info()
+251 for display in displays:
+252 print('=======================')
+253 # the manufacturer name plus the model
+254 print('Name:', display['name'])
+255 # the general model of the display
+256 print('Model:', display['model'])
+257 # the serial of the display
+258 print('Serial:', display['serial'])
+259 # the name of the brand of the display
+260 print('Manufacturer:', display['manufacturer'])
+261 # the 3 letter code corresponding to the brand name, EG: BNQ -> BenQ
+262 print('Manufacturer ID:', display['manufacturer_id'])
+263 # the index of that display FOR THE SPECIFIC METHOD THE DISPLAY USES
+264 print('Index:', display['index'])
+265 # the method this display can be addressed by
+266 print('Method:', display['method'])
+267 # the EDID string associated with that display
+268 print('EDID:', display['edid'])
+269 # The UID of the display
+270 print('UID:', display['uid'])
+271 ```
+272 '''
+273return_OS_MODULE.list_monitors_info(
+274method=method,allow_duplicates=allow_duplicates,unsupported=unsupported
+275)
+
+
+
+
List detailed information about all displays that are controllable by this library
+
+
Arguments:
+
+
+
method: the method to use to list the available displays. See get_methods for
+more info on available methods
+
allow_duplicates: controls whether to filter out duplicate displays or not.
+
unsupported: include detected displays that are invalid or unsupported
+
+
+
Returns:
+
+
+
list: list of dictionaries containing information about the detected displays
+
+
+
Example:
+
+
+
+
importscreen_brightness_controlassbc
+displays=sbc.list_monitors_info()
+fordisplayindisplays:
+ print('=======================')
+ # the manufacturer name plus the model
+ 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'])
+ # the EDID string associated with that display
+ print('EDID:',display['edid'])
+ # The UID of the display
+ print('UID:',display['uid'])
+
278@config.default_params
+279deflist_monitors(method:Optional[str]=None,allow_duplicates:Optional[bool]=None)->List[str]:
+280'''
+281 List the names of all detected displays
+282
+283 Args:
+284 method: the method to use to list the available displays. See `get_methods` for
+285 more info on available methods
+286 allow_duplicates: controls whether to filter out duplicate displays or not.
+287
+288 Example:
+289 ```python
+290 import screen_brightness_control as sbc
+291 display_names = sbc.list_monitors()
+292 # eg: ['BenQ GL2450H', 'Dell U2211H']
+293 ```
+294 '''
+295return[i['name']foriinlist_monitors_info(method=method,allow_duplicates=allow_duplicates)]
+
+
+
+
List the names of all detected displays
+
+
Arguments:
+
+
+
method: the method to use to list the available displays. See get_methods for
+more info on available methods
+
allow_duplicates: controls whether to filter out duplicate displays or not.
298defget_methods(name:Optional[str]=None)->Dict[str,Type[BrightnessMethod]]:
+299'''
+300 Returns all available brightness method names and their associated classes.
+301
+302 Args:
+303 name: if specified, return the method corresponding to this name
+304
+305 Raises:
+306 ValueError: if the given name is incorrect
+307
+308 Example:
+309 ```python
+310 import screen_brightness_control as sbc
+311
+312 all_methods = sbc.get_methods()
+313
+314 for method_name, method_class in all_methods.items():
+315 print('Method:', method_name)
+316 print('Class:', method_class)
+317 print('Associated monitors:', sbc.list_monitors(method=method_name))
+318 ```
+319 '''
+320methods:Dict[str,type[BrightnessMethod]]={i.__name__.lower():iforiin_OS_MODULE.METHODS}
+321
+322ifnameisNone:
+323returnmethods
+324
+325ifnotisinstance(name,str):
+326raiseTypeError(f'name must be of type str, not {type(name)!r}')
+327
+328name=name.lower()
+329ifnameinmethods:
+330return{name:methods[name]}
+331
+332_logger.debug(f'requested method {name!r} invalid')
+333raiseValueError(
+334f'invalid method {name!r}, must be one of: {list(methods)}')
+
+
+
+
Returns all available brightness method names and their associated classes.
+
+
Arguments:
+
+
+
name: if specified, return the method corresponding to this name
337@dataclass
+338classDisplay():
+339'''
+340 Represents a single connected display.
+341 '''
+342index:int
+343'''The index of the display relative to the method it uses.
+344 So if the index is 0 and the method is `windows.VCP`, then this is the 1st
+345 display reported by `windows.VCP`, not the first display overall.'''
+346method:Type[BrightnessMethod]
+347'''The method by which this monitor can be addressed.
+348 This will be a class from either the windows or linux sub-module'''
+349
+350uid:Optional[str]=None
+351'''A unique identifier for the display. This is usually inferred from the display's connection to the machine.'''
+352edid:Optional[str]=None
+353'''A 256 character hex string containing information about a display and its capabilities'''
+354manufacturer:Optional[str]=None
+355'''Name of the display's manufacturer'''
+356manufacturer_id:Optional[str]=None
+357'''3 letter code corresponding to the manufacturer name'''
+358model:Optional[str]=None
+359'''Model name of the display'''
+360name:Optional[str]=None
+361'''The name of the display, often the manufacturer name plus the model name'''
+362serial:Optional[str]=None
+363'''The serial number of the display or (if serial is not available) an ID assigned by the OS'''
+364
+365_logger:logging.Logger=field(init=False,repr=False)
+366_fade_thread_dict:ClassVar[Dict[FrozenSet[Any],threading.Thread]]={}
+367'''A dictionary mapping display identifiers to latest fade threads for stopping fades.'''
+368
+369def__post_init__(self):
+370self._logger=_logger.getChild(self.__class__.__name__).getChild(
+371str(self.get_identifier()[1])[:20])
+372
+373deffade_brightness(
+374self,
+375finish:Percentage,
+376start:Optional[Percentage]=None,
+377interval:float=0.01,
+378increment:int=1,
+379force:bool=False,
+380logarithmic:bool=True,
+381blocking:bool=True,
+382stoppable:bool=True
+383)->Optional[threading.Thread]:
+384'''
+385 Gradually change the brightness of this display to a set value.
+386 Can execute in the current thread, blocking until completion,
+387 or in a separate thread, allowing concurrent operations.
+388 When set as non-blocking and stoppable, a new fade can halt the this operation.
+389
+390 Args:
+391 finish (.types.Percentage): the brightness level to end up on
+392 start (.types.Percentage): where the fade should start from. Defaults
+393 to whatever the current brightness level for the display is
+394 interval: time delay between each change in brightness
+395 increment: amount to change the brightness by each time (as a percentage)
+396 force: [*Linux only*] allow the brightness to be set to 0. By default,
+397 brightness values will never be set lower than 1, since setting them to 0
+398 often turns off the backlight
+399 logarithmic: follow a logarithmic curve when setting brightness values.
+400 See `logarithmic_range` for rationale
+401 blocking: run this function in the current thread and block until it completes
+402 stoppable: whether this fade will be stopped by starting a new fade on the same display
+403
+404 Returns:
+405 If `blocking` is `False`, returns a `threading.Thread` object representing the
+406 thread in which the fade operation is running.
+407 Otherwise, it returns None.
+408 '''
+409thread=threading.Thread(target=self._fade_brightness,args=(finish,),kwargs={
+410'start':start,
+411'interval':interval,
+412'increment':increment,
+413'force':force,
+414'logarithmic':logarithmic,
+415'stoppable':stoppable
+416})
+417thread.start()
+418
+419ifnotblocking:
+420returnthread
+421else:
+422thread.join()
+423returnNone
+424
+425def_fade_brightness(
+426self,
+427finish:Percentage,
+428start:Optional[Percentage]=None,
+429interval:float=0.01,
+430increment:int=1,
+431force:bool=False,
+432logarithmic:bool=True,
+433stoppable:bool=True
+434)->None:
+435# Record the latest thread for this display so that other stoppable threads can be stopped
+436display_key=frozenset((self.method,self.index))
+437self._fade_thread_dict[display_key]=threading.current_thread()
+438# minimum brightness value
+439ifplatform.system()=='Linux'andnotforce:
+440lower_bound=1
+441else:
+442lower_bound=0
+443
+444current=self.get_brightness()
+445
+446finish=percentage(finish,current,lower_bound)
+447start=percentage(
+448currentifstartisNoneelsestart,current,lower_bound)
+449
+450# mypy says "object is not callable" but range is. Ignore this
+451range_func:Callable=logarithmic_rangeiflogarithmicelserange# type: ignore[assignment]
+452increment=abs(increment)
+453ifstart>finish:
+454increment=-increment
+455
+456self._logger.debug(
+457f'fade {start}->{finish}:{increment}:logarithmic={logarithmic}')
+458
+459# Record the time when the next brightness change should start
+460next_change_start_time=time.time()
+461forvalueinrange_func(start,finish,increment):
+462ifstoppableandthreading.current_thread()!=self._fade_thread_dict[display_key]:
+463# If the current thread is stoppable and it's not the latest thread, stop fading
+464break
+465# `value` is ensured not to hit `finish` in loop, this will be handled in the final step.
+466self.set_brightness(value,force=force)
+467
+468# `interval` is the intended time between the start of each brightness change.
+469next_change_start_time+=interval
+470sleep_time=next_change_start_time-time.time()
+471# Skip sleep if the scheduled time has already passed
+472ifsleep_time>0:
+473time.sleep(sleep_time)
+474else:
+475# As `value` doesn't hit `finish` in loop, we explicitly set brightness to `finish`.
+476# This also avoids an unnecessary sleep in the last iteration.
+477ifnotstoppableorthreading.current_thread()==self._fade_thread_dict[display_key]:
+478self.set_brightness(finish,force=force)
+479
+480@classmethod
+481deffrom_dict(cls,display:dict)->'Display':
+482'''
+483 Initialise an instance of the class from a dictionary, ignoring
+484 any unwanted keys
+485 '''
+486returncls(
+487index=display['index'],
+488method=display['method'],
+489edid=display['edid'],
+490manufacturer=display['manufacturer'],
+491manufacturer_id=display['manufacturer_id'],
+492model=display['model'],
+493name=display['name'],
+494serial=display['serial'],
+495uid=display.get('uid')
+496)
+497
+498defget_brightness(self)->IntPercentage:
+499'''
+500 Returns the brightness of this display.
+501
+502 Returns:
+503 The brightness value of the display, as a percentage.
+504 See `.types.IntPercentage`
+505 '''
+506returnself.method.get_brightness(display=self.index)[0]
+507
+508defget_identifier(self)->Tuple[str,DisplayIdentifier]:
+509'''
+510 Returns the `.types.DisplayIdentifier` for this display.
+511 Will iterate through the UID, EDID, serial, name and index and return the first
+512 value that is not equal to None
+513
+514 Returns:
+515 The name of the property returned and the value of said property.
+516 EG: `('serial', '123abc...')` or `('name', 'BenQ GL2450H')`
+517 '''
+518forkeyin('uid','edid','serial','name'):
+519value=getattr(self,key,None)
+520ifvalueisnotNone:
+521returnkey,value
+522# the index should surely never be `None`
+523return'index',self.index
+524
+525defis_active(self)->bool:
+526'''
+527 Attempts to retrieve the brightness for this display. If it works the display is deemed active
+528 '''
+529try:
+530self.get_brightness()
+531returnTrue
+532exceptExceptionase:
+533self._logger.error(
+534f'Display.is_active: {self.get_identifier()} failed get_brightness call'
+535f' - {format_exc(e)}'
+536)
+537returnFalse
+538
+539defset_brightness(self,value:Percentage,force:bool=False):
+540'''
+541 Sets the brightness for this display. See `set_brightness` for the full docs
+542
+543 Args:
+544 value (.types.Percentage): the brightness percentage to set the display to
+545 force: allow the brightness to be set to 0 on Linux. This is disabled by default
+546 because setting the brightness of 0 will often turn off the backlight
+547 '''
+548# convert brightness value to percentage
+549ifplatform.system()=='Linux'andnotforce:
+550lower_bound=1
+551else:
+552lower_bound=0
+553
+554value=percentage(
+555value,
+556current=self.get_brightness,
+557lower_bound=lower_bound
+558)
+559
+560self.method.set_brightness(value,display=self.index)
+
The index of the display relative to the method it uses.
+So if the index is 0 and the method is windows.VCP, then this is the 1st
+display reported by windows.VCP, not the first display overall.
373deffade_brightness(
+374self,
+375finish:Percentage,
+376start:Optional[Percentage]=None,
+377interval:float=0.01,
+378increment:int=1,
+379force:bool=False,
+380logarithmic:bool=True,
+381blocking:bool=True,
+382stoppable:bool=True
+383)->Optional[threading.Thread]:
+384'''
+385 Gradually change the brightness of this display to a set value.
+386 Can execute in the current thread, blocking until completion,
+387 or in a separate thread, allowing concurrent operations.
+388 When set as non-blocking and stoppable, a new fade can halt the this operation.
+389
+390 Args:
+391 finish (.types.Percentage): the brightness level to end up on
+392 start (.types.Percentage): where the fade should start from. Defaults
+393 to whatever the current brightness level for the display is
+394 interval: time delay between each change in brightness
+395 increment: amount to change the brightness by each time (as a percentage)
+396 force: [*Linux only*] allow the brightness to be set to 0. By default,
+397 brightness values will never be set lower than 1, since setting them to 0
+398 often turns off the backlight
+399 logarithmic: follow a logarithmic curve when setting brightness values.
+400 See `logarithmic_range` for rationale
+401 blocking: run this function in the current thread and block until it completes
+402 stoppable: whether this fade will be stopped by starting a new fade on the same display
+403
+404 Returns:
+405 If `blocking` is `False`, returns a `threading.Thread` object representing the
+406 thread in which the fade operation is running.
+407 Otherwise, it returns None.
+408 '''
+409thread=threading.Thread(target=self._fade_brightness,args=(finish,),kwargs={
+410'start':start,
+411'interval':interval,
+412'increment':increment,
+413'force':force,
+414'logarithmic':logarithmic,
+415'stoppable':stoppable
+416})
+417thread.start()
+418
+419ifnotblocking:
+420returnthread
+421else:
+422thread.join()
+423returnNone
+
+
+
+
Gradually change the brightness of this display to a set value.
+Can execute in the current thread, blocking until completion,
+or in a separate thread, allowing concurrent operations.
+When set as non-blocking and stoppable, a new fade can halt the this operation.
interval: time delay between each change in brightness
+
increment: amount to change the brightness by each time (as a percentage)
+
force: [Linux only] allow the brightness to be set to 0. By default,
+brightness values will never be set lower than 1, since setting them to 0
+often turns off the backlight
+
logarithmic: follow a logarithmic curve when setting brightness values.
+See logarithmic_range for rationale
+
blocking: run this function in the current thread and block until it completes
+
stoppable: whether this fade will be stopped by starting a new fade on the same display
+
+
+
Returns:
+
+
+
If blocking is False, returns a threading.Thread object representing the
+ thread in which the fade operation is running.
+ Otherwise, it returns None.
480@classmethod
+481deffrom_dict(cls,display:dict)->'Display':
+482'''
+483 Initialise an instance of the class from a dictionary, ignoring
+484 any unwanted keys
+485 '''
+486returncls(
+487index=display['index'],
+488method=display['method'],
+489edid=display['edid'],
+490manufacturer=display['manufacturer'],
+491manufacturer_id=display['manufacturer_id'],
+492model=display['model'],
+493name=display['name'],
+494serial=display['serial'],
+495uid=display.get('uid')
+496)
+
+
+
+
Initialise an instance of the class from a dictionary, ignoring
+any unwanted keys
+
+
+
+
+
+
+
+
+ def
+ get_brightness(self) -> int:
+
+
+
+
+
+
498defget_brightness(self)->IntPercentage:
+499'''
+500 Returns the brightness of this display.
+501
+502 Returns:
+503 The brightness value of the display, as a percentage.
+504 See `.types.IntPercentage`
+505 '''
+506returnself.method.get_brightness(display=self.index)[0]
+
508defget_identifier(self)->Tuple[str,DisplayIdentifier]:
+509'''
+510 Returns the `.types.DisplayIdentifier` for this display.
+511 Will iterate through the UID, EDID, serial, name and index and return the first
+512 value that is not equal to None
+513
+514 Returns:
+515 The name of the property returned and the value of said property.
+516 EG: `('serial', '123abc...')` or `('name', 'BenQ GL2450H')`
+517 '''
+518forkeyin('uid','edid','serial','name'):
+519value=getattr(self,key,None)
+520ifvalueisnotNone:
+521returnkey,value
+522# the index should surely never be `None`
+523return'index',self.index
+
The name of the property returned and the value of said property.
+ EG: ('serial', '123abc...') or ('name', 'BenQ GL2450H')
+
+
+
+
+
+
+
+
+
+ def
+ is_active(self) -> bool:
+
+
+
+
+
+
525defis_active(self)->bool:
+526'''
+527 Attempts to retrieve the brightness for this display. If it works the display is deemed active
+528 '''
+529try:
+530self.get_brightness()
+531returnTrue
+532exceptExceptionase:
+533self._logger.error(
+534f'Display.is_active: {self.get_identifier()} failed get_brightness call'
+535f' - {format_exc(e)}'
+536)
+537returnFalse
+
+
+
+
Attempts to retrieve the brightness for this display. If it works the display is deemed active
539defset_brightness(self,value:Percentage,force:bool=False):
+540'''
+541 Sets the brightness for this display. See `set_brightness` for the full docs
+542
+543 Args:
+544 value (.types.Percentage): the brightness percentage to set the display to
+545 force: allow the brightness to be set to 0 on Linux. This is disabled by default
+546 because setting the brightness of 0 will often turn off the backlight
+547 '''
+548# convert brightness value to percentage
+549ifplatform.system()=='Linux'andnotforce:
+550lower_bound=1
+551else:
+552lower_bound=0
+553
+554value=percentage(
+555value,
+556current=self.get_brightness,
+557lower_bound=lower_bound
+558)
+559
+560self.method.set_brightness(value,display=self.index)
+
+
+
+
Sets the brightness for this display. See set_brightness for the full docs
force: allow the brightness to be set to 0 on Linux. This is disabled by default
+because setting the brightness of 0 will often turn off the backlight
563@config.default_params
+564deffilter_monitors(
+565display:Optional[DisplayIdentifier]=None,
+566haystack:Optional[List[dict]]=None,
+567method:Optional[str]=None,
+568include:List[str]=[],
+569allow_duplicates:Optional[bool]=None
+570)->List[dict]:
+571'''
+572 Searches through the information for all detected displays
+573 and attempts to return the info matching the value given.
+574 Will attempt to match against index, name, edid, method and serial
+575
+576 Args:
+577 display (.types.DisplayIdentifier): the display you are searching for
+578 haystack: the information to filter from.
+579 If this isn't set it defaults to the return of `list_monitors_info`
+580 method: the method the monitors use. See `get_methods` for
+581 more info on available methods
+582 include: extra fields of information to sort by
+583 allow_duplicates: controls whether to filter out duplicate displays or not
+584
+585 Raises:
+586 NoValidDisplayError: if the display does not have a match
+587 TypeError: if the `display` kwarg is not `int` or `str`
+588
+589 Example:
+590 ```python
+591 import screen_brightness_control as sbc
+592
+593 search = 'GL2450H'
+594 match = sbc.filter_displays(search)
+595 print(match)
+596 # EG output: [{'name': 'BenQ GL2450H', 'model': 'GL2450H', ... }]
+597 ```
+598 '''
+599ifdisplayisnotNoneandtype(display)notin(str,int):
+600raiseTypeError(
+601f'display kwarg must be int or str, not "{type(display).__name__}"')
+602
+603defget_monitor_list():
+604# if we have been provided with a list of monitors to sift through then use that
+605# otherwise, get the info ourselves
+606ifhaystackisnotNone:
+607monitors_with_duplicates=haystack
+608ifmethodisnotNone:
+609method_class=next(iter(get_methods(method).values()))
+610monitors_with_duplicates=[
+611iforiinhaystackifi['method']==method_class]
+612else:
+613monitors_with_duplicates=list_monitors_info(
+614method=method,allow_duplicates=True)
+615
+616returnmonitors_with_duplicates
+617
+618deffilter_monitor_list(to_filter):
+619# This loop does two things:
+620# 1. Filters out duplicate monitors
+621# 2. Matches the display kwarg (if applicable)
+622
+623# When duplicates are allowed, the logic is straightforward:
+624ifallow_duplicates:
+625ifdisplayisNone:
+626# no monitor should be filtered out
+627returnto_filter
+628elifisinstance(display,int):
+629# 'display' variable should be the index of the monitor
+630# return a list with the monitor at the index or an empty list if the index is out of range
+631returnto_filter[display:display+1]
+632elifisinstance(display,str):
+633# 'display' variable should be an identifier of the monitor
+634# multiple monitors with the same identifier are allowed here, so return all of them
+635monitors=[]
+636formonitorinto_filter:
+637foridentifierin['uid','edid','serial','name']+include:
+638ifdisplay==monitor.get(identifier,None):
+639monitors.append(monitor)
+640break
+641returnmonitors
+642
+643filtered_displays={}
+644formonitorinto_filter:
+645# find a valid identifier for a monitor, excluding any which are equal to None
+646added=False
+647foridentifierin['uid','edid','serial','name']+include:
+648ifmonitor.get(identifier,None)isNone:
+649continue
+650
+651m_id=monitor[identifier]
+652ifm_idinfiltered_displays:
+653break
+654
+655ifisinstance(display,str)andm_id!=display:
+656continue
+657
+658# check we haven't already added the monitor
+659ifnotadded:
+660filtered_displays[m_id]=monitor
+661added=True
+662
+663# if the display kwarg is an integer and we are currently at that index
+664ifisinstance(display,int)andlen(filtered_displays)-1==display:
+665return[monitor]
+666
+667ifadded:
+668break
+669returnlist(filtered_displays.values())
+670
+671duplicates=[]
+672for_inrange(3):
+673duplicates=get_monitor_list()
+674ifduplicates:
+675break
+676time.sleep(0.4)
+677else:
+678msg='no displays detected'
+679ifmethodisnotNone:
+680msg+=f' with method: {method!r}'
+681raiseNoValidDisplayError(msg)
+682
+683monitors=filter_monitor_list(duplicates)
+684ifnotmonitors:
+685# if no displays matched the query
+686msg='no displays found'
+687ifdisplayisnotNone:
+688msg+=f' with name/serial/edid/index of {display!r}'
+689ifmethodisnotNone:
+690msg+=f' with method of {method!r}'
+691raiseNoValidDisplayError(msg)
+692
+693returnmonitors
+
+
+
+
Searches through the information for all detected displays
+and attempts to return the info matching the value given.
+Will attempt to match against index, name, edid, method and serial
1'''
+ 2Helper functions for the library
+ 3'''
+ 4from__future__importannotations
+ 5
+ 6importlogging
+ 7importstruct
+ 8importsubprocess
+ 9importtime
+ 10fromabcimportABC,abstractmethod
+ 11fromfunctoolsimportlru_cache
+ 12fromtypingimportAny,Callable,Dict,List,Optional,Tuple,Union
+ 13
+ 14from.exceptionsimport(EDIDParseError,MaxRetriesExceededError,# noqa:F401
+ 15ScreenBrightnessError,format_exc)
+ 16from.typesimportDisplayIdentifier,IntPercentage,Percentage,Generator
+ 17
+ 18_logger=logging.getLogger(__name__)
+ 19
+ 20MONITOR_MANUFACTURER_CODES={
+ 21"AAC":"AcerView",
+ 22"ACI":"Asus (ASUSTeK Computer Inc.)",
+ 23"ACR":"Acer",
+ 24"ACT":"Targa",
+ 25"ADI":"ADI Corporation",
+ 26"AIC":"AG Neovo",
+ 27"ALX":"Anrecson",
+ 28"AMW":"AMW",
+ 29"AOC":"AOC",
+ 30"API":"Acer America Corp.",
+ 31"APP":"Apple Computer",
+ 32"ART":"ArtMedia",
+ 33"AST":"AST Research",
+ 34"AUO":"Asus",
+ 35"BMM":"BMM",
+ 36"BNQ":"BenQ",
+ 37"BOE":"BOE Display Technology",
+ 38"CMO":"Acer",
+ 39"CPL":"Compal",
+ 40"CPQ":"Compaq",
+ 41"CPT":"Chunghwa Picture Tubes, Ltd.",
+ 42"CTX":"CTX",
+ 43"DEC":"DEC",
+ 44"DEL":"Dell",
+ 45"DPC":"Delta",
+ 46"DWE":"Daewoo",
+ 47"ECS":"ELITEGROUP Computer Systems",
+ 48"EIZ":"EIZO",
+ 49"ELS":"ELSA",
+ 50"ENC":"EIZO",
+ 51"EPI":"Envision",
+ 52"FCM":"Funai",
+ 53"FUJ":"Fujitsu",
+ 54"FUS":"Fujitsu-Siemens",
+ 55"GSM":"LG Electronics",
+ 56"GWY":"Gateway 2000",
+ 57"GBT":"Gigabyte",
+ 58"HEI":"Hyundai",
+ 59"HIQ":"Hyundai ImageQuest",
+ 60"HIT":"Hyundai",
+ 61"HPN":"HP",
+ 62"HSD":"Hannspree Inc",
+ 63"HSL":"Hansol",
+ 64"HTC":"Hitachi/Nissei",
+ 65"HWP":"HP",
+ 66"IBM":"IBM",
+ 67"ICL":"Fujitsu ICL",
+ 68"IFS":"InFocus",
+ 69"IQT":"Hyundai",
+ 70"IVM":"Iiyama",
+ 71"KDS":"Korea Data Systems",
+ 72"KFC":"KFC Computek",
+ 73"LEN":"Lenovo",
+ 74"LGD":"Asus",
+ 75"LKM":"ADLAS / AZALEA",
+ 76"LNK":"LINK Technologies, Inc.",
+ 77"LPL":"Fujitsu",
+ 78"LTN":"Lite-On",
+ 79"MAG":"MAG InnoVision",
+ 80"MAX":"Belinea",
+ 81"MEI":"Panasonic",
+ 82"MEL":"Mitsubishi Electronics",
+ 83"MIR":"miro Computer Products AG",
+ 84"MSI":"MSI",
+ 85"MS_":"Panasonic",
+ 86"MTC":"MITAC",
+ 87"NAN":"Nanao",
+ 88"NEC":"NEC",
+ 89"NOK":"Nokia Data",
+ 90"NVD":"Fujitsu",
+ 91"OPT":"Optoma",
+ 92"OQI":"OPTIQUEST",
+ 93"PBN":"Packard Bell",
+ 94"PCK":"Daewoo",
+ 95"PDC":"Polaroid",
+ 96"PGS":"Princeton Graphic Systems",
+ 97"PHL":"Philips",
+ 98"PRT":"Princeton",
+ 99"REL":"Relisys",
+100"SAM":"Samsung",
+101"SAN":"Samsung",
+102"SBI":"Smarttech",
+103"SEC":"Hewlett-Packard",
+104"SGI":"SGI",
+105"SMC":"Samtron",
+106"SMI":"Smile",
+107"SNI":"Siemens Nixdorf",
+108"SNY":"Sony",
+109"SPT":"Sceptre",
+110"SRC":"Shamrock",
+111"STN":"Samtron",
+112"STP":"Sceptre",
+113"SUN":"Sun Microsystems",
+114"TAT":"Tatung",
+115"TOS":"Toshiba",
+116"TRL":"Royal Information Company",
+117"TSB":"Toshiba",
+118"UNK":"Unknown",
+119"UNM":"Unisys Corporation",
+120"VSC":"ViewSonic",
+121"WTC":"Wen Technology",
+122"ZCM":"Zenith",
+123"_YV":"Fujitsu"
+124}
+125
+126
+127classBrightnessMethod(ABC):
+128@classmethod
+129@abstractmethod
+130defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+131'''
+132 Return information about detected displays.
+133
+134 Args:
+135 display (.types.DisplayIdentifier): the specific display to return
+136 information about. This parameter is passed to `filter_monitors`
+137
+138 Returns:
+139 A list of dictionaries, each representing a detected display.
+140 Each returned dictionary will have the following keys:
+141 - name (`str`): the name of the display
+142 - model (`str`): the model of the display
+143 - manufacturer (`str`): the name of the display manufacturer
+144 - manufacturer_id (`str`): the three letter manufacturer code (see `MONITOR_MANUFACTURER_CODES`)
+145 - serial (`str`): the serial of the display OR some other unique identifier
+146 - edid (`str`): the EDID string for the display
+147 - method (`BrightnessMethod`): the brightness method associated with this display
+148 - index (`int`): the index of the display, relative to the brightness method
+149 '''
+150...
+151
+152@classmethod
+153@abstractmethod
+154defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+155'''
+156 Args:
+157 display: the index of the specific display to query.
+158 If unspecified, all detected displays are queried
+159
+160 Returns:
+161 A list of `.types.IntPercentage` values, one for each
+162 queried display
+163 '''
+164...
+165
+166@classmethod
+167@abstractmethod
+168defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+169'''
+170 Args:
+171 value (.types.IntPercentage): the new brightness value
+172 display: the index of the specific display to adjust.
+173 If unspecified, all detected displays are adjusted
+174 '''
+175...
+176
+177
+178classBrightnessMethodAdv(BrightnessMethod):
+179@classmethod
+180@abstractmethod
+181def_gdi(cls)->List[dict]:
+182'''
+183 Similar to `BrightnessMethod.get_display_info` except this method will also
+184 return unsupported displays, indicated by an `unsupported: bool` property
+185 in the returned dict
+186 '''
+187...
+188
+189
+190class__Cache:
+191'''class to cache data with a short shelf life'''
+192
+193def__init__(self):
+194self.logger=_logger.getChild(f'{self.__class__.__name__}_{id(self)}')
+195self.enabled=True
+196self._store:Dict[str,Tuple[Any,float]]={}
+197
+198defexpire(self,key:Optional[str]=None,startswith:Optional[str]=None):
+199'''
+200 @private
+201
+202 Runs through all keys in the cache and removes any expired items.
+203 Can optionally specify additional keys that should be removed.
+204
+205 Args:
+206 key: a specific key to remove. `KeyError` exceptions are suppressed if this key doesn't exist.
+207 startswith: remove any keys that start with this string
+208 '''
+209ifkeyisnotNone:
+210try:
+211delself._store[key]
+212self.logger.debug(f'delete key {key!r}')
+213exceptKeyError:
+214pass
+215
+216fork,vintuple(self._store.items()):
+217ifstartswithisnotNoneandk.startswith(startswith):
+218delself._store[k]
+219self.logger.debug(f'delete keys {startswith=}')
+220continue
+221ifv[1]<time.time():
+222delself._store[k]
+223self.logger.debug(f'delete expired key {k}')
+224
+225defget(self,key:str)->Any:
+226ifnotself.enabled:
+227returnNone
+228self.expire()
+229ifkeynotinself._store:
+230self.logger.debug(f'{key!r} not present in cache')
+231returnNone
+232returnself._store[key][0]
+233
+234defstore(self,key:str,value:Any,expires:float=1):
+235ifnotself.enabled:
+236return
+237self.logger.debug(f'cache set {key!r}, {expires=}')
+238self._store[key]=(value,expires+time.time())
+239
+240
+241classEDID:
+242'''
+243 Simple structure and method to extract display serial and name from an EDID string.
+244 '''
+245EDID_FORMAT:str=(
+246">"# big-endian
+247"8s"# constant header (8 bytes)
+248"H"# manufacturer id (2 bytes)
+249"H"# product id (2 bytes)
+250"I"# serial number (4 bytes)
+251"B"# manufactoring week (1 byte)
+252"B"# manufactoring year (1 byte)
+253"B"# edid version (1 byte)
+254"B"# edid revision (1 byte)
+255"B"# video input type (1 byte)
+256"B"# horizontal size in cm (1 byte)
+257"B"# vertical size in cm (1 byte)
+258"B"# display gamma (1 byte)
+259"B"# supported features (1 byte)
+260"10s"# colour characteristics (10 bytes)
+261"H"# supported timings (2 bytes)
+262"B"# reserved timing (1 byte)
+263"16s"# EDID supported timings (16 bytes)
+264"18s"# timing / display descriptor block 1 (18 bytes)
+265"18s"# timing / display descriptor block 2 (18 bytes)
+266"18s"# timing / display descriptor block 3 (18 bytes)
+267"18s"# timing / display descriptor block 4 (18 bytes)
+268"B"# extension flag (1 byte)
+269"B"# checksum (1 byte)
+270)
+271'''
+272 The byte structure for EDID strings, taken from
+273 [pyedid](https://github.com/jojonas/pyedid/blob/2382910d968b2fa8de1fab495fbbdfebcdb39f19/pyedid/edid.py#L21),
+274 [Copyright 2019-2020 Jonas Lieb, Davydov Denis](https://github.com/jojonas/pyedid/blob/master/LICENSE).
+275 '''
+276SERIAL_DESCRIPTOR=bytes.fromhex('00 00 00 ff 00')
+277NAME_DESCRIPTOR=bytes.fromhex('00 00 00 fc 00')
+278
+279@classmethod
+280defparse(cls,edid:Union[bytes,str])->Tuple[Union[str,None],...]:
+281'''
+282 Takes an EDID string and parses some relevant information from it according to the
+283 [EDID 1.4](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format)
+284 specification on Wikipedia.
+285
+286 Args:
+287 edid (bytes or str): the EDID, can either be raw bytes or
+288 a hex formatted string (00 ff ff ff ff...)
+289
+290 Returns:
+291 tuple[str | None]: A tuple of 5 items representing the display's manufacturer ID,
+292 manufacturer, model, name, serial in that order.
+293 If any of these values are unable to be determined, they will be None.
+294 Otherwise, expect a string
+295
+296 Raises:
+297 EDIDParseError: if the EDID info cannot be unpacked
+298 TypeError: if `edid` is not `str` or `bytes`
+299
+300 Example:
+301 ```python
+302 import screen_brightness_control as sbc
+303
+304 edid = sbc.list_monitors_info()[0]['edid']
+305 manufacturer_id, manufacturer, model, name, serial = sbc.EDID.parse(edid)
+306
+307 print('Manufacturer:', manufacturer_id or 'Unknown')
+308 print('Model:', model or 'Unknown')
+309 print('Name:', name or 'Unknown')
+310 ```
+311 '''
+312# see https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format
+313ifisinstance(edid,str):
+314edid=bytes.fromhex(edid)
+315elifnotisinstance(edid,bytes):
+316raiseTypeError(f'edid must be of type bytes or str, not {type(edid)!r}')
+317
+318try:
+319blocks=struct.unpack(cls.EDID_FORMAT,edid)
+320exceptstruct.errorase:
+321raiseEDIDParseError('cannot unpack edid')frome
+322
+323# split mfg_id (2 bytes) into 3 letters, 5 bits each (ignoring reserved bit)
+324mfg_id_chars=(
+325blocks[1]>>10,# First 6 bits (reserved bit at start is always 0)
+326(blocks[1]>>5)&0b11111,# isolate next 5 bits from first 11 using bitwise AND
+327blocks[1]&0b11111# Last five bits
+328)
+329# turn numbers into ascii
+330mfg_id=''.join(chr(i+64)foriinmfg_id_chars)
+331
+332# now grab the manufacturer name
+333mfg_lookup=_monitor_brand_lookup(mfg_id)
+334ifmfg_lookupisnotNone:
+335manufacturer=mfg_lookup[1]
+336else:
+337manufacturer=None
+338
+339serial=None
+340name=None
+341fordescriptor_blockinblocks[17:21]:
+342# decode the serial
+343ifdescriptor_block.startswith(cls.SERIAL_DESCRIPTOR):
+344# strip descriptor bytes and trailing whitespace
+345serial_bytes=descriptor_block[len(cls.SERIAL_DESCRIPTOR):].rstrip()
+346serial=serial_bytes.decode()
+347
+348# decode the monitor name
+349elifdescriptor_block.startswith(cls.NAME_DESCRIPTOR):
+350# strip descriptor bytes and trailing whitespace
+351name_bytes=descriptor_block[len(cls.NAME_DESCRIPTOR):].rstrip()
+352name=name_bytes.decode()
+353
+354# now try to figure out what model the display is
+355model=None
+356ifnameisnotNone:
+357ifmanufacturerisnotNoneandname.startswith(manufacturer):
+358# eg: 'BenQ GL2450H' -> 'GL2450H'
+359model=name.replace(manufacturer,'',1).strip()
+360
+361# if previous method did not work (or if we don't know the manufacturer),
+362# try taking last word of name
+363ifnotmodel:
+364try:
+365# eg: 'BenQ GL2450H' -> ['BenQ', 'GL2450H']
+366model=name.strip().rsplit(' ',1)[1]
+367exceptIndexError:
+368# If the name does not include model information then
+369# give it something generic
+370model='Generic Monitor'
+371
+372returnmfg_id,manufacturer,model,name,serial
+373
+374@staticmethod
+375defhexdump(file:str)->str:
+376'''
+377 Returns a hexadecimal string of binary data from a file
+378
+379 Args:
+380 file (str): the file to read
+381
+382 Returns:
+383 str: one long hex string
+384
+385 Example:
+386 ```python
+387 from screen_brightness_control import EDID
+388
+389 print(EDID.hexdump('/sys/class/backlight/intel_backlight/device/edid'))
+390 # '00ffffffffffff00...'
+391 ```
+392 '''
+393withopen(file,'rb')asf:
+394hex_str=''.join(f'{char:02x}'forcharinf.read())
+395
+396returnhex_str
+397
+398
+399defcheck_output(command:List[str],max_tries:int=1)->bytes:
+400'''
+401 Run a command with retry management built in.
+402
+403 Args:
+404 command: the command to run
+405 max_tries: the maximum number of retries to allow before raising an error
+406
+407 Returns:
+408 The output from the command
+409 '''
+410tries=1
+411whileTrue:
+412try:
+413output=subprocess.check_output(command,stderr=subprocess.PIPE)
+414exceptsubprocess.CalledProcessErrorase:
+415iftries>=max_tries:
+416raiseMaxRetriesExceededError(f'process failed after {tries} tries',e)frome
+417tries+=1
+418time.sleep(0.04iftries<5else0.5)
+419else:
+420iftries>1:
+421_logger.debug(f'command {command} took {tries}/{max_tries} tries')
+422returnoutput
+423
+424
+425deflogarithmic_range(start:int,stop:int,step:int=1)->Generator[int,None,None]:
+426'''
+427 A `range`-like function that yields a sequence of integers following
+428 a logarithmic curve (`y = 10 ^ (x / 50)`) from `start` (inclusive) to
+429 `stop` (inclusive).
+430
+431 This is useful because it skips many of the higher percentages in the
+432 sequence where single percent brightness changes are hard to notice.
+433
+434 This function is designed to deal with brightness percentages, and so
+435 will never return a value less than 0 or greater than 100.
+436
+437 Args:
+438 start: the start of your percentage range
+439 stop: the end of your percentage range
+440 step: the increment per iteration through the sequence
+441
+442 Yields:
+443 int
+444 '''
+445start=int(max(0,start))
+446stop=int(min(100,stop))
+447
+448ifstart==stoporabs(stop-start)<=1:
+449yieldstart
+450else:
+451value_range=stop-start
+452
+453defdirection(x):
+454returnxifstep>0else100-x
+455
+456last_yielded=None
+457x:float
+458forxinrange(start,stop+1,step):
+459# get difference from base point
+460x-=start
+461# calculate progress through our range as a percentage
+462x=(x/value_range)*100
+463# convert along logarithmic curve (inverse of y = 50log(x)) to another percentage
+464x=10**(direction(x)/50)
+465# apply this percentage to our range and add back starting offset
+466x=int(((direction(x)/100)*value_range)+start)
+467
+468ifx==last_yielded:
+469continue
+470yieldx
+471last_yielded=x
+472
+473
+474@lru_cache(maxsize=None)
+475def_monitor_brand_lookup(search:str)->Union[Tuple[str,str],None]:
+476'''internal function to search the monitor manufacturer codes dict'''
+477keys=tuple(MONITOR_MANUFACTURER_CODES.keys())
+478keys_lower=tuple(map(str.lower,keys))
+479values=tuple(MONITOR_MANUFACTURER_CODES.values())
+480search=search.lower()
+481
+482ifsearchinkeys_lower:
+483index=keys_lower.index(search)
+484else:
+485values_lower=tuple(map(str.lower,values))
+486ifsearchinvalues_lower:
+487index=values_lower.index(search)
+488else:
+489returnNone
+490returnkeys[index],values[index]
+491
+492
+493defpercentage(
+494value:Percentage,
+495current:Optional[Union[int,Callable[[],int]]]=None,
+496lower_bound:int=0
+497)->IntPercentage:
+498'''
+499 Convenience function to convert a brightness value into a percentage. Can handle
+500 integers, floats and strings. Also can handle relative strings (eg: `'+10'` or `'-10'`)
+501
+502 Args:
+503 value: the brightness value to convert
+504 current: the current brightness value or a function that returns the current brightness
+505 value. Used when dealing with relative brightness values
+506 lower_bound: the minimum value the brightness can be set to
+507
+508 Returns:
+509 `.types.IntPercentage`: The new brightness percentage, between `lower_bound` and 100
+510 '''
+511ifisinstance(value,str)and('+'invalueor'-'invalue):
+512ifcallable(current):
+513current=current()
+514value=int(float(value))+int(float(str(current)))
+515else:
+516value=int(float(str(value)))
+517
+518returnmin(100,max(lower_bound,value))
+
128classBrightnessMethod(ABC):
+129@classmethod
+130@abstractmethod
+131defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+132'''
+133 Return information about detected displays.
+134
+135 Args:
+136 display (.types.DisplayIdentifier): the specific display to return
+137 information about. This parameter is passed to `filter_monitors`
+138
+139 Returns:
+140 A list of dictionaries, each representing a detected display.
+141 Each returned dictionary will have the following keys:
+142 - name (`str`): the name of the display
+143 - model (`str`): the model of the display
+144 - manufacturer (`str`): the name of the display manufacturer
+145 - manufacturer_id (`str`): the three letter manufacturer code (see `MONITOR_MANUFACTURER_CODES`)
+146 - serial (`str`): the serial of the display OR some other unique identifier
+147 - edid (`str`): the EDID string for the display
+148 - method (`BrightnessMethod`): the brightness method associated with this display
+149 - index (`int`): the index of the display, relative to the brightness method
+150 '''
+151...
+152
+153@classmethod
+154@abstractmethod
+155defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+156'''
+157 Args:
+158 display: the index of the specific display to query.
+159 If unspecified, all detected displays are queried
+160
+161 Returns:
+162 A list of `.types.IntPercentage` values, one for each
+163 queried display
+164 '''
+165...
+166
+167@classmethod
+168@abstractmethod
+169defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+170'''
+171 Args:
+172 value (.types.IntPercentage): the new brightness value
+173 display: the index of the specific display to adjust.
+174 If unspecified, all detected displays are adjusted
+175 '''
+176...
+
+
+
+
Helper class that provides a standard way to create an ABC using
+inheritance.
129@classmethod
+130@abstractmethod
+131defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+132'''
+133 Return information about detected displays.
+134
+135 Args:
+136 display (.types.DisplayIdentifier): the specific display to return
+137 information about. This parameter is passed to `filter_monitors`
+138
+139 Returns:
+140 A list of dictionaries, each representing a detected display.
+141 Each returned dictionary will have the following keys:
+142 - name (`str`): the name of the display
+143 - model (`str`): the model of the display
+144 - manufacturer (`str`): the name of the display manufacturer
+145 - manufacturer_id (`str`): the three letter manufacturer code (see `MONITOR_MANUFACTURER_CODES`)
+146 - serial (`str`): the serial of the display OR some other unique identifier
+147 - edid (`str`): the EDID string for the display
+148 - method (`BrightnessMethod`): the brightness method associated with this display
+149 - index (`int`): the index of the display, relative to the brightness method
+150 '''
+151...
+
153@classmethod
+154@abstractmethod
+155defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+156'''
+157 Args:
+158 display: the index of the specific display to query.
+159 If unspecified, all detected displays are queried
+160
+161 Returns:
+162 A list of `.types.IntPercentage` values, one for each
+163 queried display
+164 '''
+165...
+
+
+
+
Arguments:
+
+
+
display: the index of the specific display to query.
+If unspecified, all detected displays are queried
167@classmethod
+168@abstractmethod
+169defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+170'''
+171 Args:
+172 value (.types.IntPercentage): the new brightness value
+173 display: the index of the specific display to adjust.
+174 If unspecified, all detected displays are adjusted
+175 '''
+176...
+
179classBrightnessMethodAdv(BrightnessMethod):
+180@classmethod
+181@abstractmethod
+182def_gdi(cls)->List[dict]:
+183'''
+184 Similar to `BrightnessMethod.get_display_info` except this method will also
+185 return unsupported displays, indicated by an `unsupported: bool` property
+186 in the returned dict
+187 '''
+188...
+
+
+
+
Helper class that provides a standard way to create an ABC using
+inheritance.
242classEDID:
+243'''
+244 Simple structure and method to extract display serial and name from an EDID string.
+245 '''
+246EDID_FORMAT:str=(
+247">"# big-endian
+248"8s"# constant header (8 bytes)
+249"H"# manufacturer id (2 bytes)
+250"H"# product id (2 bytes)
+251"I"# serial number (4 bytes)
+252"B"# manufactoring week (1 byte)
+253"B"# manufactoring year (1 byte)
+254"B"# edid version (1 byte)
+255"B"# edid revision (1 byte)
+256"B"# video input type (1 byte)
+257"B"# horizontal size in cm (1 byte)
+258"B"# vertical size in cm (1 byte)
+259"B"# display gamma (1 byte)
+260"B"# supported features (1 byte)
+261"10s"# colour characteristics (10 bytes)
+262"H"# supported timings (2 bytes)
+263"B"# reserved timing (1 byte)
+264"16s"# EDID supported timings (16 bytes)
+265"18s"# timing / display descriptor block 1 (18 bytes)
+266"18s"# timing / display descriptor block 2 (18 bytes)
+267"18s"# timing / display descriptor block 3 (18 bytes)
+268"18s"# timing / display descriptor block 4 (18 bytes)
+269"B"# extension flag (1 byte)
+270"B"# checksum (1 byte)
+271)
+272'''
+273 The byte structure for EDID strings, taken from
+274 [pyedid](https://github.com/jojonas/pyedid/blob/2382910d968b2fa8de1fab495fbbdfebcdb39f19/pyedid/edid.py#L21),
+275 [Copyright 2019-2020 Jonas Lieb, Davydov Denis](https://github.com/jojonas/pyedid/blob/master/LICENSE).
+276 '''
+277SERIAL_DESCRIPTOR=bytes.fromhex('00 00 00 ff 00')
+278NAME_DESCRIPTOR=bytes.fromhex('00 00 00 fc 00')
+279
+280@classmethod
+281defparse(cls,edid:Union[bytes,str])->Tuple[Union[str,None],...]:
+282'''
+283 Takes an EDID string and parses some relevant information from it according to the
+284 [EDID 1.4](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format)
+285 specification on Wikipedia.
+286
+287 Args:
+288 edid (bytes or str): the EDID, can either be raw bytes or
+289 a hex formatted string (00 ff ff ff ff...)
+290
+291 Returns:
+292 tuple[str | None]: A tuple of 5 items representing the display's manufacturer ID,
+293 manufacturer, model, name, serial in that order.
+294 If any of these values are unable to be determined, they will be None.
+295 Otherwise, expect a string
+296
+297 Raises:
+298 EDIDParseError: if the EDID info cannot be unpacked
+299 TypeError: if `edid` is not `str` or `bytes`
+300
+301 Example:
+302 ```python
+303 import screen_brightness_control as sbc
+304
+305 edid = sbc.list_monitors_info()[0]['edid']
+306 manufacturer_id, manufacturer, model, name, serial = sbc.EDID.parse(edid)
+307
+308 print('Manufacturer:', manufacturer_id or 'Unknown')
+309 print('Model:', model or 'Unknown')
+310 print('Name:', name or 'Unknown')
+311 ```
+312 '''
+313# see https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format
+314ifisinstance(edid,str):
+315edid=bytes.fromhex(edid)
+316elifnotisinstance(edid,bytes):
+317raiseTypeError(f'edid must be of type bytes or str, not {type(edid)!r}')
+318
+319try:
+320blocks=struct.unpack(cls.EDID_FORMAT,edid)
+321exceptstruct.errorase:
+322raiseEDIDParseError('cannot unpack edid')frome
+323
+324# split mfg_id (2 bytes) into 3 letters, 5 bits each (ignoring reserved bit)
+325mfg_id_chars=(
+326blocks[1]>>10,# First 6 bits (reserved bit at start is always 0)
+327(blocks[1]>>5)&0b11111,# isolate next 5 bits from first 11 using bitwise AND
+328blocks[1]&0b11111# Last five bits
+329)
+330# turn numbers into ascii
+331mfg_id=''.join(chr(i+64)foriinmfg_id_chars)
+332
+333# now grab the manufacturer name
+334mfg_lookup=_monitor_brand_lookup(mfg_id)
+335ifmfg_lookupisnotNone:
+336manufacturer=mfg_lookup[1]
+337else:
+338manufacturer=None
+339
+340serial=None
+341name=None
+342fordescriptor_blockinblocks[17:21]:
+343# decode the serial
+344ifdescriptor_block.startswith(cls.SERIAL_DESCRIPTOR):
+345# strip descriptor bytes and trailing whitespace
+346serial_bytes=descriptor_block[len(cls.SERIAL_DESCRIPTOR):].rstrip()
+347serial=serial_bytes.decode()
+348
+349# decode the monitor name
+350elifdescriptor_block.startswith(cls.NAME_DESCRIPTOR):
+351# strip descriptor bytes and trailing whitespace
+352name_bytes=descriptor_block[len(cls.NAME_DESCRIPTOR):].rstrip()
+353name=name_bytes.decode()
+354
+355# now try to figure out what model the display is
+356model=None
+357ifnameisnotNone:
+358ifmanufacturerisnotNoneandname.startswith(manufacturer):
+359# eg: 'BenQ GL2450H' -> 'GL2450H'
+360model=name.replace(manufacturer,'',1).strip()
+361
+362# if previous method did not work (or if we don't know the manufacturer),
+363# try taking last word of name
+364ifnotmodel:
+365try:
+366# eg: 'BenQ GL2450H' -> ['BenQ', 'GL2450H']
+367model=name.strip().rsplit(' ',1)[1]
+368exceptIndexError:
+369# If the name does not include model information then
+370# give it something generic
+371model='Generic Monitor'
+372
+373returnmfg_id,manufacturer,model,name,serial
+374
+375@staticmethod
+376defhexdump(file:str)->str:
+377'''
+378 Returns a hexadecimal string of binary data from a file
+379
+380 Args:
+381 file (str): the file to read
+382
+383 Returns:
+384 str: one long hex string
+385
+386 Example:
+387 ```python
+388 from screen_brightness_control import EDID
+389
+390 print(EDID.hexdump('/sys/class/backlight/intel_backlight/device/edid'))
+391 # '00ffffffffffff00...'
+392 ```
+393 '''
+394withopen(file,'rb')asf:
+395hex_str=''.join(f'{char:02x}'forcharinf.read())
+396
+397returnhex_str
+
+
+
+
Simple structure and method to extract display serial and name from an EDID string.
280@classmethod
+281defparse(cls,edid:Union[bytes,str])->Tuple[Union[str,None],...]:
+282'''
+283 Takes an EDID string and parses some relevant information from it according to the
+284 [EDID 1.4](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format)
+285 specification on Wikipedia.
+286
+287 Args:
+288 edid (bytes or str): the EDID, can either be raw bytes or
+289 a hex formatted string (00 ff ff ff ff...)
+290
+291 Returns:
+292 tuple[str | None]: A tuple of 5 items representing the display's manufacturer ID,
+293 manufacturer, model, name, serial in that order.
+294 If any of these values are unable to be determined, they will be None.
+295 Otherwise, expect a string
+296
+297 Raises:
+298 EDIDParseError: if the EDID info cannot be unpacked
+299 TypeError: if `edid` is not `str` or `bytes`
+300
+301 Example:
+302 ```python
+303 import screen_brightness_control as sbc
+304
+305 edid = sbc.list_monitors_info()[0]['edid']
+306 manufacturer_id, manufacturer, model, name, serial = sbc.EDID.parse(edid)
+307
+308 print('Manufacturer:', manufacturer_id or 'Unknown')
+309 print('Model:', model or 'Unknown')
+310 print('Name:', name or 'Unknown')
+311 ```
+312 '''
+313# see https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format
+314ifisinstance(edid,str):
+315edid=bytes.fromhex(edid)
+316elifnotisinstance(edid,bytes):
+317raiseTypeError(f'edid must be of type bytes or str, not {type(edid)!r}')
+318
+319try:
+320blocks=struct.unpack(cls.EDID_FORMAT,edid)
+321exceptstruct.errorase:
+322raiseEDIDParseError('cannot unpack edid')frome
+323
+324# split mfg_id (2 bytes) into 3 letters, 5 bits each (ignoring reserved bit)
+325mfg_id_chars=(
+326blocks[1]>>10,# First 6 bits (reserved bit at start is always 0)
+327(blocks[1]>>5)&0b11111,# isolate next 5 bits from first 11 using bitwise AND
+328blocks[1]&0b11111# Last five bits
+329)
+330# turn numbers into ascii
+331mfg_id=''.join(chr(i+64)foriinmfg_id_chars)
+332
+333# now grab the manufacturer name
+334mfg_lookup=_monitor_brand_lookup(mfg_id)
+335ifmfg_lookupisnotNone:
+336manufacturer=mfg_lookup[1]
+337else:
+338manufacturer=None
+339
+340serial=None
+341name=None
+342fordescriptor_blockinblocks[17:21]:
+343# decode the serial
+344ifdescriptor_block.startswith(cls.SERIAL_DESCRIPTOR):
+345# strip descriptor bytes and trailing whitespace
+346serial_bytes=descriptor_block[len(cls.SERIAL_DESCRIPTOR):].rstrip()
+347serial=serial_bytes.decode()
+348
+349# decode the monitor name
+350elifdescriptor_block.startswith(cls.NAME_DESCRIPTOR):
+351# strip descriptor bytes and trailing whitespace
+352name_bytes=descriptor_block[len(cls.NAME_DESCRIPTOR):].rstrip()
+353name=name_bytes.decode()
+354
+355# now try to figure out what model the display is
+356model=None
+357ifnameisnotNone:
+358ifmanufacturerisnotNoneandname.startswith(manufacturer):
+359# eg: 'BenQ GL2450H' -> 'GL2450H'
+360model=name.replace(manufacturer,'',1).strip()
+361
+362# if previous method did not work (or if we don't know the manufacturer),
+363# try taking last word of name
+364ifnotmodel:
+365try:
+366# eg: 'BenQ GL2450H' -> ['BenQ', 'GL2450H']
+367model=name.strip().rsplit(' ',1)[1]
+368exceptIndexError:
+369# If the name does not include model information then
+370# give it something generic
+371model='Generic Monitor'
+372
+373returnmfg_id,manufacturer,model,name,serial
+
+
+
+
Takes an EDID string and parses some relevant information from it according to the
+EDID 1.4
+specification on Wikipedia.
+
+
Arguments:
+
+
+
edid (bytes or str): the EDID, can either be raw bytes or
+a hex formatted string (00 ff ff ff ff...)
+
+
+
Returns:
+
+
+
tuple[str | None]: A tuple of 5 items representing the display's manufacturer ID,
+ manufacturer, model, name, serial in that order.
+ If any of these values are unable to be determined, they will be None.
+ Otherwise, expect a string
+
+
+
Raises:
+
+
+
EDIDParseError: if the EDID info cannot be unpacked
400defcheck_output(command:List[str],max_tries:int=1)->bytes:
+401'''
+402 Run a command with retry management built in.
+403
+404 Args:
+405 command: the command to run
+406 max_tries: the maximum number of retries to allow before raising an error
+407
+408 Returns:
+409 The output from the command
+410 '''
+411tries=1
+412whileTrue:
+413try:
+414output=subprocess.check_output(command,stderr=subprocess.PIPE)
+415exceptsubprocess.CalledProcessErrorase:
+416iftries>=max_tries:
+417raiseMaxRetriesExceededError(f'process failed after {tries} tries',e)frome
+418tries+=1
+419time.sleep(0.04iftries<5else0.5)
+420else:
+421iftries>1:
+422_logger.debug(f'command {command} took {tries}/{max_tries} tries')
+423returnoutput
+
+
+
+
Run a command with retry management built in.
+
+
Arguments:
+
+
+
command: the command to run
+
max_tries: the maximum number of retries to allow before raising an error
426deflogarithmic_range(start:int,stop:int,step:int=1)->Generator[int,None,None]:
+427'''
+428 A `range`-like function that yields a sequence of integers following
+429 a logarithmic curve (`y = 10 ^ (x / 50)`) from `start` (inclusive) to
+430 `stop` (inclusive).
+431
+432 This is useful because it skips many of the higher percentages in the
+433 sequence where single percent brightness changes are hard to notice.
+434
+435 This function is designed to deal with brightness percentages, and so
+436 will never return a value less than 0 or greater than 100.
+437
+438 Args:
+439 start: the start of your percentage range
+440 stop: the end of your percentage range
+441 step: the increment per iteration through the sequence
+442
+443 Yields:
+444 int
+445 '''
+446start=int(max(0,start))
+447stop=int(min(100,stop))
+448
+449ifstart==stoporabs(stop-start)<=1:
+450yieldstart
+451else:
+452value_range=stop-start
+453
+454defdirection(x):
+455returnxifstep>0else100-x
+456
+457last_yielded=None
+458x:float
+459forxinrange(start,stop+1,step):
+460# get difference from base point
+461x-=start
+462# calculate progress through our range as a percentage
+463x=(x/value_range)*100
+464# convert along logarithmic curve (inverse of y = 50log(x)) to another percentage
+465x=10**(direction(x)/50)
+466# apply this percentage to our range and add back starting offset
+467x=int(((direction(x)/100)*value_range)+start)
+468
+469ifx==last_yielded:
+470continue
+471yieldx
+472last_yielded=x
+
+
+
+
A range-like function that yields a sequence of integers following
+a logarithmic curve (y = 10 ^ (x / 50)) from start (inclusive) to
+stop (inclusive).
+
+
This is useful because it skips many of the higher percentages in the
+sequence where single percent brightness changes are hard to notice.
+
+
This function is designed to deal with brightness percentages, and so
+will never return a value less than 0 or greater than 100.
+
+
Arguments:
+
+
+
start: the start of your percentage range
+
stop: the end of your percentage range
+
step: the increment per iteration through the sequence
494defpercentage(
+495value:Percentage,
+496current:Optional[Union[int,Callable[[],int]]]=None,
+497lower_bound:int=0
+498)->IntPercentage:
+499'''
+500 Convenience function to convert a brightness value into a percentage. Can handle
+501 integers, floats and strings. Also can handle relative strings (eg: `'+10'` or `'-10'`)
+502
+503 Args:
+504 value: the brightness value to convert
+505 current: the current brightness value or a function that returns the current brightness
+506 value. Used when dealing with relative brightness values
+507 lower_bound: the minimum value the brightness can be set to
+508
+509 Returns:
+510 `.types.IntPercentage`: The new brightness percentage, between `lower_bound` and 100
+511 '''
+512ifisinstance(value,str)and('+'invalueor'-'invalue):
+513ifcallable(current):
+514current=current()
+515value=int(float(value))+int(float(str(current)))
+516else:
+517value=int(float(str(value)))
+518
+519returnmin(100,max(lower_bound,value))
+
+
+
+
Convenience function to convert a brightness value into a percentage. Can handle
+integers, floats and strings. Also can handle relative strings (eg: '+10' or '-10')
+
+
Arguments:
+
+
+
value: the brightness value to convert
+
current: the current brightness value or a function that returns the current brightness
+value. Used when dealing with relative brightness values
+
lower_bound: the minimum value the brightness can be set to
1importfcntl
+ 2importfunctools
+ 3importglob
+ 4importlogging
+ 5importoperator
+ 6importos
+ 7importre
+ 8importtime
+ 9fromtypingimportList,Optional,Tuple
+ 10
+ 11from.importfilter_monitors,get_methods
+ 12from.exceptionsimportI2CValidationError,NoValidDisplayError,format_exc
+ 13from.helpersimport(EDID,BrightnessMethod,BrightnessMethodAdv,__Cache,
+ 14_monitor_brand_lookup,check_output)
+ 15from.typesimportDisplayIdentifier,IntPercentage
+ 16
+ 17__cache__=__Cache()
+ 18_logger=logging.getLogger(__name__)
+ 19
+ 20
+ 21classSysFiles(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
+ 36defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+ 37subsystems=set()
+ 38forfolderinos.listdir('/sys/class/backlight'):
+ 39ifos.path.isdir(f'/sys/class/backlight/{folder}/subsystem'):
+ 40subsystems.add(tuple(os.listdir(f'/sys/class/backlight/{folder}/subsystem')))
+ 41
+ 42displays_by_edid={}
+ 43index=0
+ 44
+ 45# map drm devices to their pci device paths
+ 46drm_paths={}
+ 47forfolderinglob.glob('/sys/class/drm/card*-*'):
+ 48drm_paths[os.path.realpath(folder)]=folder
+ 49
+ 50forsubsysteminsubsystems:
+ 51
+ 52device:dict={
+ 53'name':subsystem[0],
+ 54'path':f'/sys/class/backlight/{subsystem[0]}',
+ 55'method':cls,
+ 56'index':index,
+ 57'model':None,
+ 58'serial':None,
+ 59'manufacturer':None,
+ 60'manufacturer_id':None,
+ 61'edid':None,
+ 62'scale':None,
+ 63'uid':None
+ 64}
+ 65
+ 66forfolderinsubsystem:
+ 67# subsystems like intel_backlight usually have an acpi_video0
+ 68# counterpart, which we don't want so lets find the 'best' candidate
+ 69try:
+ 70withopen(f'/sys/class/backlight/{folder}/max_brightness')asf:
+ 71# scale for SysFiles is just a multiplier for the set/get brightness values
+ 72scale=int(f.read().rstrip(' \n'))/100
+ 73
+ 74# use the display with the highest resolution scale
+ 75ifdevice['scale']isNoneorscale>device['scale']:
+ 76device['name']=folder
+ 77device['path']=f'/sys/class/backlight/{folder}'
+ 78device['scale']=scale
+ 79except(FileNotFoundError,TypeError)ase:
+ 80cls._logger.error(
+ 81f'error getting highest resolution scale for {folder}'
+ 82f' - {format_exc(e)}'
+ 83)
+ 84continue
+ 85
+ 86# check if backlight subsystem device matches any of the PCI devices discovered earlier
+ 87# if so, extract the i2c bus from the drm device folder
+ 88pci_path=os.path.realpath(f'/sys/class/backlight/{folder}/device')
+ 89ifpci_pathindrm_paths:
+ 90device['uid']=device['uid']ori2c_bus_from_drm_device(drm_paths[pci_path])
+ 91
+ 92ifos.path.isfile('%s/device/edid'%device['path']):
+ 93device['edid']=EDID.hexdump('%s/device/edid'%device['path'])
+ 94
+ 95forkey,valueinzip(
+ 96('manufacturer_id','manufacturer','model','name','serial'),
+ 97EDID.parse(device['edid'])
+ 98):
+ 99ifvalueisNone:
+100continue
+101device[key]=value
+102
+103displays_by_edid[device['edid']]=device
+104index+=1
+105
+106all_displays=list(displays_by_edid.values())
+107ifdisplayisnotNone:
+108all_displays=filter_monitors(
+109display=display,haystack=all_displays,include=['path'])
+110returnall_displays
+111
+112@classmethod
+113defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+114info=cls.get_display_info()
+115ifdisplayisnotNone:
+116info=[info[display]]
+117
+118results=[]
+119fordeviceininfo:
+120withopen(os.path.join(device['path'],'brightness'),'r')asf:
+121brightness=int(f.read().rstrip('\n'))
+122results.append(int(brightness/device['scale']))
+123
+124returnresults
+125
+126@classmethod
+127defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+128info=cls.get_display_info()
+129ifdisplayisnotNone:
+130info=[info[display]]
+131
+132fordeviceininfo:
+133withopen(os.path.join(device['path'],'brightness'),'w')asf:
+134f.write(str(int(value*device['scale'])))
+135
+136
+137classI2C(BrightnessMethod):
+138'''
+139 In the same spirit as `SysFiles`, this class serves as a way of getting
+140 display information and adjusting the brightness without relying on any
+141 3rd party software.
+142
+143 Usage of this class requires read and write permission for `/dev/i2c-*`.
+144
+145 This class works over the I2C bus, primarily with desktop monitors as I
+146 haven't tested any e-DP displays yet.
+147
+148 Massive thanks to [siemer](https://github.com/siemer) for
+149 his work on the [ddcci.py](https://github.com/siemer/ddcci) project,
+150 which served as a my main reference for this.
+151
+152 References:
+153 * [ddcci.py](https://github.com/siemer/ddcci)
+154 * [DDCCI Spec](https://milek7.pl/ddcbacklight/ddcci.pdf)
+155 '''
+156_logger=_logger.getChild('I2C')
+157
+158# vcp commands
+159GET_VCP_CMD=0x01
+160'''VCP command to get the value of a feature (eg: brightness)'''
+161GET_VCP_REPLY=0x02
+162'''VCP feature reply op code'''
+163SET_VCP_CMD=0x03
+164'''VCP command to set the value of a feature (eg: brightness)'''
+165
+166# addresses
+167DDCCI_ADDR=0x37
+168'''DDC packets are transmittred using this I2C address'''
+169HOST_ADDR_R=0x50
+170'''Packet source address (the computer) when reading data'''
+171HOST_ADDR_W=0x51
+172'''Packet source address (the computer) when writing data'''
+173DESTINATION_ADDR_W=0x6e
+174'''Packet destination address (the monitor) when writing data'''
+175I2C_SLAVE=0x0703
+176'''The I2C slave address'''
+177
+178# timings
+179WAIT_TIME=0.05
+180'''How long to wait between I2C commands'''
+181
+182_max_brightness_cache:dict={}
+183
+184classI2CDevice():
+185'''
+186 Class to read and write data to an I2C bus,
+187 based on the `I2CDev` class from [ddcci.py](https://github.com/siemer/ddcci)
+188 '''
+189
+190def__init__(self,fname:str,slave_addr:int):
+191'''
+192 Args:
+193 fname: the I2C path, eg: `/dev/i2c-2`
+194 slave_addr: not entirely sure what this is meant to be
+195 '''
+196self.device=os.open(fname,os.O_RDWR)
+197# I2C_SLAVE address setup
+198fcntl.ioctl(self.device,I2C.I2C_SLAVE,slave_addr)
+199
+200defread(self,length:int)->bytes:
+201'''
+202 Read a certain number of bytes from the I2C bus
+203
+204 Args:
+205 length: the number of bytes to read
+206
+207 Returns:
+208 bytes
+209 '''
+210returnos.read(self.device,length)
+211
+212defwrite(self,data:bytes)->int:
+213'''
+214 Writes data to the I2C bus
+215
+216 Args:
+217 data: the data to write
+218
+219 Returns:
+220 The number of bytes written
+221 '''
+222returnos.write(self.device,data)
+223
+224classDDCInterface(I2CDevice):
+225'''
+226 Class to send DDC (Display Data Channel) commands to an I2C device,
+227 based on the `Ddcci` and `Mccs` classes from [ddcci.py](https://github.com/siemer/ddcci)
+228 '''
+229
+230PROTOCOL_FLAG=0x80
+231
+232def__init__(self,i2c_path:str):
+233'''
+234 Args:
+235 i2c_path: the path to the I2C device, eg: `/dev/i2c-2`
+236 '''
+237self.logger=_logger.getChild(
+238self.__class__.__name__).getChild(i2c_path)
+239super().__init__(i2c_path,I2C.DDCCI_ADDR)
+240
+241defwrite(self,*args)->int:
+242'''
+243 Write some data to the I2C device.
+244
+245 It is recommended to use `setvcp` to set VCP values on the DDC device
+246 instead of using this function directly.
+247
+248 Args:
+249 *args: variable length list of arguments. This will be put
+250 into a `bytearray` and wrapped up in various flags and
+251 checksums before being written to the I2C device
+252
+253 Returns:
+254 The number of bytes that were written
+255 '''
+256time.sleep(I2C.WAIT_TIME)
+257
+258ba=bytearray(args)
+259ba.insert(0,len(ba)|self.PROTOCOL_FLAG)# add length info
+260ba.insert(0,I2C.HOST_ADDR_W)# insert source address
+261ba.append(functools.reduce(operator.xor,ba,
+262I2C.DESTINATION_ADDR_W))# checksum
+263
+264returnsuper().write(ba)
+265
+266defsetvcp(self,vcp_code:int,value:int)->int:
+267'''
+268 Set a VCP value on the device
+269
+270 Args:
+271 vcp_code: the VCP command to send, eg: `0x10` is brightness
+272 value: what to set the value to
+273
+274 Returns:
+275 The number of bytes written to the device
+276 '''
+277returnself.write(I2C.SET_VCP_CMD,vcp_code,*value.to_bytes(2,'big'))
+278
+279defread(self,amount:int)->bytes:
+280'''
+281 Reads data from the DDC device.
+282
+283 It is recommended to use `getvcp` to retrieve VCP values from the
+284 DDC device instead of using this function directly.
+285
+286 Args:
+287 amount: the number of bytes to read
+288
+289 Raises:
+290 ValueError: if the read data is deemed invalid
+291 '''
+292time.sleep(I2C.WAIT_TIME)
+293
+294ba=super().read(amount+3)
+295
+296# check the bytes read
+297checks={
+298'source address':ba[0]==I2C.DESTINATION_ADDR_W,
+299'checksum':functools.reduce(operator.xor,ba)==I2C.HOST_ADDR_R,
+300'length':len(ba)>=(ba[1]&~self.PROTOCOL_FLAG)+3
+301}
+302ifFalseinchecks.values():
+303self.logger.error('i2c read check failed: '+repr(checks))
+304raiseI2CValidationError(
+305'i2c read check failed: '+repr(checks))
+306
+307returnba[2:-1]
+308
+309defgetvcp(self,vcp_code:int)->Tuple[int,int]:
+310'''
+311 Retrieves a VCP value from the DDC device.
+312
+313 Args:
+314 vcp_code: the VCP value to read, eg: `0x10` is brightness
+315
+316 Returns:
+317 The current and maximum value respectively
+318
+319 Raises:
+320 ValueError: if the read data is deemed invalid
+321 '''
+322self.write(I2C.GET_VCP_CMD,vcp_code)
+323ba=self.read(8)
+324
+325checks={
+326'is feature reply':ba[0]==I2C.GET_VCP_REPLY,
+327'supported VCP opcode':ba[1]==0,
+328'answer matches request':ba[2]==vcp_code
+329}
+330ifFalseinchecks.values():
+331self.logger.error('i2c read check failed: '+repr(checks))
+332raiseI2CValidationError(
+333'i2c read check failed: '+repr(checks))
+334
+335# current and max values
+336returnint.from_bytes(ba[6:8],'big'),int.from_bytes(ba[4:6],'big')
+337
+338@classmethod
+339defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+340all_displays=__cache__.get('i2c_display_info')
+341ifall_displaysisNone:
+342all_displays=[]
+343index=0
+344
+345fori2c_pathinglob.glob('/dev/i2c-*'):
+346ifnotos.path.exists(i2c_path):
+347continue
+348
+349try:
+350# open the I2C device using the host read address
+351device=cls.I2CDevice(i2c_path,cls.HOST_ADDR_R)
+352# read some 512 bytes from the device
+353data=device.read(512)
+354exceptIOErrorase:
+355cls._logger.error(
+356f'IOError reading from device {i2c_path}: {e}')
+357continue
+358
+359# search for the EDID header within our 512 read bytes
+360start=data.find(bytes.fromhex('00 FF FF FF FF FF FF 00'))
+361ifstart<0:
+362continue
+363
+364# grab 128 bytes of the edid
+365edid=data[start:start+128]
+366# parse the EDID
+367(
+368manufacturer_id,
+369manufacturer,
+370model,
+371name,
+372serial
+373)=EDID.parse(edid)
+374
+375all_displays.append(
+376{
+377'name':name,
+378'model':model,
+379'manufacturer':manufacturer,
+380'manufacturer_id':manufacturer_id,
+381'serial':serial,
+382'method':cls,
+383'index':index,
+384# convert edid to hex string
+385'edid':''.join(f'{i:02x}'foriinedid),
+386'i2c_bus':i2c_path,
+387'uid':i2c_path.split('-')[-1]
+388}
+389)
+390index+=1
+391
+392ifall_displays:
+393__cache__.store('i2c_display_info',all_displays,expires=2)
+394
+395ifdisplayisnotNone:
+396returnfilter_monitors(display=display,haystack=all_displays,include=['i2c_bus'])
+397returnall_displays
+398
+399@classmethod
+400defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+401all_displays=cls.get_display_info()
+402ifdisplayisnotNone:
+403all_displays=[all_displays[display]]
+404
+405results=[]
+406fordeviceinall_displays:
+407interface=cls.DDCInterface(device['i2c_bus'])
+408value,max_value=interface.getvcp(0x10)
+409
+410# make sure display's max brighness is cached
+411cache_ident='%s-%s-%s'%(device['name'],
+412device['model'],device['serial'])
+413ifcache_identnotincls._max_brightness_cache:
+414cls._max_brightness_cache[cache_ident]=max_value
+415cls._logger.info(
+416f'{cache_ident} max brightness:{max_value} (current: {value})')
+417
+418ifmax_value!=100:
+419# if max value is not 100 then we have to adjust the scale to be
+420# a percentage
+421value=int((value/max_value)*100)
+422
+423results.append(value)
+424
+425returnresults
+426
+427@classmethod
+428defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+429all_displays=cls.get_display_info()
+430ifdisplayisnotNone:
+431all_displays=[all_displays[display]]
+432
+433fordeviceinall_displays:
+434# make sure display brightness max value is cached
+435cache_ident='%s-%s-%s'%(device['name'],
+436device['model'],device['serial'])
+437ifcache_identnotincls._max_brightness_cache:
+438cls.get_brightness(display=device['index'])
+439
+440# scale the brightness value according to the max brightness
+441max_value=cls._max_brightness_cache[cache_ident]
+442ifmax_value!=100:
+443value=int((value/100)*max_value)
+444
+445interface=cls.DDCInterface(device['i2c_bus'])
+446interface.setvcp(0x10,value)
+447
+448
+449classXRandr(BrightnessMethodAdv):
+450'''collection of screen brightness related methods using the xrandr executable'''
+451
+452executable:str='xrandr'
+453'''the xrandr executable to be called'''
+454
+455@staticmethod
+456def_get_uid(interface:str)->Optional[str]:
+457'''
+458 Attempts to find a UID (I2C bus path) for a given display interface.
+459
+460 This works by parsing the interface name and matching it up to the entries in `/sys/class/drm`.
+461 `i2c_bus_from_drm_device` is then used to extract the bus number
+462
+463 Args:
+464 interface: the interface in question. EG: `eDP-1`, `eDP1`, `HDMI-1`...
+465
+466 Returns:
+467 The bus number as a string if found. Otherwise, none.
+468 '''
+469ifnotos.path.isdir('/sys/class/drm'):
+470returnNone
+471
+472# use regex because sometimes it can be `eDP-1` and sometimes it's `eDP1`
+473ifinterface_match:=re.match(r'([a-z]+)-?(\d+)',interface,re.I):
+474interface,count=interface_match.groups()
+475else:
+476returnNone
+477
+478fordirinos.listdir('/sys/class/drm/'):
+479# use regex here for case insensitivity on the interface
+480ifnotre.match(r'card\d+-%s(?:-[A-Z])?-%s'%(interface,count),dir,re.I):
+481continue
+482dir=f'/sys/class/drm/{dir}'
+483ifbus:=i2c_bus_from_drm_device(dir):
+484returnbus
+485
+486@classmethod
+487def_gdi(cls):
+488'''
+489 .. warning:: Don't use this
+490 This function isn't final and I will probably make breaking changes to it.
+491 You have been warned
+492
+493 Gets all displays reported by XRandr even if they're not supported
+494 '''
+495xrandr_output=check_output(
+496[cls.executable,'--verbose']).decode().split('\n')
+497
+498display_count=0
+499tmp_display:dict={}
+500
+501forline_index,lineinenumerate(xrandr_output):
+502ifline=='':
+503continue
+504
+505ifnotline.startswith((' ','\t'))and'connected'inlineand'disconnected'notinline:
+506iftmp_display:
+507yieldtmp_display
+508
+509tmp_display={
+510'name':line.split(' ')[0],
+511'interface':line.split(' ')[0],
+512'method':cls,
+513'index':display_count,
+514'model':None,
+515'serial':None,
+516'manufacturer':None,
+517'manufacturer_id':None,
+518'edid':None,
+519'unsupported':line.startswith('XWAYLAND')or'WAYLAND_DISPLAY'inos.environ,
+520'uid':cls._get_uid(line.split(' ')[0])
+521}
+522display_count+=1
+523
+524elif'EDID:'inline:
+525# extract the edid from the chunk of the output that will contain the edid
+526edid=''.join(
+527i.replace('\t','').replace(' ','')foriinxrandr_output[line_index+1:line_index+9]
+528)
+529tmp_display['edid']=edid
+530
+531forkey,valueinzip(
+532('manufacturer_id','manufacturer','model','name','serial'),
+533EDID.parse(tmp_display['edid'])
+534):
+535ifvalueisNone:
+536continue
+537tmp_display[key]=value
+538
+539elif'Brightness:'inline:
+540tmp_display['brightness']=int(
+541float(line.replace('Brightness:',''))*100)
+542
+543iftmp_display:
+544yieldtmp_display
+545
+546@classmethod
+547defget_display_info(cls,display:Optional[DisplayIdentifier]=None,brightness:bool=False)->List[dict]:
+548'''
+549 Implements `BrightnessMethod.get_display_info`.
+550
+551 Args:
+552 display: the index of the specific display to query.
+553 If unspecified, all detected displays are queried
+554 brightness: whether to include the current brightness
+555 in the returned info
+556 '''
+557valid_displays=[]
+558foritemincls._gdi():
+559ifitem['unsupported']:
+560continue
+561ifnotbrightness:
+562delitem['brightness']
+563delitem['unsupported']
+564valid_displays.append(item)
+565ifdisplayisnotNone:
+566valid_displays=filter_monitors(
+567display=display,haystack=valid_displays,include=['interface'])
+568returnvalid_displays
+569
+570@classmethod
+571defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+572monitors=cls.get_display_info(brightness=True)
+573ifdisplayisnotNone:
+574monitors=[monitors[display]]
+575brightness=[i['brightness']foriinmonitors]
+576
+577returnbrightness
+578
+579@classmethod
+580defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+581value_as_str=str(float(value)/100)
+582info=cls.get_display_info()
+583ifdisplayisnotNone:
+584info=[info[display]]
+585
+586foriininfo:
+587check_output([cls.executable,'--output',
+588i['interface'],'--brightness',value_as_str])
+589
+590
+591classDDCUtil(BrightnessMethodAdv):
+592'''collection of screen brightness related methods using the ddcutil executable'''
+593_logger=_logger.getChild('DDCUtil')
+594
+595executable:str='ddcutil'
+596'''The ddcutil executable to be called'''
+597sleep_multiplier:float=0.5
+598'''
+599 How long ddcutil should sleep between each DDC request (lower is shorter).
+600 See [the ddcutil docs](https://www.ddcutil.com/performance_options/#option-sleep-multiplier).
+601 '''
+602cmd_max_tries:int=10
+603'''Max number of retries when calling the ddcutil'''
+604enable_async=True
+605'''
+606 Use the `--async` flag when calling ddcutil.
+607 See [ddcutil docs](https://www.ddcutil.com/performance_options/#option-async)
+608 '''
+609_max_brightness_cache:dict={}
+610'''Cache for displays and their maximum brightness values'''
+611
+612@classmethod
+613def_gdi(cls):
+614'''
+615 .. warning:: Don't use this
+616 This function isn't final and I will probably make breaking changes to it.
+617 You have been warned
+618
+619 Gets all displays reported by DDCUtil even if they're not supported
+620 '''
+621raw_ddcutil_output=str(
+622check_output(
+623[
+624cls.executable,'detect','-v',
+625f'--sleep-multiplier={cls.sleep_multiplier}'
+626]+['--async']ifcls.enable_asyncelse[],max_tries=cls.cmd_max_tries
+627)
+628)[2:-1].split('\\n')
+629# Use -v to get EDID string but this means output cannot be decoded.
+630# Or maybe it can. I don't know the encoding though, so let's assume it cannot be decoded.
+631# Use str()[2:-1] workaround
+632
+633# include "Invalid display" sections because they tell us where one displays metadata ends
+634# and another begins. We filter out invalid displays later on
+635ddcutil_output=[iforiinraw_ddcutil_outputifi.startswith(
+636('Invalid display','Display','\t',' '))]
+637tmp_display:dict={}
+638display_count=0
+639
+640forline_index,lineinenumerate(ddcutil_output):
+641ifnotline.startswith(('\t',' ')):
+642iftmp_display:
+643yieldtmp_display
+644
+645tmp_display={
+646'method':cls,
+647'index':display_count,
+648'model':None,
+649'serial':None,
+650'bin_serial':None,
+651'manufacturer':None,
+652'manufacturer_id':None,
+653'edid':None,
+654'unsupported':'invalid display'inline.lower(),
+655'uid':None
+656}
+657display_count+=1
+658
+659elif'I2C bus'inline:
+660tmp_display['i2c_bus']=line[line.index('/'):]
+661tmp_display['bus_number']=int(
+662tmp_display['i2c_bus'].replace('/dev/i2c-',''))
+663tmp_display['uid']=tmp_display['i2c_bus'].split('-')[-1]
+664
+665elif'Mfg id'inline:
+666# Recently ddcutil has started reporting manufacturer IDs like
+667# 'BNQ - UNK' or 'MSI - Microstep' so we have to split the line
+668# into chunks of alpha chars and check for a valid mfg id
+669forcodeinre.split(r'[^A-Za-z]',line.replace('Mfg id:','').replace(' ','')):
+670iflen(code)!=3:
+671# all mfg ids are 3 chars long
+672continue
+673
+674if(brand:=_monitor_brand_lookup(code)):
+675tmp_display['manufacturer_id'],tmp_display['manufacturer']=brand
+676break
+677
+678elif'Model'inline:
+679# the split() removes extra spaces
+680name=line.replace('Model:','').split()
+681try:
+682tmp_display['model']=name[1]
+683exceptIndexError:
+684pass
+685tmp_display['name']=' '.join(name)
+686
+687elif'Serial number'inline:
+688tmp_display['serial']=line.replace(
+689'Serial number:','').replace(' ','')orNone
+690
+691elif'Binary serial number:'inline:
+692tmp_display['bin_serial']=line.split(' ')[-1][3:-1]
+693
+694elif'EDID hex dump:'inline:
+695try:
+696tmp_display['edid']=''.join(
+697''.join(i.split()[1:17])foriinddcutil_output[line_index+2:line_index+10]
+698)
+699exceptException:
+700pass
+701
+702iftmp_display:
+703yieldtmp_display
+704
+705@classmethod
+706defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+707valid_displays=__cache__.get('ddcutil_monitors_info')
+708ifvalid_displaysisNone:
+709valid_displays=[]
+710foritemincls._gdi():
+711ifitem['unsupported']:
+712continue
+713delitem['unsupported']
+714valid_displays.append(item)
+715
+716ifvalid_displays:
+717__cache__.store('ddcutil_monitors_info',valid_displays)
+718
+719ifdisplayisnotNone:
+720valid_displays=filter_monitors(
+721display=display,haystack=valid_displays,include=['i2c_bus'])
+722returnvalid_displays
+723
+724@classmethod
+725defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+726monitors=cls.get_display_info()
+727ifdisplayisnotNone:
+728monitors=[monitors[display]]
+729
+730res=[]
+731formonitorinmonitors:
+732value=__cache__.get(f'ddcutil_brightness_{monitor["index"]}')
+733ifvalueisNone:
+734cmd_out=check_output(
+735[
+736cls.executable,
+737'getvcp','10','-t',
+738'-b',str(monitor['bus_number']),
+739f'--sleep-multiplier={cls.sleep_multiplier}'
+740],max_tries=cls.cmd_max_tries
+741).decode().split(' ')
+742
+743value=int(cmd_out[-2])
+744max_value=int(cmd_out[-1])
+745ifmax_value!=100:
+746# if the max brightness is not 100 then the number is not a percentage
+747# and will need to be scaled
+748value=int((value/max_value)*100)
+749
+750# now make sure max brightness is recorded so set_brightness can use it
+751cache_ident='%s-%s-%s'%(monitor['name'],
+752monitor['serial'],monitor['bin_serial'])
+753ifcache_identnotincls._max_brightness_cache:
+754cls._max_brightness_cache[cache_ident]=max_value
+755cls._logger.debug(
+756f'{cache_ident} max brightness:{max_value} (current: {value})')
+757
+758__cache__.store(
+759f'ddcutil_brightness_{monitor["index"]}',value,expires=0.5)
+760res.append(value)
+761returnres
+762
+763@classmethod
+764defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+765monitors=cls.get_display_info()
+766ifdisplayisnotNone:
+767monitors=[monitors[display]]
+768
+769__cache__.expire(startswith='ddcutil_brightness_')
+770formonitorinmonitors:
+771# check if monitor has a max brightness that requires us to scale this value
+772cache_ident='%s-%s-%s'%(monitor['name'],
+773monitor['serial'],monitor['bin_serial'])
+774ifcache_identnotincls._max_brightness_cache:
+775cls.get_brightness(display=monitor['index'])
+776
+777ifcls._max_brightness_cache[cache_ident]!=100:
+778value=int((value/100)*cls._max_brightness_cache[cache_ident])
+779
+780check_output(
+781[
+782cls.executable,'setvcp','10',str(value),
+783'-b',str(monitor['bus_number']),
+784f'--sleep-multiplier={cls.sleep_multiplier}'
+785],max_tries=cls.cmd_max_tries
+786)
+787
+788
+789defi2c_bus_from_drm_device(dir:str)->Optional[str]:
+790'''
+791 Extract the relevant I2C bus number from a device in `/sys/class/drm`.
+792
+793 This function works by searching the directory for `i2c-*` and `ddc/i2c-dev/i2c-*` folders.
+794
+795 Args:
+796 dir: the DRM directory, in the format `/sys/class/drm/<device>`
+797
+798 Returns:
+799 Returns the I2C bus number as a string if found. Otherwise, returns None
+800 '''
+801# check for enabled file and skip device if monitor inactive
+802ifos.path.isfile(f'{dir}/enabled'):
+803withopen(f'{dir}/enabled')asf:
+804iff.read().strip().lower()!='enabled':
+805return
+806
+807# sometimes the i2c path is in /sys/class/drm/*/i2c-*
+808# do this first because, in my testing, sometimes a device can have both `.../i2c-X` and `.../ddc/i2c-dev/...`
+809# and the latter is usually the wrong i2c path
+810paths=glob.glob(f'{dir}/i2c-*')
+811ifpaths:
+812returnpaths[0].split('-')[-1]
+813
+814# sometimes the i2c path is in /sys/class/drm/*/ddc/i2c-dev
+815ifos.path.isdir(f'{dir}/ddc/i2c-dev'):
+816paths=os.listdir(f'{dir}/ddc/i2c-dev')
+817ifpaths:
+818returnpaths[0].replace('i2c-','')
+819
+820
+821deflist_monitors_info(
+822method:Optional[str]=None,allow_duplicates:bool=False,unsupported:bool=False
+823)->List[dict]:
+824'''
+825 Lists detailed information about all detected displays
+826
+827 Args:
+828 method: the method the display can be addressed by. See `.get_methods`
+829 for more info on available methods
+830 allow_duplicates: whether to filter out duplicate displays (displays with the same EDID) or not
+831 unsupported: include detected displays that are invalid or unsupported
+832 '''
+833all_methods=get_methods(method).values()
+834haystack=[]
+835formethod_classinall_methods:
+836try:
+837ifunsupportedandissubclass(method_class,BrightnessMethodAdv):
+838haystack+=method_class._gdi()
+839else:
+840haystack+=method_class.get_display_info()
+841exceptExceptionase:
+842_logger.warning(
+843f'error grabbing display info from {method_class} - {format_exc(e)}')
+844pass
+845
+846ifallow_duplicates:
+847returnhaystack
+848
+849try:
+850# use filter_monitors to remove duplicates
+851returnfilter_monitors(haystack=haystack)
+852exceptNoValidDisplayError:
+853return[]
+854
+855
+856METHODS=(SysFiles,I2C,XRandr,DDCUtil)
+
22classSysFiles(BrightnessMethod):
+ 23'''
+ 24 A way of getting display information and adjusting the brightness
+ 25 that does not rely on any 3rd party software.
+ 26
+ 27 This class works with displays that show up in the `/sys/class/backlight`
+ 28 directory (so usually laptop displays).
+ 29
+ 30 To set the brightness, your user will need write permissions for
+ 31 `/sys/class/backlight/*/brightness` or you will need to run the program
+ 32 as root.
+ 33 '''
+ 34_logger=_logger.getChild('SysFiles')
+ 35
+ 36@classmethod
+ 37defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+ 38subsystems=set()
+ 39forfolderinos.listdir('/sys/class/backlight'):
+ 40ifos.path.isdir(f'/sys/class/backlight/{folder}/subsystem'):
+ 41subsystems.add(tuple(os.listdir(f'/sys/class/backlight/{folder}/subsystem')))
+ 42
+ 43displays_by_edid={}
+ 44index=0
+ 45
+ 46# map drm devices to their pci device paths
+ 47drm_paths={}
+ 48forfolderinglob.glob('/sys/class/drm/card*-*'):
+ 49drm_paths[os.path.realpath(folder)]=folder
+ 50
+ 51forsubsysteminsubsystems:
+ 52
+ 53device:dict={
+ 54'name':subsystem[0],
+ 55'path':f'/sys/class/backlight/{subsystem[0]}',
+ 56'method':cls,
+ 57'index':index,
+ 58'model':None,
+ 59'serial':None,
+ 60'manufacturer':None,
+ 61'manufacturer_id':None,
+ 62'edid':None,
+ 63'scale':None,
+ 64'uid':None
+ 65}
+ 66
+ 67forfolderinsubsystem:
+ 68# subsystems like intel_backlight usually have an acpi_video0
+ 69# counterpart, which we don't want so lets find the 'best' candidate
+ 70try:
+ 71withopen(f'/sys/class/backlight/{folder}/max_brightness')asf:
+ 72# scale for SysFiles is just a multiplier for the set/get brightness values
+ 73scale=int(f.read().rstrip(' \n'))/100
+ 74
+ 75# use the display with the highest resolution scale
+ 76ifdevice['scale']isNoneorscale>device['scale']:
+ 77device['name']=folder
+ 78device['path']=f'/sys/class/backlight/{folder}'
+ 79device['scale']=scale
+ 80except(FileNotFoundError,TypeError)ase:
+ 81cls._logger.error(
+ 82f'error getting highest resolution scale for {folder}'
+ 83f' - {format_exc(e)}'
+ 84)
+ 85continue
+ 86
+ 87# check if backlight subsystem device matches any of the PCI devices discovered earlier
+ 88# if so, extract the i2c bus from the drm device folder
+ 89pci_path=os.path.realpath(f'/sys/class/backlight/{folder}/device')
+ 90ifpci_pathindrm_paths:
+ 91device['uid']=device['uid']ori2c_bus_from_drm_device(drm_paths[pci_path])
+ 92
+ 93ifos.path.isfile('%s/device/edid'%device['path']):
+ 94device['edid']=EDID.hexdump('%s/device/edid'%device['path'])
+ 95
+ 96forkey,valueinzip(
+ 97('manufacturer_id','manufacturer','model','name','serial'),
+ 98EDID.parse(device['edid'])
+ 99):
+100ifvalueisNone:
+101continue
+102device[key]=value
+103
+104displays_by_edid[device['edid']]=device
+105index+=1
+106
+107all_displays=list(displays_by_edid.values())
+108ifdisplayisnotNone:
+109all_displays=filter_monitors(
+110display=display,haystack=all_displays,include=['path'])
+111returnall_displays
+112
+113@classmethod
+114defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+115info=cls.get_display_info()
+116ifdisplayisnotNone:
+117info=[info[display]]
+118
+119results=[]
+120fordeviceininfo:
+121withopen(os.path.join(device['path'],'brightness'),'r')asf:
+122brightness=int(f.read().rstrip('\n'))
+123results.append(int(brightness/device['scale']))
+124
+125returnresults
+126
+127@classmethod
+128defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+129info=cls.get_display_info()
+130ifdisplayisnotNone:
+131info=[info[display]]
+132
+133fordeviceininfo:
+134withopen(os.path.join(device['path'],'brightness'),'w')asf:
+135f.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.
138classI2C(BrightnessMethod):
+139'''
+140 In the same spirit as `SysFiles`, this class serves as a way of getting
+141 display information and adjusting the brightness without relying on any
+142 3rd party software.
+143
+144 Usage of this class requires read and write permission for `/dev/i2c-*`.
+145
+146 This class works over the I2C bus, primarily with desktop monitors as I
+147 haven't tested any e-DP displays yet.
+148
+149 Massive thanks to [siemer](https://github.com/siemer) for
+150 his work on the [ddcci.py](https://github.com/siemer/ddcci) project,
+151 which served as a my main reference for this.
+152
+153 References:
+154 * [ddcci.py](https://github.com/siemer/ddcci)
+155 * [DDCCI Spec](https://milek7.pl/ddcbacklight/ddcci.pdf)
+156 '''
+157_logger=_logger.getChild('I2C')
+158
+159# vcp commands
+160GET_VCP_CMD=0x01
+161'''VCP command to get the value of a feature (eg: brightness)'''
+162GET_VCP_REPLY=0x02
+163'''VCP feature reply op code'''
+164SET_VCP_CMD=0x03
+165'''VCP command to set the value of a feature (eg: brightness)'''
+166
+167# addresses
+168DDCCI_ADDR=0x37
+169'''DDC packets are transmittred using this I2C address'''
+170HOST_ADDR_R=0x50
+171'''Packet source address (the computer) when reading data'''
+172HOST_ADDR_W=0x51
+173'''Packet source address (the computer) when writing data'''
+174DESTINATION_ADDR_W=0x6e
+175'''Packet destination address (the monitor) when writing data'''
+176I2C_SLAVE=0x0703
+177'''The I2C slave address'''
+178
+179# timings
+180WAIT_TIME=0.05
+181'''How long to wait between I2C commands'''
+182
+183_max_brightness_cache:dict={}
+184
+185classI2CDevice():
+186'''
+187 Class to read and write data to an I2C bus,
+188 based on the `I2CDev` class from [ddcci.py](https://github.com/siemer/ddcci)
+189 '''
+190
+191def__init__(self,fname:str,slave_addr:int):
+192'''
+193 Args:
+194 fname: the I2C path, eg: `/dev/i2c-2`
+195 slave_addr: not entirely sure what this is meant to be
+196 '''
+197self.device=os.open(fname,os.O_RDWR)
+198# I2C_SLAVE address setup
+199fcntl.ioctl(self.device,I2C.I2C_SLAVE,slave_addr)
+200
+201defread(self,length:int)->bytes:
+202'''
+203 Read a certain number of bytes from the I2C bus
+204
+205 Args:
+206 length: the number of bytes to read
+207
+208 Returns:
+209 bytes
+210 '''
+211returnos.read(self.device,length)
+212
+213defwrite(self,data:bytes)->int:
+214'''
+215 Writes data to the I2C bus
+216
+217 Args:
+218 data: the data to write
+219
+220 Returns:
+221 The number of bytes written
+222 '''
+223returnos.write(self.device,data)
+224
+225classDDCInterface(I2CDevice):
+226'''
+227 Class to send DDC (Display Data Channel) commands to an I2C device,
+228 based on the `Ddcci` and `Mccs` classes from [ddcci.py](https://github.com/siemer/ddcci)
+229 '''
+230
+231PROTOCOL_FLAG=0x80
+232
+233def__init__(self,i2c_path:str):
+234'''
+235 Args:
+236 i2c_path: the path to the I2C device, eg: `/dev/i2c-2`
+237 '''
+238self.logger=_logger.getChild(
+239self.__class__.__name__).getChild(i2c_path)
+240super().__init__(i2c_path,I2C.DDCCI_ADDR)
+241
+242defwrite(self,*args)->int:
+243'''
+244 Write some data to the I2C device.
+245
+246 It is recommended to use `setvcp` to set VCP values on the DDC device
+247 instead of using this function directly.
+248
+249 Args:
+250 *args: variable length list of arguments. This will be put
+251 into a `bytearray` and wrapped up in various flags and
+252 checksums before being written to the I2C device
+253
+254 Returns:
+255 The number of bytes that were written
+256 '''
+257time.sleep(I2C.WAIT_TIME)
+258
+259ba=bytearray(args)
+260ba.insert(0,len(ba)|self.PROTOCOL_FLAG)# add length info
+261ba.insert(0,I2C.HOST_ADDR_W)# insert source address
+262ba.append(functools.reduce(operator.xor,ba,
+263I2C.DESTINATION_ADDR_W))# checksum
+264
+265returnsuper().write(ba)
+266
+267defsetvcp(self,vcp_code:int,value:int)->int:
+268'''
+269 Set a VCP value on the device
+270
+271 Args:
+272 vcp_code: the VCP command to send, eg: `0x10` is brightness
+273 value: what to set the value to
+274
+275 Returns:
+276 The number of bytes written to the device
+277 '''
+278returnself.write(I2C.SET_VCP_CMD,vcp_code,*value.to_bytes(2,'big'))
+279
+280defread(self,amount:int)->bytes:
+281'''
+282 Reads data from the DDC device.
+283
+284 It is recommended to use `getvcp` to retrieve VCP values from the
+285 DDC device instead of using this function directly.
+286
+287 Args:
+288 amount: the number of bytes to read
+289
+290 Raises:
+291 ValueError: if the read data is deemed invalid
+292 '''
+293time.sleep(I2C.WAIT_TIME)
+294
+295ba=super().read(amount+3)
+296
+297# check the bytes read
+298checks={
+299'source address':ba[0]==I2C.DESTINATION_ADDR_W,
+300'checksum':functools.reduce(operator.xor,ba)==I2C.HOST_ADDR_R,
+301'length':len(ba)>=(ba[1]&~self.PROTOCOL_FLAG)+3
+302}
+303ifFalseinchecks.values():
+304self.logger.error('i2c read check failed: '+repr(checks))
+305raiseI2CValidationError(
+306'i2c read check failed: '+repr(checks))
+307
+308returnba[2:-1]
+309
+310defgetvcp(self,vcp_code:int)->Tuple[int,int]:
+311'''
+312 Retrieves a VCP value from the DDC device.
+313
+314 Args:
+315 vcp_code: the VCP value to read, eg: `0x10` is brightness
+316
+317 Returns:
+318 The current and maximum value respectively
+319
+320 Raises:
+321 ValueError: if the read data is deemed invalid
+322 '''
+323self.write(I2C.GET_VCP_CMD,vcp_code)
+324ba=self.read(8)
+325
+326checks={
+327'is feature reply':ba[0]==I2C.GET_VCP_REPLY,
+328'supported VCP opcode':ba[1]==0,
+329'answer matches request':ba[2]==vcp_code
+330}
+331ifFalseinchecks.values():
+332self.logger.error('i2c read check failed: '+repr(checks))
+333raiseI2CValidationError(
+334'i2c read check failed: '+repr(checks))
+335
+336# current and max values
+337returnint.from_bytes(ba[6:8],'big'),int.from_bytes(ba[4:6],'big')
+338
+339@classmethod
+340defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+341all_displays=__cache__.get('i2c_display_info')
+342ifall_displaysisNone:
+343all_displays=[]
+344index=0
+345
+346fori2c_pathinglob.glob('/dev/i2c-*'):
+347ifnotos.path.exists(i2c_path):
+348continue
+349
+350try:
+351# open the I2C device using the host read address
+352device=cls.I2CDevice(i2c_path,cls.HOST_ADDR_R)
+353# read some 512 bytes from the device
+354data=device.read(512)
+355exceptIOErrorase:
+356cls._logger.error(
+357f'IOError reading from device {i2c_path}: {e}')
+358continue
+359
+360# search for the EDID header within our 512 read bytes
+361start=data.find(bytes.fromhex('00 FF FF FF FF FF FF 00'))
+362ifstart<0:
+363continue
+364
+365# grab 128 bytes of the edid
+366edid=data[start:start+128]
+367# parse the EDID
+368(
+369manufacturer_id,
+370manufacturer,
+371model,
+372name,
+373serial
+374)=EDID.parse(edid)
+375
+376all_displays.append(
+377{
+378'name':name,
+379'model':model,
+380'manufacturer':manufacturer,
+381'manufacturer_id':manufacturer_id,
+382'serial':serial,
+383'method':cls,
+384'index':index,
+385# convert edid to hex string
+386'edid':''.join(f'{i:02x}'foriinedid),
+387'i2c_bus':i2c_path,
+388'uid':i2c_path.split('-')[-1]
+389}
+390)
+391index+=1
+392
+393ifall_displays:
+394__cache__.store('i2c_display_info',all_displays,expires=2)
+395
+396ifdisplayisnotNone:
+397returnfilter_monitors(display=display,haystack=all_displays,include=['i2c_bus'])
+398returnall_displays
+399
+400@classmethod
+401defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+402all_displays=cls.get_display_info()
+403ifdisplayisnotNone:
+404all_displays=[all_displays[display]]
+405
+406results=[]
+407fordeviceinall_displays:
+408interface=cls.DDCInterface(device['i2c_bus'])
+409value,max_value=interface.getvcp(0x10)
+410
+411# make sure display's max brighness is cached
+412cache_ident='%s-%s-%s'%(device['name'],
+413device['model'],device['serial'])
+414ifcache_identnotincls._max_brightness_cache:
+415cls._max_brightness_cache[cache_ident]=max_value
+416cls._logger.info(
+417f'{cache_ident} max brightness:{max_value} (current: {value})')
+418
+419ifmax_value!=100:
+420# if max value is not 100 then we have to adjust the scale to be
+421# a percentage
+422value=int((value/max_value)*100)
+423
+424results.append(value)
+425
+426returnresults
+427
+428@classmethod
+429defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+430all_displays=cls.get_display_info()
+431ifdisplayisnotNone:
+432all_displays=[all_displays[display]]
+433
+434fordeviceinall_displays:
+435# make sure display brightness max value is cached
+436cache_ident='%s-%s-%s'%(device['name'],
+437device['model'],device['serial'])
+438ifcache_identnotincls._max_brightness_cache:
+439cls.get_brightness(display=device['index'])
+440
+441# scale the brightness value according to the max brightness
+442max_value=cls._max_brightness_cache[cache_ident]
+443ifmax_value!=100:
+444value=int((value/100)*max_value)
+445
+446interface=cls.DDCInterface(device['i2c_bus'])
+447interface.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.
400@classmethod
+401defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+402all_displays=cls.get_display_info()
+403ifdisplayisnotNone:
+404all_displays=[all_displays[display]]
+405
+406results=[]
+407fordeviceinall_displays:
+408interface=cls.DDCInterface(device['i2c_bus'])
+409value,max_value=interface.getvcp(0x10)
+410
+411# make sure display's max brighness is cached
+412cache_ident='%s-%s-%s'%(device['name'],
+413device['model'],device['serial'])
+414ifcache_identnotincls._max_brightness_cache:
+415cls._max_brightness_cache[cache_ident]=max_value
+416cls._logger.info(
+417f'{cache_ident} max brightness:{max_value} (current: {value})')
+418
+419ifmax_value!=100:
+420# if max value is not 100 then we have to adjust the scale to be
+421# a percentage
+422value=int((value/max_value)*100)
+423
+424results.append(value)
+425
+426returnresults
+
+
+
+
Arguments:
+
+
+
display: the index of the specific display to query.
+If unspecified, all detected displays are queried
428@classmethod
+429defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+430all_displays=cls.get_display_info()
+431ifdisplayisnotNone:
+432all_displays=[all_displays[display]]
+433
+434fordeviceinall_displays:
+435# make sure display brightness max value is cached
+436cache_ident='%s-%s-%s'%(device['name'],
+437device['model'],device['serial'])
+438ifcache_identnotincls._max_brightness_cache:
+439cls.get_brightness(display=device['index'])
+440
+441# scale the brightness value according to the max brightness
+442max_value=cls._max_brightness_cache[cache_ident]
+443ifmax_value!=100:
+444value=int((value/100)*max_value)
+445
+446interface=cls.DDCInterface(device['i2c_bus'])
+447interface.setvcp(0x10,value)
+
display: the index of the specific display to adjust.
+If unspecified, all detected displays are adjusted
+
+
+
+
+
+
+
+
+
+
+ class
+ I2C.I2CDevice:
+
+
+
+
+
+
185classI2CDevice():
+186'''
+187 Class to read and write data to an I2C bus,
+188 based on the `I2CDev` class from [ddcci.py](https://github.com/siemer/ddcci)
+189 '''
+190
+191def__init__(self,fname:str,slave_addr:int):
+192'''
+193 Args:
+194 fname: the I2C path, eg: `/dev/i2c-2`
+195 slave_addr: not entirely sure what this is meant to be
+196 '''
+197self.device=os.open(fname,os.O_RDWR)
+198# I2C_SLAVE address setup
+199fcntl.ioctl(self.device,I2C.I2C_SLAVE,slave_addr)
+200
+201defread(self,length:int)->bytes:
+202'''
+203 Read a certain number of bytes from the I2C bus
+204
+205 Args:
+206 length: the number of bytes to read
+207
+208 Returns:
+209 bytes
+210 '''
+211returnos.read(self.device,length)
+212
+213defwrite(self,data:bytes)->int:
+214'''
+215 Writes data to the I2C bus
+216
+217 Args:
+218 data: the data to write
+219
+220 Returns:
+221 The number of bytes written
+222 '''
+223returnos.write(self.device,data)
+
+
+
+
Class to read and write data to an I2C bus,
+based on the I2CDev class from ddcci.py
191def__init__(self,fname:str,slave_addr:int):
+192'''
+193 Args:
+194 fname: the I2C path, eg: `/dev/i2c-2`
+195 slave_addr: not entirely sure what this is meant to be
+196 '''
+197self.device=os.open(fname,os.O_RDWR)
+198# I2C_SLAVE address setup
+199fcntl.ioctl(self.device,I2C.I2C_SLAVE,slave_addr)
+
+
+
+
Arguments:
+
+
+
fname: the I2C path, eg: /dev/i2c-2
+
slave_addr: not entirely sure what this is meant to be
201defread(self,length:int)->bytes:
+202'''
+203 Read a certain number of bytes from the I2C bus
+204
+205 Args:
+206 length: the number of bytes to read
+207
+208 Returns:
+209 bytes
+210 '''
+211returnos.read(self.device,length)
+
+
+
+
Read a certain number of bytes from the I2C bus
+
+
Arguments:
+
+
+
length: the number of bytes to read
+
+
+
Returns:
+
+
+
bytes
+
+
+
+
+
+
+
+
+
+ def
+ write(self, data:bytes) -> int:
+
+
+
+
+
+
213defwrite(self,data:bytes)->int:
+214'''
+215 Writes data to the I2C bus
+216
+217 Args:
+218 data: the data to write
+219
+220 Returns:
+221 The number of bytes written
+222 '''
+223returnos.write(self.device,data)
+
+
+
+
Writes data to the I2C bus
+
+
Arguments:
+
+
+
data: the data to write
+
+
+
Returns:
+
+
+
The number of bytes written
+
+
+
+
+
+
+
+
+
+
+ class
+ I2C.DDCInterface(I2C.I2CDevice):
+
+
+
+
+
+
225classDDCInterface(I2CDevice):
+226'''
+227 Class to send DDC (Display Data Channel) commands to an I2C device,
+228 based on the `Ddcci` and `Mccs` classes from [ddcci.py](https://github.com/siemer/ddcci)
+229 '''
+230
+231PROTOCOL_FLAG=0x80
+232
+233def__init__(self,i2c_path:str):
+234'''
+235 Args:
+236 i2c_path: the path to the I2C device, eg: `/dev/i2c-2`
+237 '''
+238self.logger=_logger.getChild(
+239self.__class__.__name__).getChild(i2c_path)
+240super().__init__(i2c_path,I2C.DDCCI_ADDR)
+241
+242defwrite(self,*args)->int:
+243'''
+244 Write some data to the I2C device.
+245
+246 It is recommended to use `setvcp` to set VCP values on the DDC device
+247 instead of using this function directly.
+248
+249 Args:
+250 *args: variable length list of arguments. This will be put
+251 into a `bytearray` and wrapped up in various flags and
+252 checksums before being written to the I2C device
+253
+254 Returns:
+255 The number of bytes that were written
+256 '''
+257time.sleep(I2C.WAIT_TIME)
+258
+259ba=bytearray(args)
+260ba.insert(0,len(ba)|self.PROTOCOL_FLAG)# add length info
+261ba.insert(0,I2C.HOST_ADDR_W)# insert source address
+262ba.append(functools.reduce(operator.xor,ba,
+263I2C.DESTINATION_ADDR_W))# checksum
+264
+265returnsuper().write(ba)
+266
+267defsetvcp(self,vcp_code:int,value:int)->int:
+268'''
+269 Set a VCP value on the device
+270
+271 Args:
+272 vcp_code: the VCP command to send, eg: `0x10` is brightness
+273 value: what to set the value to
+274
+275 Returns:
+276 The number of bytes written to the device
+277 '''
+278returnself.write(I2C.SET_VCP_CMD,vcp_code,*value.to_bytes(2,'big'))
+279
+280defread(self,amount:int)->bytes:
+281'''
+282 Reads data from the DDC device.
+283
+284 It is recommended to use `getvcp` to retrieve VCP values from the
+285 DDC device instead of using this function directly.
+286
+287 Args:
+288 amount: the number of bytes to read
+289
+290 Raises:
+291 ValueError: if the read data is deemed invalid
+292 '''
+293time.sleep(I2C.WAIT_TIME)
+294
+295ba=super().read(amount+3)
+296
+297# check the bytes read
+298checks={
+299'source address':ba[0]==I2C.DESTINATION_ADDR_W,
+300'checksum':functools.reduce(operator.xor,ba)==I2C.HOST_ADDR_R,
+301'length':len(ba)>=(ba[1]&~self.PROTOCOL_FLAG)+3
+302}
+303ifFalseinchecks.values():
+304self.logger.error('i2c read check failed: '+repr(checks))
+305raiseI2CValidationError(
+306'i2c read check failed: '+repr(checks))
+307
+308returnba[2:-1]
+309
+310defgetvcp(self,vcp_code:int)->Tuple[int,int]:
+311'''
+312 Retrieves a VCP value from the DDC device.
+313
+314 Args:
+315 vcp_code: the VCP value to read, eg: `0x10` is brightness
+316
+317 Returns:
+318 The current and maximum value respectively
+319
+320 Raises:
+321 ValueError: if the read data is deemed invalid
+322 '''
+323self.write(I2C.GET_VCP_CMD,vcp_code)
+324ba=self.read(8)
+325
+326checks={
+327'is feature reply':ba[0]==I2C.GET_VCP_REPLY,
+328'supported VCP opcode':ba[1]==0,
+329'answer matches request':ba[2]==vcp_code
+330}
+331ifFalseinchecks.values():
+332self.logger.error('i2c read check failed: '+repr(checks))
+333raiseI2CValidationError(
+334'i2c read check failed: '+repr(checks))
+335
+336# current and max values
+337returnint.from_bytes(ba[6:8],'big'),int.from_bytes(ba[4:6],'big')
+
+
+
+
Class to send DDC (Display Data Channel) commands to an I2C device,
+based on the Ddcci and Mccs classes from ddcci.py
+
+
+
+
+
+
+
+ I2C.DDCInterface(i2c_path:str)
+
+
+
+
+
+
233def__init__(self,i2c_path:str):
+234'''
+235 Args:
+236 i2c_path: the path to the I2C device, eg: `/dev/i2c-2`
+237 '''
+238self.logger=_logger.getChild(
+239self.__class__.__name__).getChild(i2c_path)
+240super().__init__(i2c_path,I2C.DDCCI_ADDR)
+
+
+
+
Arguments:
+
+
+
i2c_path: the path to the I2C device, eg: /dev/i2c-2
+
+
+
+
+
+
+
+ PROTOCOL_FLAG =
+128
+
+
+
+
+
+
+
+
+
+
+ logger
+
+
+
+
+
+
+
+
+
+
+
+
+ def
+ write(self, *args) -> int:
+
+
+
+
+
+
242defwrite(self,*args)->int:
+243'''
+244 Write some data to the I2C device.
+245
+246 It is recommended to use `setvcp` to set VCP values on the DDC device
+247 instead of using this function directly.
+248
+249 Args:
+250 *args: variable length list of arguments. This will be put
+251 into a `bytearray` and wrapped up in various flags and
+252 checksums before being written to the I2C device
+253
+254 Returns:
+255 The number of bytes that were written
+256 '''
+257time.sleep(I2C.WAIT_TIME)
+258
+259ba=bytearray(args)
+260ba.insert(0,len(ba)|self.PROTOCOL_FLAG)# add length info
+261ba.insert(0,I2C.HOST_ADDR_W)# insert source address
+262ba.append(functools.reduce(operator.xor,ba,
+263I2C.DESTINATION_ADDR_W))# checksum
+264
+265returnsuper().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
267defsetvcp(self,vcp_code:int,value:int)->int:
+268'''
+269 Set a VCP value on the device
+270
+271 Args:
+272 vcp_code: the VCP command to send, eg: `0x10` is brightness
+273 value: what to set the value to
+274
+275 Returns:
+276 The number of bytes written to the device
+277 '''
+278returnself.write(I2C.SET_VCP_CMD,vcp_code,*value.to_bytes(2,'big'))
+
+
+
+
Set a VCP value on the device
+
+
Arguments:
+
+
+
vcp_code: the VCP command to send, eg: 0x10 is brightness
280defread(self,amount:int)->bytes:
+281'''
+282 Reads data from the DDC device.
+283
+284 It is recommended to use `getvcp` to retrieve VCP values from the
+285 DDC device instead of using this function directly.
+286
+287 Args:
+288 amount: the number of bytes to read
+289
+290 Raises:
+291 ValueError: if the read data is deemed invalid
+292 '''
+293time.sleep(I2C.WAIT_TIME)
+294
+295ba=super().read(amount+3)
+296
+297# check the bytes read
+298checks={
+299'source address':ba[0]==I2C.DESTINATION_ADDR_W,
+300'checksum':functools.reduce(operator.xor,ba)==I2C.HOST_ADDR_R,
+301'length':len(ba)>=(ba[1]&~self.PROTOCOL_FLAG)+3
+302}
+303ifFalseinchecks.values():
+304self.logger.error('i2c read check failed: '+repr(checks))
+305raiseI2CValidationError(
+306'i2c read check failed: '+repr(checks))
+307
+308returnba[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.
310defgetvcp(self,vcp_code:int)->Tuple[int,int]:
+311'''
+312 Retrieves a VCP value from the DDC device.
+313
+314 Args:
+315 vcp_code: the VCP value to read, eg: `0x10` is brightness
+316
+317 Returns:
+318 The current and maximum value respectively
+319
+320 Raises:
+321 ValueError: if the read data is deemed invalid
+322 '''
+323self.write(I2C.GET_VCP_CMD,vcp_code)
+324ba=self.read(8)
+325
+326checks={
+327'is feature reply':ba[0]==I2C.GET_VCP_REPLY,
+328'supported VCP opcode':ba[1]==0,
+329'answer matches request':ba[2]==vcp_code
+330}
+331ifFalseinchecks.values():
+332self.logger.error('i2c read check failed: '+repr(checks))
+333raiseI2CValidationError(
+334'i2c read check failed: '+repr(checks))
+335
+336# current and max values
+337returnint.from_bytes(ba[6:8],'big'),int.from_bytes(ba[4:6],'big')
+
+
+
+
Retrieves a VCP value from the DDC device.
+
+
Arguments:
+
+
+
vcp_code: the VCP value to read, eg: 0x10 is brightness
450classXRandr(BrightnessMethodAdv):
+451'''collection of screen brightness related methods using the xrandr executable'''
+452
+453executable:str='xrandr'
+454'''the xrandr executable to be called'''
+455
+456@staticmethod
+457def_get_uid(interface:str)->Optional[str]:
+458'''
+459 Attempts to find a UID (I2C bus path) for a given display interface.
+460
+461 This works by parsing the interface name and matching it up to the entries in `/sys/class/drm`.
+462 `i2c_bus_from_drm_device` is then used to extract the bus number
+463
+464 Args:
+465 interface: the interface in question. EG: `eDP-1`, `eDP1`, `HDMI-1`...
+466
+467 Returns:
+468 The bus number as a string if found. Otherwise, none.
+469 '''
+470ifnotos.path.isdir('/sys/class/drm'):
+471returnNone
+472
+473# use regex because sometimes it can be `eDP-1` and sometimes it's `eDP1`
+474ifinterface_match:=re.match(r'([a-z]+)-?(\d+)',interface,re.I):
+475interface,count=interface_match.groups()
+476else:
+477returnNone
+478
+479fordirinos.listdir('/sys/class/drm/'):
+480# use regex here for case insensitivity on the interface
+481ifnotre.match(r'card\d+-%s(?:-[A-Z])?-%s'%(interface,count),dir,re.I):
+482continue
+483dir=f'/sys/class/drm/{dir}'
+484ifbus:=i2c_bus_from_drm_device(dir):
+485returnbus
+486
+487@classmethod
+488def_gdi(cls):
+489'''
+490 .. warning:: Don't use this
+491 This function isn't final and I will probably make breaking changes to it.
+492 You have been warned
+493
+494 Gets all displays reported by XRandr even if they're not supported
+495 '''
+496xrandr_output=check_output(
+497[cls.executable,'--verbose']).decode().split('\n')
+498
+499display_count=0
+500tmp_display:dict={}
+501
+502forline_index,lineinenumerate(xrandr_output):
+503ifline=='':
+504continue
+505
+506ifnotline.startswith((' ','\t'))and'connected'inlineand'disconnected'notinline:
+507iftmp_display:
+508yieldtmp_display
+509
+510tmp_display={
+511'name':line.split(' ')[0],
+512'interface':line.split(' ')[0],
+513'method':cls,
+514'index':display_count,
+515'model':None,
+516'serial':None,
+517'manufacturer':None,
+518'manufacturer_id':None,
+519'edid':None,
+520'unsupported':line.startswith('XWAYLAND')or'WAYLAND_DISPLAY'inos.environ,
+521'uid':cls._get_uid(line.split(' ')[0])
+522}
+523display_count+=1
+524
+525elif'EDID:'inline:
+526# extract the edid from the chunk of the output that will contain the edid
+527edid=''.join(
+528i.replace('\t','').replace(' ','')foriinxrandr_output[line_index+1:line_index+9]
+529)
+530tmp_display['edid']=edid
+531
+532forkey,valueinzip(
+533('manufacturer_id','manufacturer','model','name','serial'),
+534EDID.parse(tmp_display['edid'])
+535):
+536ifvalueisNone:
+537continue
+538tmp_display[key]=value
+539
+540elif'Brightness:'inline:
+541tmp_display['brightness']=int(
+542float(line.replace('Brightness:',''))*100)
+543
+544iftmp_display:
+545yieldtmp_display
+546
+547@classmethod
+548defget_display_info(cls,display:Optional[DisplayIdentifier]=None,brightness:bool=False)->List[dict]:
+549'''
+550 Implements `BrightnessMethod.get_display_info`.
+551
+552 Args:
+553 display: the index of the specific display to query.
+554 If unspecified, all detected displays are queried
+555 brightness: whether to include the current brightness
+556 in the returned info
+557 '''
+558valid_displays=[]
+559foritemincls._gdi():
+560ifitem['unsupported']:
+561continue
+562ifnotbrightness:
+563delitem['brightness']
+564delitem['unsupported']
+565valid_displays.append(item)
+566ifdisplayisnotNone:
+567valid_displays=filter_monitors(
+568display=display,haystack=valid_displays,include=['interface'])
+569returnvalid_displays
+570
+571@classmethod
+572defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+573monitors=cls.get_display_info(brightness=True)
+574ifdisplayisnotNone:
+575monitors=[monitors[display]]
+576brightness=[i['brightness']foriinmonitors]
+577
+578returnbrightness
+579
+580@classmethod
+581defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+582value_as_str=str(float(value)/100)
+583info=cls.get_display_info()
+584ifdisplayisnotNone:
+585info=[info[display]]
+586
+587foriininfo:
+588check_output([cls.executable,'--output',
+589i['interface'],'--brightness',value_as_str])
+
+
+
+
collection of screen brightness related methods using the xrandr executable
547@classmethod
+548defget_display_info(cls,display:Optional[DisplayIdentifier]=None,brightness:bool=False)->List[dict]:
+549'''
+550 Implements `BrightnessMethod.get_display_info`.
+551
+552 Args:
+553 display: the index of the specific display to query.
+554 If unspecified, all detected displays are queried
+555 brightness: whether to include the current brightness
+556 in the returned info
+557 '''
+558valid_displays=[]
+559foritemincls._gdi():
+560ifitem['unsupported']:
+561continue
+562ifnotbrightness:
+563delitem['brightness']
+564delitem['unsupported']
+565valid_displays.append(item)
+566ifdisplayisnotNone:
+567valid_displays=filter_monitors(
+568display=display,haystack=valid_displays,include=['interface'])
+569returnvalid_displays
+
+
+
+
Implements BrightnessMethod.get_display_info.
+
+
Arguments:
+
+
+
display: the index of the specific display to query.
+If unspecified, all detected displays are queried
+
brightness: whether to include the current brightness
+in the returned info
592classDDCUtil(BrightnessMethodAdv):
+593'''collection of screen brightness related methods using the ddcutil executable'''
+594_logger=_logger.getChild('DDCUtil')
+595
+596executable:str='ddcutil'
+597'''The ddcutil executable to be called'''
+598sleep_multiplier:float=0.5
+599'''
+600 How long ddcutil should sleep between each DDC request (lower is shorter).
+601 See [the ddcutil docs](https://www.ddcutil.com/performance_options/#option-sleep-multiplier).
+602 '''
+603cmd_max_tries:int=10
+604'''Max number of retries when calling the ddcutil'''
+605enable_async=True
+606'''
+607 Use the `--async` flag when calling ddcutil.
+608 See [ddcutil docs](https://www.ddcutil.com/performance_options/#option-async)
+609 '''
+610_max_brightness_cache:dict={}
+611'''Cache for displays and their maximum brightness values'''
+612
+613@classmethod
+614def_gdi(cls):
+615'''
+616 .. warning:: Don't use this
+617 This function isn't final and I will probably make breaking changes to it.
+618 You have been warned
+619
+620 Gets all displays reported by DDCUtil even if they're not supported
+621 '''
+622raw_ddcutil_output=str(
+623check_output(
+624[
+625cls.executable,'detect','-v',
+626f'--sleep-multiplier={cls.sleep_multiplier}'
+627]+['--async']ifcls.enable_asyncelse[],max_tries=cls.cmd_max_tries
+628)
+629)[2:-1].split('\\n')
+630# Use -v to get EDID string but this means output cannot be decoded.
+631# Or maybe it can. I don't know the encoding though, so let's assume it cannot be decoded.
+632# Use str()[2:-1] workaround
+633
+634# include "Invalid display" sections because they tell us where one displays metadata ends
+635# and another begins. We filter out invalid displays later on
+636ddcutil_output=[iforiinraw_ddcutil_outputifi.startswith(
+637('Invalid display','Display','\t',' '))]
+638tmp_display:dict={}
+639display_count=0
+640
+641forline_index,lineinenumerate(ddcutil_output):
+642ifnotline.startswith(('\t',' ')):
+643iftmp_display:
+644yieldtmp_display
+645
+646tmp_display={
+647'method':cls,
+648'index':display_count,
+649'model':None,
+650'serial':None,
+651'bin_serial':None,
+652'manufacturer':None,
+653'manufacturer_id':None,
+654'edid':None,
+655'unsupported':'invalid display'inline.lower(),
+656'uid':None
+657}
+658display_count+=1
+659
+660elif'I2C bus'inline:
+661tmp_display['i2c_bus']=line[line.index('/'):]
+662tmp_display['bus_number']=int(
+663tmp_display['i2c_bus'].replace('/dev/i2c-',''))
+664tmp_display['uid']=tmp_display['i2c_bus'].split('-')[-1]
+665
+666elif'Mfg id'inline:
+667# Recently ddcutil has started reporting manufacturer IDs like
+668# 'BNQ - UNK' or 'MSI - Microstep' so we have to split the line
+669# into chunks of alpha chars and check for a valid mfg id
+670forcodeinre.split(r'[^A-Za-z]',line.replace('Mfg id:','').replace(' ','')):
+671iflen(code)!=3:
+672# all mfg ids are 3 chars long
+673continue
+674
+675if(brand:=_monitor_brand_lookup(code)):
+676tmp_display['manufacturer_id'],tmp_display['manufacturer']=brand
+677break
+678
+679elif'Model'inline:
+680# the split() removes extra spaces
+681name=line.replace('Model:','').split()
+682try:
+683tmp_display['model']=name[1]
+684exceptIndexError:
+685pass
+686tmp_display['name']=' '.join(name)
+687
+688elif'Serial number'inline:
+689tmp_display['serial']=line.replace(
+690'Serial number:','').replace(' ','')orNone
+691
+692elif'Binary serial number:'inline:
+693tmp_display['bin_serial']=line.split(' ')[-1][3:-1]
+694
+695elif'EDID hex dump:'inline:
+696try:
+697tmp_display['edid']=''.join(
+698''.join(i.split()[1:17])foriinddcutil_output[line_index+2:line_index+10]
+699)
+700exceptException:
+701pass
+702
+703iftmp_display:
+704yieldtmp_display
+705
+706@classmethod
+707defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+708valid_displays=__cache__.get('ddcutil_monitors_info')
+709ifvalid_displaysisNone:
+710valid_displays=[]
+711foritemincls._gdi():
+712ifitem['unsupported']:
+713continue
+714delitem['unsupported']
+715valid_displays.append(item)
+716
+717ifvalid_displays:
+718__cache__.store('ddcutil_monitors_info',valid_displays)
+719
+720ifdisplayisnotNone:
+721valid_displays=filter_monitors(
+722display=display,haystack=valid_displays,include=['i2c_bus'])
+723returnvalid_displays
+724
+725@classmethod
+726defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+727monitors=cls.get_display_info()
+728ifdisplayisnotNone:
+729monitors=[monitors[display]]
+730
+731res=[]
+732formonitorinmonitors:
+733value=__cache__.get(f'ddcutil_brightness_{monitor["index"]}')
+734ifvalueisNone:
+735cmd_out=check_output(
+736[
+737cls.executable,
+738'getvcp','10','-t',
+739'-b',str(monitor['bus_number']),
+740f'--sleep-multiplier={cls.sleep_multiplier}'
+741],max_tries=cls.cmd_max_tries
+742).decode().split(' ')
+743
+744value=int(cmd_out[-2])
+745max_value=int(cmd_out[-1])
+746ifmax_value!=100:
+747# if the max brightness is not 100 then the number is not a percentage
+748# and will need to be scaled
+749value=int((value/max_value)*100)
+750
+751# now make sure max brightness is recorded so set_brightness can use it
+752cache_ident='%s-%s-%s'%(monitor['name'],
+753monitor['serial'],monitor['bin_serial'])
+754ifcache_identnotincls._max_brightness_cache:
+755cls._max_brightness_cache[cache_ident]=max_value
+756cls._logger.debug(
+757f'{cache_ident} max brightness:{max_value} (current: {value})')
+758
+759__cache__.store(
+760f'ddcutil_brightness_{monitor["index"]}',value,expires=0.5)
+761res.append(value)
+762returnres
+763
+764@classmethod
+765defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+766monitors=cls.get_display_info()
+767ifdisplayisnotNone:
+768monitors=[monitors[display]]
+769
+770__cache__.expire(startswith='ddcutil_brightness_')
+771formonitorinmonitors:
+772# check if monitor has a max brightness that requires us to scale this value
+773cache_ident='%s-%s-%s'%(monitor['name'],
+774monitor['serial'],monitor['bin_serial'])
+775ifcache_identnotincls._max_brightness_cache:
+776cls.get_brightness(display=monitor['index'])
+777
+778ifcls._max_brightness_cache[cache_ident]!=100:
+779value=int((value/100)*cls._max_brightness_cache[cache_ident])
+780
+781check_output(
+782[
+783cls.executable,'setvcp','10',str(value),
+784'-b',str(monitor['bus_number']),
+785f'--sleep-multiplier={cls.sleep_multiplier}'
+786],max_tries=cls.cmd_max_tries
+787)
+
+
+
+
collection of screen brightness related methods using the ddcutil executable
+
+
+
+
+
+ executable: str =
+'ddcutil'
+
+
+
+
+
+
The ddcutil executable to be called
+
+
+
+
+
+
+ sleep_multiplier: float =
+0.5
+
+
+
+
+
+
How long ddcutil should sleep between each DDC request (lower is shorter).
+See the ddcutil docs.
+
+
+
+
+
+
+ cmd_max_tries: int =
+10
+
+
+
+
+
+
Max number of retries when calling the ddcutil
+
+
+
+
+
+
+ enable_async =
+True
+
+
+
+
+
+
Use the --async flag when calling ddcutil.
+See ddcutil docs
725@classmethod
+726defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+727monitors=cls.get_display_info()
+728ifdisplayisnotNone:
+729monitors=[monitors[display]]
+730
+731res=[]
+732formonitorinmonitors:
+733value=__cache__.get(f'ddcutil_brightness_{monitor["index"]}')
+734ifvalueisNone:
+735cmd_out=check_output(
+736[
+737cls.executable,
+738'getvcp','10','-t',
+739'-b',str(monitor['bus_number']),
+740f'--sleep-multiplier={cls.sleep_multiplier}'
+741],max_tries=cls.cmd_max_tries
+742).decode().split(' ')
+743
+744value=int(cmd_out[-2])
+745max_value=int(cmd_out[-1])
+746ifmax_value!=100:
+747# if the max brightness is not 100 then the number is not a percentage
+748# and will need to be scaled
+749value=int((value/max_value)*100)
+750
+751# now make sure max brightness is recorded so set_brightness can use it
+752cache_ident='%s-%s-%s'%(monitor['name'],
+753monitor['serial'],monitor['bin_serial'])
+754ifcache_identnotincls._max_brightness_cache:
+755cls._max_brightness_cache[cache_ident]=max_value
+756cls._logger.debug(
+757f'{cache_ident} max brightness:{max_value} (current: {value})')
+758
+759__cache__.store(
+760f'ddcutil_brightness_{monitor["index"]}',value,expires=0.5)
+761res.append(value)
+762returnres
+
+
+
+
Arguments:
+
+
+
display: the index of the specific display to query.
+If unspecified, all detected displays are queried
764@classmethod
+765defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+766monitors=cls.get_display_info()
+767ifdisplayisnotNone:
+768monitors=[monitors[display]]
+769
+770__cache__.expire(startswith='ddcutil_brightness_')
+771formonitorinmonitors:
+772# check if monitor has a max brightness that requires us to scale this value
+773cache_ident='%s-%s-%s'%(monitor['name'],
+774monitor['serial'],monitor['bin_serial'])
+775ifcache_identnotincls._max_brightness_cache:
+776cls.get_brightness(display=monitor['index'])
+777
+778ifcls._max_brightness_cache[cache_ident]!=100:
+779value=int((value/100)*cls._max_brightness_cache[cache_ident])
+780
+781check_output(
+782[
+783cls.executable,'setvcp','10',str(value),
+784'-b',str(monitor['bus_number']),
+785f'--sleep-multiplier={cls.sleep_multiplier}'
+786],max_tries=cls.cmd_max_tries
+787)
+
790defi2c_bus_from_drm_device(dir:str)->Optional[str]:
+791'''
+792 Extract the relevant I2C bus number from a device in `/sys/class/drm`.
+793
+794 This function works by searching the directory for `i2c-*` and `ddc/i2c-dev/i2c-*` folders.
+795
+796 Args:
+797 dir: the DRM directory, in the format `/sys/class/drm/<device>`
+798
+799 Returns:
+800 Returns the I2C bus number as a string if found. Otherwise, returns None
+801 '''
+802# check for enabled file and skip device if monitor inactive
+803ifos.path.isfile(f'{dir}/enabled'):
+804withopen(f'{dir}/enabled')asf:
+805iff.read().strip().lower()!='enabled':
+806return
+807
+808# sometimes the i2c path is in /sys/class/drm/*/i2c-*
+809# do this first because, in my testing, sometimes a device can have both `.../i2c-X` and `.../ddc/i2c-dev/...`
+810# and the latter is usually the wrong i2c path
+811paths=glob.glob(f'{dir}/i2c-*')
+812ifpaths:
+813returnpaths[0].split('-')[-1]
+814
+815# sometimes the i2c path is in /sys/class/drm/*/ddc/i2c-dev
+816ifos.path.isdir(f'{dir}/ddc/i2c-dev'):
+817paths=os.listdir(f'{dir}/ddc/i2c-dev')
+818ifpaths:
+819returnpaths[0].replace('i2c-','')
+
+
+
+
Extract the relevant I2C bus number from a device in /sys/class/drm.
+
+
This function works by searching the directory for i2c-* and ddc/i2c-dev/i2c-* folders.
+
+
Arguments:
+
+
+
dir: the DRM directory, in the format /sys/class/drm/<device>
+
+
+
Returns:
+
+
+
Returns the I2C bus number as a string if found. Otherwise, returns None
822deflist_monitors_info(
+823method:Optional[str]=None,allow_duplicates:bool=False,unsupported:bool=False
+824)->List[dict]:
+825'''
+826 Lists detailed information about all detected displays
+827
+828 Args:
+829 method: the method the display can be addressed by. See `.get_methods`
+830 for more info on available methods
+831 allow_duplicates: whether to filter out duplicate displays (displays with the same EDID) or not
+832 unsupported: include detected displays that are invalid or unsupported
+833 '''
+834all_methods=get_methods(method).values()
+835haystack=[]
+836formethod_classinall_methods:
+837try:
+838ifunsupportedandissubclass(method_class,BrightnessMethodAdv):
+839haystack+=method_class._gdi()
+840else:
+841haystack+=method_class.get_display_info()
+842exceptExceptionase:
+843_logger.warning(
+844f'error grabbing display info from {method_class} - {format_exc(e)}')
+845pass
+846
+847ifallow_duplicates:
+848returnhaystack
+849
+850try:
+851# use filter_monitors to remove duplicates
+852returnfilter_monitors(haystack=haystack)
+853exceptNoValidDisplayError:
+854return[]
+
+
+
+
Lists detailed information about all detected displays
Submodule containing types and type aliases used throughout the library.
+
+
Splitting these definitions into a seperate submodule allows for detailed
+explanations and verbose type definitions, without cluttering up the rest
+of the library.
+
+
This file is also useful for wrangling types based on the current Python
+version.
+
+
+
+
+
+
+
1'''
+ 2Submodule containing types and type aliases used throughout the library.
+ 3
+ 4Splitting these definitions into a seperate submodule allows for detailed
+ 5explanations and verbose type definitions, without cluttering up the rest
+ 6of the library.
+ 7
+ 8This file is also useful for wrangling types based on the current Python
+ 9version.
+10'''
+11fromtypingimportUnion
+12importsys
+13
+14# a bunch of typing classes were deprecated in Python 3.9
+15# in favour of collections.abc (https://www.python.org/dev/peps/pep-0585/)
+16ifsys.version_info[1]>=9:
+17fromcollections.abcimportGenerator
+18else:
+19fromtypingimportGenerator# noqa: F401
+20
+21IntPercentage=int
+22'''
+23An integer between 0 and 100 (inclusive) that represents a brightness level.
+24Other than the implied bounds, this is just a normal integer.
+25'''
+26Percentage=Union[IntPercentage,str]
+27'''
+28An `IntPercentage` or a string representing an `IntPercentage`.
+29
+30String values may come in two forms:
+31- Absolute values: for example `'40'` converts directly to `int('40')`
+32- Relative values: strings prefixed with `+`/`-` will be interpreted relative to the
+33 current brightness level. In this case, the integer value of your string will be added to the
+34 current brightness level.
+35 For example, if the current brightness is 50%, a value of `'+40'` would imply 90% brightness
+36 and a value of `'-40'` would imply 10% brightness.
+37
+38Relative brightness values will usually be resolved by the `.helpers.percentage` function.
+39'''
+40
+41
+42DisplayIdentifier=Union[int,str]
+43'''
+44Something that can be used to identify a particular display.
+45Can be any one of the following properties of a display:
+46- uid (str)
+47- edid (str)
+48- serial (str)
+49- name (str)
+50- index (int)
+51
+52See `.Display` for descriptions of each property and its type
+53'''
+
+
+
+
+
+
+
+
+ IntPercentage =
+<class 'int'>
+
+
+
+
+
+
An integer between 0 and 100 (inclusive) that represents a brightness level.
+Other than the implied bounds, this is just a normal integer.
Absolute values: for example '40' converts directly to int('40')
+
Relative values: strings prefixed with +/- will be interpreted relative to the
+current brightness level. In this case, the integer value of your string will be added to the
+current brightness level.
+For example, if the current brightness is 50%, a value of '+40' would imply 90% brightness
+and a value of '-40' would imply 10% brightness.
1importlogging
+ 2importre
+ 3importtime
+ 4fromcontextlibimportcontextmanager
+ 5fromctypesimportStructure,WinError,byref,windll
+ 6fromctypes.wintypesimportBYTE,DWORD,HANDLE,WCHAR
+ 7fromtypingimportList,Optional
+ 8
+ 9importpythoncom
+ 10importpywintypes
+ 11importwin32api
+ 12importwin32con
+ 13importwmi
+ 14
+ 15from.importfilter_monitors,get_methods
+ 16from.exceptionsimportEDIDParseError,NoValidDisplayError,format_exc
+ 17from.helpersimportEDID,BrightnessMethod,__Cache,_monitor_brand_lookup
+ 18from.typesimportDisplayIdentifier,Generator,IntPercentage
+ 19
+ 20__cache__=__Cache()
+ 21_logger=logging.getLogger(__name__)
+ 22
+ 23COM_MODEL=None
+ 24'''
+ 25The concurrency model and flags used when calling `pythoncom.CoInitializeEx`.
+ 26If left as `None` the library will call `CoInitialize` instead with whatever defaults it chooses.
+ 27
+ 28Refer to the [MS docs](https://learn.microsoft.com/en-us/windows/win32/api/objbase/ne-objbase-coinit)
+ 29for all the possible flags.
+ 30
+ 31See also:
+ 32- https://timgolden.me.uk/pywin32-docs/pythoncom__CoInitialize_meth.html
+ 33- https://timgolden.me.uk/pywin32-docs/pythoncom__CoInitializeEx_meth.html
+ 34- https://github.com/Crozzers/screen_brightness_control/issues/42
+ 35'''
+ 36
+ 37
+ 38@contextmanager
+ 39def_wmi_init():
+ 40'''internal function to create and return a wmi instance'''
+ 41com_init=False
+ 42try:
+ 43yieldwmi.WMI(namespace='wmi')
+ 44exceptExceptionase:
+ 45# WMI init will fail outside the main thread, or if CoInitialize wasn't called first
+ 46_logger.debug(f'WMI init failed ({e!r}). Calling CoInitialize and retrying')
+ 47com_init=True
+ 48ifCOM_MODELisNone:
+ 49pythoncom.CoInitialize()
+ 50else:
+ 51pythoncom.CoInitializeEx(COM_MODEL)
+ 52
+ 53yieldwmi.WMI(namespace='wmi')
+ 54
+ 55# only uninitialise if we initialised. Avoid cleaning up resources being used by another library
+ 56ifcom_init:
+ 57pythoncom.CoUninitialize()
+ 58
+ 59
+ 60defenum_display_devices()->Generator[win32api.PyDISPLAY_DEVICEType,None,None]:
+ 61'''
+ 62 Yields all display devices connected to the computer
+ 63 '''
+ 64formonitor_enuminwin32api.EnumDisplayMonitors():
+ 65pyhandle=monitor_enum[0]
+ 66monitor_info=win32api.GetMonitorInfo(pyhandle.handle)
+ 67foradaptor_indexinrange(5):
+ 68try:
+ 69# EDD_GET_DEVICE_INTERFACE_NAME flag to populate DeviceID field
+ 70device=win32api.EnumDisplayDevices(
+ 71monitor_info['Device'],adaptor_index,1)
+ 72exceptpywintypes.error:
+ 73_logger.debug(
+ 74f'failed to get display device {monitor_info["Device"]} on adaptor index {adaptor_index}')
+ 75else:
+ 76yielddevice
+ 77break
+ 78
+ 79
+ 80defget_display_info()->List[dict]:
+ 81'''
+ 82 Gets information about all connected displays using WMI and win32api
+ 83
+ 84 Example:
+ 85 ```python
+ 86 import screen_brightness_control as s
+ 87
+ 88 info = s.windows.get_display_info()
+ 89 for display in info:
+ 90 print(display['name'])
+ 91 ```
+ 92 '''
+ 93info=__cache__.get('windows_monitors_info_raw')
+ 94ifinfoisNone:
+ 95info=[]
+ 96# collect all monitor UIDs (derived from DeviceID)
+ 97monitor_uids={}
+ 98fordeviceinenum_display_devices():
+ 99monitor_uids[device.DeviceID.split('#')[2]]=device
+100
+101# gather list of laptop displays to check against later
+102with_wmi_init()aswmi:
+103try:
+104laptop_displays=[
+105i.InstanceName
+106foriinwmi.WmiMonitorBrightness()
+107]
+108exceptExceptionase:
+109# don't do specific exception classes here because WMI does not play ball with it
+110_logger.warning(
+111f'get_display_info: failed to gather list of laptop displays - {format_exc(e)}')
+112laptop_displays=[]
+113
+114extras,desktop,laptop=[],0,0
+115uid_keys=list(monitor_uids.keys())
+116formonitorinwmi.WmiMonitorDescriptorMethods():
+117model,serial,manufacturer,man_id,edid=None,None,None,None,None
+118instance_name=monitor.InstanceName.replace(
+119'_0','',1).split('\\')[2]
+120try:
+121pydevice=monitor_uids[instance_name]
+122exceptKeyError:
+123# if laptop display WAS connected but was later put to sleep (#33)
+124ifinstance_nameinlaptop_displays:
+125laptop+=1
+126else:
+127desktop+=1
+128_logger.warning(
+129f'display {instance_name!r} is detected but not present in monitor_uids.'
+130' Maybe it is asleep?'
+131)
+132continue
+133
+134# get the EDID
+135try:
+136edid=''.join(
+137f'{char:02x}'forcharinmonitor.WmiGetMonitorRawEEdidV1Block(0)[0])
+138# we do the EDID parsing ourselves because calling wmi.WmiMonitorID
+139# takes too long
+140parsed=EDID.parse(edid)
+141man_id,manufacturer,model,name,serial=parsed
+142ifnameisNone:
+143raiseEDIDParseError(
+144'parsed EDID returned invalid display name')
+145exceptEDIDParseErrorase:
+146edid=None
+147_logger.warning(
+148f'exception parsing edid str for {monitor.InstanceName} - {format_exc(e)}')
+149exceptExceptionase:
+150edid=None
+151_logger.error(
+152f'failed to get EDID string for {monitor.InstanceName} - {format_exc(e)}')
+153finally:
+154ifedidisNone:
+155devid=pydevice.DeviceID.split('#')
+156serial=devid[2]
+157man_id=devid[1][:3]
+158model=devid[1][3:]or'Generic Monitor'
+159deldevid
+160if(brand:=_monitor_brand_lookup(man_id)):
+161man_id,manufacturer=brand
+162
+163if(serial,model)!=(None,None):
+164data:dict={
+165'name':f'{manufacturer}{model}',
+166'model':model,
+167'serial':serial,
+168'manufacturer':manufacturer,
+169'manufacturer_id':man_id,
+170'edid':edid,
+171'uid':uid_match.group(1)if(uid_match:=re.search(r"UID(\d+)",instance_name))elseNone,
+172}
+173ifmonitor.InstanceNameinlaptop_displays:
+174data['index']=laptop
+175data['method']=WMI
+176laptop+=1
+177else:
+178data['method']=VCP
+179desktop+=1
+180
+181ifinstance_nameinuid_keys:
+182# insert the data into the uid_keys list because
+183# uid_keys has the monitors sorted correctly. This
+184# means we don't have to re-sort the list later
+185uid_keys[uid_keys.index(instance_name)]=data
+186else:
+187extras.append(data)
+188
+189info=uid_keys+extras
+190ifdesktop:
+191# now make sure desktop monitors have the correct index
+192count=0
+193foritemininfo:
+194ifitem['method']==VCP:
+195item['index']=count
+196count+=1
+197
+198# return info only which has correct data
+199info=[iforiininfoifisinstance(i,dict)]
+200
+201__cache__.store('windows_monitors_info_raw',info)
+202
+203returninfo
+204
+205
+206classWMI(BrightnessMethod):
+207'''
+208 A collection of screen brightness related methods using the WMI API.
+209 This class primarily works with laptop displays.
+210 '''
+211@classmethod
+212defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+213info=[iforiinget_display_info()ifi['method']==cls]
+214ifdisplayisnotNone:
+215info=filter_monitors(display=display,haystack=info)
+216returninfo
+217
+218@classmethod
+219defset_brightness(cls,value:IntPercentage,display:Optional[int]=None):
+220with_wmi_init()aswmi:
+221brightness_method=wmi.WmiMonitorBrightnessMethods()
+222ifdisplayisnotNone:
+223brightness_method=[brightness_method[display]]
+224
+225formethodinbrightness_method:
+226method.WmiSetBrightness(value,0)
+227
+228@classmethod
+229defget_brightness(cls,display:Optional[int]=None)->List[IntPercentage]:
+230with_wmi_init()aswmi:
+231brightness_method=wmi.WmiMonitorBrightness()
+232ifdisplayisnotNone:
+233brightness_method=[brightness_method[display]]
+234
+235values=[i.CurrentBrightnessforiinbrightness_method]
+236returnvalues
+237
+238
+239classVCP(BrightnessMethod):
+240'''Collection of screen brightness related methods using the DDC/CI commands'''
+241_logger=_logger.getChild('VCP')
+242
+243class_PHYSICAL_MONITOR(Structure):
+244'''internal class, do not call'''
+245_fields_=[('handle',HANDLE),
+246('description',WCHAR*128)]
+247
+248@classmethod
+249defiter_physical_monitors(cls,start:int=0)->Generator[HANDLE,None,None]:
+250'''
+251 A generator to iterate through all physical monitors
+252 and then close them again afterwards, yielding their handles.
+253 It is not recommended to use this function unless you are familiar with `ctypes` and `windll`
+254
+255 Args:
+256 start: skip the first X handles
+257
+258 Raises:
+259 ctypes.WinError: upon failure to enumerate through the monitors
+260 '''
+261# user index keeps track of valid monitors
+262user_index=0
+263# monitor index keeps track of valid and pseudo monitors
+264monitor_index=0
+265display_devices=list(enum_display_devices())
+266
+267with_wmi_init()aswmi:
+268try:
+269laptop_displays=[
+270i.InstanceName.replace('_0','').split('\\')[2]
+271foriinwmi.WmiMonitorBrightness()
+272]
+273exceptExceptionase:
+274cls._logger.warning(
+275f'failed to gather list of laptop displays - {format_exc(e)}')
+276laptop_displays=[]
+277
+278formonitorinmap(lambdam:m[0].handle,win32api.EnumDisplayMonitors()):
+279# Get physical monitor count
+280count=DWORD()
+281ifnotwindll.dxva2.GetNumberOfPhysicalMonitorsFromHMONITOR(monitor,byref(count)):
+282raiseWinError(None,'call to GetNumberOfPhysicalMonitorsFromHMONITOR returned invalid result')
+283ifcount.value>0:
+284# Get physical monitor handles
+285physical_array=(cls._PHYSICAL_MONITOR*count.value)()
+286ifnotwindll.dxva2.GetPhysicalMonitorsFromHMONITOR(monitor,count.value,physical_array):
+287raiseWinError(None,'call to GetPhysicalMonitorsFromHMONITOR returned invalid result')
+288foriteminphysical_array:
+289# check that the monitor is not a pseudo monitor by
+290# checking its StateFlags for the
+291# win32con DISPLAY_DEVICE_ATTACHED_TO_DESKTOP flag
+292ifdisplay_devices[monitor_index].StateFlags&win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP:
+293# check if monitor is actually a laptop display
+294ifdisplay_devices[monitor_index].DeviceID.split('#')[2]notinlaptop_displays:
+295ifstartisNoneoruser_index>=start:
+296yielditem.handle
+297# increment user index as a valid monitor was found
+298user_index+=1
+299# increment monitor index
+300monitor_index+=1
+301windll.dxva2.DestroyPhysicalMonitor(item.handle)
+302
+303@classmethod
+304defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+305info=[iforiinget_display_info()ifi['method']==cls]
+306ifdisplayisnotNone:
+307info=filter_monitors(display=display,haystack=info)
+308returninfo
+309
+310@classmethod
+311defget_brightness(cls,display:Optional[int]=None,max_tries:int=50)->List[IntPercentage]:
+312'''
+313 Args:
+314 display: the index of the specific display to query.
+315 If unspecified, all detected displays are queried
+316 max_tries: the maximum allowed number of attempts to
+317 read the VCP output from the display
+318
+319 Returns:
+320 See `BrightnessMethod.get_brightness`
+321 '''
+322code=BYTE(0x10)
+323values=[]
+324start=displayifdisplayisnotNoneelse0
+325forindex,handleinenumerate(cls.iter_physical_monitors(start=start),start=start):
+326current=__cache__.get(f'vcp_brightness_{index}')
+327ifcurrentisNone:
+328cur_out=DWORD()
+329attempt=0# avoid UnboundLocalError in else clause if max_tries is 0
+330forattemptinrange(max_tries):
+331ifwindll.dxva2.GetVCPFeatureAndVCPFeatureReply(handle,code,None,byref(cur_out),None):
+332current=cur_out.value
+333break
+334current=None
+335time.sleep(0.02ifattempt<20else0.1)
+336else:
+337cls._logger.error(
+338f'failed to get VCP feature reply for display:{index} after {attempt} tries')
+339
+340ifcurrentisnotNone:
+341__cache__.store(
+342f'vcp_brightness_{index}',current,expires=0.1)
+343values.append(current)
+344
+345ifdisplay==index:
+346# if we've got the display we wanted then exit here, no point iterating through all the others.
+347# Cleanup function usually called in iter_physical_monitors won't get called if we break, so call now
+348windll.dxva2.DestroyPhysicalMonitor(handle)
+349break
+350
+351returnvalues
+352
+353@classmethod
+354defset_brightness(cls,value:IntPercentage,display:Optional[int]=None,max_tries:int=50):
+355'''
+356 Args:
+357 value: percentage brightness to set the display to
+358 display: The specific display you wish to query.
+359 max_tries: the maximum allowed number of attempts to
+360 send the VCP input to the display
+361 '''
+362__cache__.expire(startswith='vcp_brightness_')
+363code=BYTE(0x10)
+364value_dword=DWORD(value)
+365start=displayifdisplayisnotNoneelse0
+366forindex,handleinenumerate(cls.iter_physical_monitors(start=start),start=start):
+367attempt=0# avoid UnboundLocalError in else clause if max_tries is 0
+368forattemptinrange(max_tries):
+369ifwindll.dxva2.SetVCPFeature(handle,code,value_dword):
+370break
+371time.sleep(0.02ifattempt<20else0.1)
+372else:
+373cls._logger.error(
+374f'failed to set display:{index}->{value} after {attempt} tries')
+375
+376ifdisplay==index:
+377# we have the display we wanted, exit and cleanup
+378windll.dxva2.DestroyPhysicalMonitor(handle)
+379break
+380
+381
+382deflist_monitors_info(
+383method:Optional[str]=None,allow_duplicates:bool=False,unsupported:bool=False
+384)->List[dict]:
+385'''
+386 Lists detailed information about all detected displays
+387
+388 Args:
+389 method: the method the display can be addressed by. See `.get_methods`
+390 for more info on available methods
+391 allow_duplicates: whether to filter out duplicate displays (displays with the same EDID) or not
+392 unsupported: include detected displays that are invalid or unsupported.
+393 This argument does nothing on Windows
+394 '''
+395# no caching here because get_display_info caches its results
+396info=get_display_info()
+397
+398all_methods=get_methods(method).values()
+399
+400ifmethodisnotNone:
+401info=[iforiininfoifi['method']inall_methods]
+402
+403ifallow_duplicates:
+404returninfo
+405
+406try:
+407# use filter_monitors to remove duplicates
+408returnfilter_monitors(haystack=info)
+409exceptNoValidDisplayError:
+410return[]
+411
+412
+413METHODS=(WMI,VCP)
+
+
+
+
+
+
+
+
+ COM_MODEL =
+None
+
+
+
+
+
+
The concurrency model and flags used when calling pythoncom.CoInitializeEx.
+If left as None the library will call CoInitialize instead with whatever defaults it chooses.
61defenum_display_devices()->Generator[win32api.PyDISPLAY_DEVICEType,None,None]:
+62'''
+63 Yields all display devices connected to the computer
+64 '''
+65formonitor_enuminwin32api.EnumDisplayMonitors():
+66pyhandle=monitor_enum[0]
+67monitor_info=win32api.GetMonitorInfo(pyhandle.handle)
+68foradaptor_indexinrange(5):
+69try:
+70# EDD_GET_DEVICE_INTERFACE_NAME flag to populate DeviceID field
+71device=win32api.EnumDisplayDevices(
+72monitor_info['Device'],adaptor_index,1)
+73exceptpywintypes.error:
+74_logger.debug(
+75f'failed to get display device {monitor_info["Device"]} on adaptor index {adaptor_index}')
+76else:
+77yielddevice
+78break
+
+
+
+
Yields all display devices connected to the computer
81defget_display_info()->List[dict]:
+ 82'''
+ 83 Gets information about all connected displays using WMI and win32api
+ 84
+ 85 Example:
+ 86 ```python
+ 87 import screen_brightness_control as s
+ 88
+ 89 info = s.windows.get_display_info()
+ 90 for display in info:
+ 91 print(display['name'])
+ 92 ```
+ 93 '''
+ 94info=__cache__.get('windows_monitors_info_raw')
+ 95ifinfoisNone:
+ 96info=[]
+ 97# collect all monitor UIDs (derived from DeviceID)
+ 98monitor_uids={}
+ 99fordeviceinenum_display_devices():
+100monitor_uids[device.DeviceID.split('#')[2]]=device
+101
+102# gather list of laptop displays to check against later
+103with_wmi_init()aswmi:
+104try:
+105laptop_displays=[
+106i.InstanceName
+107foriinwmi.WmiMonitorBrightness()
+108]
+109exceptExceptionase:
+110# don't do specific exception classes here because WMI does not play ball with it
+111_logger.warning(
+112f'get_display_info: failed to gather list of laptop displays - {format_exc(e)}')
+113laptop_displays=[]
+114
+115extras,desktop,laptop=[],0,0
+116uid_keys=list(monitor_uids.keys())
+117formonitorinwmi.WmiMonitorDescriptorMethods():
+118model,serial,manufacturer,man_id,edid=None,None,None,None,None
+119instance_name=monitor.InstanceName.replace(
+120'_0','',1).split('\\')[2]
+121try:
+122pydevice=monitor_uids[instance_name]
+123exceptKeyError:
+124# if laptop display WAS connected but was later put to sleep (#33)
+125ifinstance_nameinlaptop_displays:
+126laptop+=1
+127else:
+128desktop+=1
+129_logger.warning(
+130f'display {instance_name!r} is detected but not present in monitor_uids.'
+131' Maybe it is asleep?'
+132)
+133continue
+134
+135# get the EDID
+136try:
+137edid=''.join(
+138f'{char:02x}'forcharinmonitor.WmiGetMonitorRawEEdidV1Block(0)[0])
+139# we do the EDID parsing ourselves because calling wmi.WmiMonitorID
+140# takes too long
+141parsed=EDID.parse(edid)
+142man_id,manufacturer,model,name,serial=parsed
+143ifnameisNone:
+144raiseEDIDParseError(
+145'parsed EDID returned invalid display name')
+146exceptEDIDParseErrorase:
+147edid=None
+148_logger.warning(
+149f'exception parsing edid str for {monitor.InstanceName} - {format_exc(e)}')
+150exceptExceptionase:
+151edid=None
+152_logger.error(
+153f'failed to get EDID string for {monitor.InstanceName} - {format_exc(e)}')
+154finally:
+155ifedidisNone:
+156devid=pydevice.DeviceID.split('#')
+157serial=devid[2]
+158man_id=devid[1][:3]
+159model=devid[1][3:]or'Generic Monitor'
+160deldevid
+161if(brand:=_monitor_brand_lookup(man_id)):
+162man_id,manufacturer=brand
+163
+164if(serial,model)!=(None,None):
+165data:dict={
+166'name':f'{manufacturer}{model}',
+167'model':model,
+168'serial':serial,
+169'manufacturer':manufacturer,
+170'manufacturer_id':man_id,
+171'edid':edid,
+172'uid':uid_match.group(1)if(uid_match:=re.search(r"UID(\d+)",instance_name))elseNone,
+173}
+174ifmonitor.InstanceNameinlaptop_displays:
+175data['index']=laptop
+176data['method']=WMI
+177laptop+=1
+178else:
+179data['method']=VCP
+180desktop+=1
+181
+182ifinstance_nameinuid_keys:
+183# insert the data into the uid_keys list because
+184# uid_keys has the monitors sorted correctly. This
+185# means we don't have to re-sort the list later
+186uid_keys[uid_keys.index(instance_name)]=data
+187else:
+188extras.append(data)
+189
+190info=uid_keys+extras
+191ifdesktop:
+192# now make sure desktop monitors have the correct index
+193count=0
+194foritemininfo:
+195ifitem['method']==VCP:
+196item['index']=count
+197count+=1
+198
+199# return info only which has correct data
+200info=[iforiininfoifisinstance(i,dict)]
+201
+202__cache__.store('windows_monitors_info_raw',info)
+203
+204returninfo
+
+
+
+
Gets information about all connected displays using WMI and win32api
240classVCP(BrightnessMethod):
+241'''Collection of screen brightness related methods using the DDC/CI commands'''
+242_logger=_logger.getChild('VCP')
+243
+244class_PHYSICAL_MONITOR(Structure):
+245'''internal class, do not call'''
+246_fields_=[('handle',HANDLE),
+247('description',WCHAR*128)]
+248
+249@classmethod
+250defiter_physical_monitors(cls,start:int=0)->Generator[HANDLE,None,None]:
+251'''
+252 A generator to iterate through all physical monitors
+253 and then close them again afterwards, yielding their handles.
+254 It is not recommended to use this function unless you are familiar with `ctypes` and `windll`
+255
+256 Args:
+257 start: skip the first X handles
+258
+259 Raises:
+260 ctypes.WinError: upon failure to enumerate through the monitors
+261 '''
+262# user index keeps track of valid monitors
+263user_index=0
+264# monitor index keeps track of valid and pseudo monitors
+265monitor_index=0
+266display_devices=list(enum_display_devices())
+267
+268with_wmi_init()aswmi:
+269try:
+270laptop_displays=[
+271i.InstanceName.replace('_0','').split('\\')[2]
+272foriinwmi.WmiMonitorBrightness()
+273]
+274exceptExceptionase:
+275cls._logger.warning(
+276f'failed to gather list of laptop displays - {format_exc(e)}')
+277laptop_displays=[]
+278
+279formonitorinmap(lambdam:m[0].handle,win32api.EnumDisplayMonitors()):
+280# Get physical monitor count
+281count=DWORD()
+282ifnotwindll.dxva2.GetNumberOfPhysicalMonitorsFromHMONITOR(monitor,byref(count)):
+283raiseWinError(None,'call to GetNumberOfPhysicalMonitorsFromHMONITOR returned invalid result')
+284ifcount.value>0:
+285# Get physical monitor handles
+286physical_array=(cls._PHYSICAL_MONITOR*count.value)()
+287ifnotwindll.dxva2.GetPhysicalMonitorsFromHMONITOR(monitor,count.value,physical_array):
+288raiseWinError(None,'call to GetPhysicalMonitorsFromHMONITOR returned invalid result')
+289foriteminphysical_array:
+290# check that the monitor is not a pseudo monitor by
+291# checking its StateFlags for the
+292# win32con DISPLAY_DEVICE_ATTACHED_TO_DESKTOP flag
+293ifdisplay_devices[monitor_index].StateFlags&win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP:
+294# check if monitor is actually a laptop display
+295ifdisplay_devices[monitor_index].DeviceID.split('#')[2]notinlaptop_displays:
+296ifstartisNoneoruser_index>=start:
+297yielditem.handle
+298# increment user index as a valid monitor was found
+299user_index+=1
+300# increment monitor index
+301monitor_index+=1
+302windll.dxva2.DestroyPhysicalMonitor(item.handle)
+303
+304@classmethod
+305defget_display_info(cls,display:Optional[DisplayIdentifier]=None)->List[dict]:
+306info=[iforiinget_display_info()ifi['method']==cls]
+307ifdisplayisnotNone:
+308info=filter_monitors(display=display,haystack=info)
+309returninfo
+310
+311@classmethod
+312defget_brightness(cls,display:Optional[int]=None,max_tries:int=50)->List[IntPercentage]:
+313'''
+314 Args:
+315 display: the index of the specific display to query.
+316 If unspecified, all detected displays are queried
+317 max_tries: the maximum allowed number of attempts to
+318 read the VCP output from the display
+319
+320 Returns:
+321 See `BrightnessMethod.get_brightness`
+322 '''
+323code=BYTE(0x10)
+324values=[]
+325start=displayifdisplayisnotNoneelse0
+326forindex,handleinenumerate(cls.iter_physical_monitors(start=start),start=start):
+327current=__cache__.get(f'vcp_brightness_{index}')
+328ifcurrentisNone:
+329cur_out=DWORD()
+330attempt=0# avoid UnboundLocalError in else clause if max_tries is 0
+331forattemptinrange(max_tries):
+332ifwindll.dxva2.GetVCPFeatureAndVCPFeatureReply(handle,code,None,byref(cur_out),None):
+333current=cur_out.value
+334break
+335current=None
+336time.sleep(0.02ifattempt<20else0.1)
+337else:
+338cls._logger.error(
+339f'failed to get VCP feature reply for display:{index} after {attempt} tries')
+340
+341ifcurrentisnotNone:
+342__cache__.store(
+343f'vcp_brightness_{index}',current,expires=0.1)
+344values.append(current)
+345
+346ifdisplay==index:
+347# if we've got the display we wanted then exit here, no point iterating through all the others.
+348# Cleanup function usually called in iter_physical_monitors won't get called if we break, so call now
+349windll.dxva2.DestroyPhysicalMonitor(handle)
+350break
+351
+352returnvalues
+353
+354@classmethod
+355defset_brightness(cls,value:IntPercentage,display:Optional[int]=None,max_tries:int=50):
+356'''
+357 Args:
+358 value: percentage brightness to set the display to
+359 display: The specific display you wish to query.
+360 max_tries: the maximum allowed number of attempts to
+361 send the VCP input to the display
+362 '''
+363__cache__.expire(startswith='vcp_brightness_')
+364code=BYTE(0x10)
+365value_dword=DWORD(value)
+366start=displayifdisplayisnotNoneelse0
+367forindex,handleinenumerate(cls.iter_physical_monitors(start=start),start=start):
+368attempt=0# avoid UnboundLocalError in else clause if max_tries is 0
+369forattemptinrange(max_tries):
+370ifwindll.dxva2.SetVCPFeature(handle,code,value_dword):
+371break
+372time.sleep(0.02ifattempt<20else0.1)
+373else:
+374cls._logger.error(
+375f'failed to set display:{index}->{value} after {attempt} tries')
+376
+377ifdisplay==index:
+378# we have the display we wanted, exit and cleanup
+379windll.dxva2.DestroyPhysicalMonitor(handle)
+380break
+
+
+
+
Collection of screen brightness related methods using the DDC/CI commands
249@classmethod
+250defiter_physical_monitors(cls,start:int=0)->Generator[HANDLE,None,None]:
+251'''
+252 A generator to iterate through all physical monitors
+253 and then close them again afterwards, yielding their handles.
+254 It is not recommended to use this function unless you are familiar with `ctypes` and `windll`
+255
+256 Args:
+257 start: skip the first X handles
+258
+259 Raises:
+260 ctypes.WinError: upon failure to enumerate through the monitors
+261 '''
+262# user index keeps track of valid monitors
+263user_index=0
+264# monitor index keeps track of valid and pseudo monitors
+265monitor_index=0
+266display_devices=list(enum_display_devices())
+267
+268with_wmi_init()aswmi:
+269try:
+270laptop_displays=[
+271i.InstanceName.replace('_0','').split('\\')[2]
+272foriinwmi.WmiMonitorBrightness()
+273]
+274exceptExceptionase:
+275cls._logger.warning(
+276f'failed to gather list of laptop displays - {format_exc(e)}')
+277laptop_displays=[]
+278
+279formonitorinmap(lambdam:m[0].handle,win32api.EnumDisplayMonitors()):
+280# Get physical monitor count
+281count=DWORD()
+282ifnotwindll.dxva2.GetNumberOfPhysicalMonitorsFromHMONITOR(monitor,byref(count)):
+283raiseWinError(None,'call to GetNumberOfPhysicalMonitorsFromHMONITOR returned invalid result')
+284ifcount.value>0:
+285# Get physical monitor handles
+286physical_array=(cls._PHYSICAL_MONITOR*count.value)()
+287ifnotwindll.dxva2.GetPhysicalMonitorsFromHMONITOR(monitor,count.value,physical_array):
+288raiseWinError(None,'call to GetPhysicalMonitorsFromHMONITOR returned invalid result')
+289foriteminphysical_array:
+290# check that the monitor is not a pseudo monitor by
+291# checking its StateFlags for the
+292# win32con DISPLAY_DEVICE_ATTACHED_TO_DESKTOP flag
+293ifdisplay_devices[monitor_index].StateFlags&win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP:
+294# check if monitor is actually a laptop display
+295ifdisplay_devices[monitor_index].DeviceID.split('#')[2]notinlaptop_displays:
+296ifstartisNoneoruser_index>=start:
+297yielditem.handle
+298# increment user index as a valid monitor was found
+299user_index+=1
+300# increment monitor index
+301monitor_index+=1
+302windll.dxva2.DestroyPhysicalMonitor(item.handle)
+
+
+
+
A generator to iterate through all physical monitors
+and then close them again afterwards, yielding their handles.
+It is not recommended to use this function unless you are familiar with ctypes and windll
+
+
Arguments:
+
+
+
start: skip the first X handles
+
+
+
Raises:
+
+
+
ctypes.WinError: upon failure to enumerate through the monitors
311@classmethod
+312defget_brightness(cls,display:Optional[int]=None,max_tries:int=50)->List[IntPercentage]:
+313'''
+314 Args:
+315 display: the index of the specific display to query.
+316 If unspecified, all detected displays are queried
+317 max_tries: the maximum allowed number of attempts to
+318 read the VCP output from the display
+319
+320 Returns:
+321 See `BrightnessMethod.get_brightness`
+322 '''
+323code=BYTE(0x10)
+324values=[]
+325start=displayifdisplayisnotNoneelse0
+326forindex,handleinenumerate(cls.iter_physical_monitors(start=start),start=start):
+327current=__cache__.get(f'vcp_brightness_{index}')
+328ifcurrentisNone:
+329cur_out=DWORD()
+330attempt=0# avoid UnboundLocalError in else clause if max_tries is 0
+331forattemptinrange(max_tries):
+332ifwindll.dxva2.GetVCPFeatureAndVCPFeatureReply(handle,code,None,byref(cur_out),None):
+333current=cur_out.value
+334break
+335current=None
+336time.sleep(0.02ifattempt<20else0.1)
+337else:
+338cls._logger.error(
+339f'failed to get VCP feature reply for display:{index} after {attempt} tries')
+340
+341ifcurrentisnotNone:
+342__cache__.store(
+343f'vcp_brightness_{index}',current,expires=0.1)
+344values.append(current)
+345
+346ifdisplay==index:
+347# if we've got the display we wanted then exit here, no point iterating through all the others.
+348# Cleanup function usually called in iter_physical_monitors won't get called if we break, so call now
+349windll.dxva2.DestroyPhysicalMonitor(handle)
+350break
+351
+352returnvalues
+
+
+
+
Arguments:
+
+
+
display: the index of the specific display to query.
+If unspecified, all detected displays are queried
+
max_tries: the maximum allowed number of attempts to
+read the VCP output from the display
354@classmethod
+355defset_brightness(cls,value:IntPercentage,display:Optional[int]=None,max_tries:int=50):
+356'''
+357 Args:
+358 value: percentage brightness to set the display to
+359 display: The specific display you wish to query.
+360 max_tries: the maximum allowed number of attempts to
+361 send the VCP input to the display
+362 '''
+363__cache__.expire(startswith='vcp_brightness_')
+364code=BYTE(0x10)
+365value_dword=DWORD(value)
+366start=displayifdisplayisnotNoneelse0
+367forindex,handleinenumerate(cls.iter_physical_monitors(start=start),start=start):
+368attempt=0# avoid UnboundLocalError in else clause if max_tries is 0
+369forattemptinrange(max_tries):
+370ifwindll.dxva2.SetVCPFeature(handle,code,value_dword):
+371break
+372time.sleep(0.02ifattempt<20else0.1)
+373else:
+374cls._logger.error(
+375f'failed to set display:{index}->{value} after {attempt} tries')
+376
+377ifdisplay==index:
+378# we have the display we wanted, exit and cleanup
+379windll.dxva2.DestroyPhysicalMonitor(handle)
+380break
+
+
+
+
Arguments:
+
+
+
value: percentage brightness to set the display to
+
display: The specific display you wish to query.
+
max_tries: the maximum allowed number of attempts to
+send the VCP input to the display
383deflist_monitors_info(
+384method:Optional[str]=None,allow_duplicates:bool=False,unsupported:bool=False
+385)->List[dict]:
+386'''
+387 Lists detailed information about all detected displays
+388
+389 Args:
+390 method: the method the display can be addressed by. See `.get_methods`
+391 for more info on available methods
+392 allow_duplicates: whether to filter out duplicate displays (displays with the same EDID) or not
+393 unsupported: include detected displays that are invalid or unsupported.
+394 This argument does nothing on Windows
+395 '''
+396# no caching here because get_display_info caches its results
+397info=get_display_info()
+398
+399all_methods=get_methods(method).values()
+400
+401ifmethodisnotNone:
+402info=[iforiininfoifi['method']inall_methods]
+403
+404ifallow_duplicates:
+405returninfo
+406
+407try:
+408# use filter_monitors to remove duplicates
+409returnfilter_monitors(haystack=info)
+410exceptNoValidDisplayError:
+411return[]
+
+
+
+
Lists detailed information about all detected displays
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/0.24.1/search.js b/docs/0.24.1/search.js
new file mode 100644
index 0000000..d06e241
--- /dev/null
+++ b/docs/0.24.1/search.js
@@ -0,0 +1,46 @@
+window.pdocSearch = (function(){
+/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o
Returns the current brightness of one or more displays
\n\n
Arguments:
\n\n
\n
display (.types.DisplayIdentifier): the specific display to query
\n
method: the method to use to get the brightness. See get_methods for\nmore info on available methods
\n
allow_duplicates: controls whether to filter out duplicate displays or not.
\n
verbose_error: controls the level of detail in the error messages
\n
\n\n
Returns:
\n\n
\n
A list of .types.IntPercentage values, each being the brightness of an\n individual display. Invalid displays may return None.
\n
\n\n
Example:
\n\n
\n
\n
importscreen_brightness_controlassbc\n\n# get the current screen brightness (for all detected displays)\ncurrent_brightness=sbc.get_brightness()\n\n# get the brightness of the primary display\nprimary_brightness=sbc.get_brightness(display=0)\n\n# get the brightness of the secondary display (if connected)\nsecondary_brightness=sbc.get_brightness(display=1)\n
Sets the brightness level of one or more displays to a given value.
\n\n
Arguments:
\n\n
\n
value (.types.Percentage): the new brightness level
\n
display (.types.DisplayIdentifier): the specific display to adjust
\n
method: the method to use to set the brightness. See get_methods for\nmore info on available methods
\n
force: [Linux Only] if False the brightness will never be set lower than 1.\nThis is because on most displays a brightness of 0 will turn off the backlight.\nIf True, this check is bypassed
\n
allow_duplicates: controls whether to filter out duplicate displays or not.
\n
verbose_error: boolean value controls the amount of detail error messages will contain
\n
no_return: don't return the new brightness level(s)
\n
\n\n
Returns:
\n\n
\n
If no_return is set to True (the default) then this function returns nothing.\n Otherwise, a list of .types.IntPercentage is returned, each item being the new\n brightness of each adjusted display (invalid displays may return None)
\n
\n\n
Example:
\n\n
\n
\n
importscreen_brightness_controlassbc\n\n# set brightness to 50%\nsbc.set_brightness(50)\n\n# set brightness to 0%\nsbc.set_brightness(0,force=True)\n\n# increase brightness by 25%\nsbc.set_brightness('+25')\n\n# decrease brightness by 30%\nsbc.set_brightness('-30')\n\n# set the brightness of display 0 to 50%\nsbc.set_brightness(50,display=0)\n
Gradually change the brightness of one or more displays
\n\n
Arguments:
\n\n
\n
finish (.types.Percentage): fade to this brightness level
\n
start (.types.Percentage): where the brightness should fade from.\nIf this arg is not specified, the fade will be started from the\ncurrent brightness.
\n
interval: the time delay between each step in brightness
\n
increment: the amount to change the brightness by per step
\n
blocking: whether this should occur in the main thread (True) or a new daemonic thread (False)
\n
force: [Linux Only] if False the brightness will never be set lower than 1.\nThis is because on most displays a brightness of 0 will turn off the backlight.\nIf True, this check is bypassed
\n
logarithmic: follow a logarithmic brightness curve when adjusting the brightness
\n
stoppable: whether the fade can be stopped by starting a new fade on the same display
\n
**kwargs: passed through to filter_monitors for display selection.\nWill also be passed to get_brightness if blocking is True
\n
\n\n
Returns:
\n\n
\n
By default, this function calls get_brightness() to return the new\n brightness of any adjusted displays.
\n \n
If blocking is set to False, then a list of threads are\n returned, one for each display being faded.
\n
\n\n
Example:
\n\n
\n
\n
importscreen_brightness_controlassbc\n\n# fade brightness from the current brightness to 50%\nsbc.fade_brightness(50)\n\n# fade the brightness from 25% to 75%\nsbc.fade_brightness(75,start=25)\n\n# fade the brightness from the current value to 100% in steps of 10%\nsbc.fade_brightness(100,increment=10)\n\n# fade the brightness from 100% to 90% with time intervals of 0.1 seconds\nsbc.fade_brightness(90,start=100,interval=0.1)\n\n# fade the brightness to 100% in a new thread\nsbc.fade_brightness(100,blocking=False)\n
List detailed information about all displays that are controllable by this library
\n\n
Arguments:
\n\n
\n
method: the method to use to list the available displays. See get_methods for\nmore info on available methods
\n
allow_duplicates: controls whether to filter out duplicate displays or not.
\n
unsupported: include detected displays that are invalid or unsupported
\n
\n\n
Returns:
\n\n
\n
list: list of dictionaries containing information about the detected displays
\n
\n\n
Example:
\n\n
\n
\n
importscreen_brightness_controlassbc\ndisplays=sbc.list_monitors_info()\nfordisplayindisplays:\n print('=======================')\n # the manufacturer name plus the model\n print('Name:',display['name'])\n # the general model of the display\n print('Model:',display['model'])\n # the serial of the display\n print('Serial:',display['serial'])\n # the name of the brand of the display\n print('Manufacturer:',display['manufacturer'])\n # the 3 letter code corresponding to the brand name, EG: BNQ -> BenQ\n print('Manufacturer ID:',display['manufacturer_id'])\n # the index of that display FOR THE SPECIFIC METHOD THE DISPLAY USES\n print('Index:',display['index'])\n # the method this display can be addressed by\n print('Method:',display['method'])\n # the EDID string associated with that display\n print('EDID:',display['edid'])\n # The UID of the display\n print('UID:',display['uid'])\n
The index of the display relative to the method it uses.\nSo if the index is 0 and the method is windows.VCP, then this is the 1st\ndisplay reported by windows.VCP, not the first display overall.
Gradually change the brightness of this display to a set value.\nCan execute in the current thread, blocking until completion,\nor in a separate thread, allowing concurrent operations.\nWhen set as non-blocking and stoppable, a new fade can halt the this operation.
\n\n
Arguments:
\n\n
\n
finish (.types.Percentage): the brightness level to end up on
\n
start (.types.Percentage): where the fade should start from. Defaults\nto whatever the current brightness level for the display is
\n
interval: time delay between each change in brightness
\n
increment: amount to change the brightness by each time (as a percentage)
\n
force: [Linux only] allow the brightness to be set to 0. By default,\nbrightness values will never be set lower than 1, since setting them to 0\noften turns off the backlight
\n
logarithmic: follow a logarithmic curve when setting brightness values.\nSee logarithmic_range for rationale
\n
blocking: run this function in the current thread and block until it completes
\n
stoppable: whether this fade will be stopped by starting a new fade on the same display
\n
\n\n
Returns:
\n\n
\n
If blocking is False, returns a threading.Thread object representing the\n thread in which the fade operation is running.\n Otherwise, it returns None.
Returns the .types.DisplayIdentifier for this display.\nWill iterate through the UID, EDID, serial, name and index and return the first\nvalue that is not equal to None
\n\n
Returns:
\n\n
\n
The name of the property returned and the value of said property.\n EG: ('serial', '123abc...') or ('name', 'BenQ GL2450H')
Sets the brightness for this display. See set_brightness for the full docs
\n\n
Arguments:
\n\n
\n
value (.types.Percentage): the brightness percentage to set the display to
\n
force: allow the brightness to be set to 0 on Linux. This is disabled by default\nbecause setting the brightness of 0 will often turn off the backlight
Searches through the information for all detected displays\nand attempts to return the info matching the value given.\nWill attempt to match against index, name, edid, method and serial
\n\n
Arguments:
\n\n
\n
display (.types.DisplayIdentifier): the display you are searching for
\n
haystack: the information to filter from.\nIf this isn't set it defaults to the return of list_monitors_info
\n
method: the method the monitors use. See get_methods for\nmore info on available methods
\n
include: extra fields of information to sort by
\n
allow_duplicates: controls whether to filter out duplicate displays or not
\n
\n\n
Raises:
\n\n
\n
NoValidDisplayError: if the display does not have a match
Takes an EDID string and parses some relevant information from it according to the\nEDID 1.4\nspecification on Wikipedia.
\n\n
Arguments:
\n\n
\n
edid (bytes or str): the EDID, can either be raw bytes or\na hex formatted string (00 ff ff ff ff...)
\n
\n\n
Returns:
\n\n
\n
tuple[str | None]: A tuple of 5 items representing the display's manufacturer ID,\n manufacturer, model, name, serial in that order.\n If any of these values are unable to be determined, they will be None.\n Otherwise, expect a string
\n
\n\n
Raises:
\n\n
\n
EDIDParseError: if the EDID info cannot be unpacked
A range-like function that yields a sequence of integers following\na logarithmic curve (y = 10 ^ (x / 50)) from start (inclusive) to\nstop (inclusive).
\n\n
This is useful because it skips many of the higher percentages in the\nsequence where single percent brightness changes are hard to notice.
\n\n
This function is designed to deal with brightness percentages, and so\nwill never return a value less than 0 or greater than 100.
\n\n
Arguments:
\n\n
\n
start: the start of your percentage range
\n
stop: the end of your percentage range
\n
step: the increment per iteration through the sequence
Convenience function to convert a brightness value into a percentage. Can handle\nintegers, floats and strings. Also can handle relative strings (eg: '+10' or '-10')
\n\n
Arguments:
\n\n
\n
value: the brightness value to convert
\n
current: the current brightness value or a function that returns the current brightness\nvalue. Used when dealing with relative brightness values
\n
lower_bound: the minimum value the brightness can be set to
\n
\n\n
Returns:
\n\n
\n
.types.IntPercentage: The new brightness percentage, between lower_bound and 100
In the same spirit as SysFiles, this class serves as a way of getting\ndisplay information and adjusting the brightness without relying on any\n3rd party software.
\n\n
Usage of this class requires read and write permission for /dev/i2c-*.
\n\n
This class works over the I2C bus, primarily with desktop monitors as I\nhaven't tested any e-DP displays yet.
\n\n
Massive thanks to siemer for\nhis work on the ddcci.py project,\nwhich served as a my main reference for this.
It is recommended to use setvcp to set VCP values on the DDC device\ninstead of using this function directly.
\n\n
Arguments:
\n\n
\n
*args: variable length list of arguments. This will be put\ninto a bytearray and wrapped up in various flags and\nchecksums before being written to the I2C device
Submodule containing types and type aliases used throughout the library.
\n\n
Splitting these definitions into a seperate submodule allows for detailed\nexplanations and verbose type definitions, without cluttering up the rest\nof the library.
\n\n
This file is also useful for wrangling types based on the current Python\nversion.
An IntPercentage or a string representing an IntPercentage.
\n\n
String values may come in two forms:
\n\n
\n
Absolute values: for example '40' converts directly to int('40')
\n
Relative values: strings prefixed with +/- will be interpreted relative to the\ncurrent brightness level. In this case, the integer value of your string will be added to the\ncurrent brightness level.\nFor example, if the current brightness is 50%, a value of '+40' would imply 90% brightness\nand a value of '-40' would imply 10% brightness.
\n
\n\n
Relative brightness values will usually be resolved by the .helpers.percentage function.
The concurrency model and flags used when calling pythoncom.CoInitializeEx.\nIf left as None the library will call CoInitialize instead with whatever defaults it chooses.
A generator to iterate through all physical monitors\nand then close them again afterwards, yielding their handles.\nIt is not recommended to use this function unless you are familiar with ctypes and windll
\n\n
Arguments:
\n\n
\n
start: skip the first X handles
\n
\n\n
Raises:
\n\n
\n
ctypes.WinError: upon failure to enumerate through the monitors