""" Automatic screen display brightness dimmer for Windows XP and Nvidia users by tnl {tnl ta asia tod com} Script tested using the following: - WinXP Pro SP2 - XFX Nvidia Geforce 7600 GT - Nvidia Forceware drivers (version 178.13) - Python 2.5.1 This script is meant for computer addicts whose computers stay in one area. It is meant to make the user more aware of what time it is and that sleep is unfortunately a required biological process. What it does: This python script runs in the background, periodically updating the apparent screen brightness of your display(s) according to a daily schedule. It works by telling the Nvidia chipset to modify its brightness and contrast settings, for the desktop and DirectX apps. It does not necessarily modify brightness/contrast for video playback overlays. Optionally, the script also modifies Windows' own gamma ramp (Note: doing this affects all displays) Ideally, one could control the brightness and contrast through the hardware of the actual displays to make better use of a contrast ratio, but this cannot be done, and people might just be upset with their time spent with calibration anyhow. Requirements: Nvidia-based graphics and recent video drivers (this is actually optional if the gamma ramp is suitable enough and works) (anyone who wants this for ATI and can help is free to contribute) WinXP... not so sure about WinVista but you can test it for me :/ Python 2.5 or newer I s'pose... (use the windows installer from python.org) Configuration: Edit nvautodim.pyw (this file) below. You can also set it to only affect a single monitor if you have more than one. Usage: Execute nvautodim.pyw when configured. If acceptable, you can place it in your startup folder or a shortcut to it there. Hints: You can rename the script to .py to debug it or test configurations to see that the script still works. That way you don't have to kill pythonw.exe to restart it You can force an update (according to the schedule) by passing a negative number as an argument. This is useful if you bind it to actions such as resuming standby/hibernation. Also, you can manually set the brightness by passing in a number between 0 and 1.0 as an argument. Experimental option: during daytime it is possible to slightly affect dimming according to weather, assuming you have an internet connection (updates once per hour) Issues: Recent Forceware drivers (from around late 2007 onwards) flicker when setting desktop configuration. So your screen may flicker when updates occur. The API used is supposedly unsupported now. Hmm, possibly disregard. Maybe there's only flicker when not setting the gamma ramp at the same time :s History: 02/24/2009 Minor fix, instructional changes ... script still tends to exit out unexpectedly??? 12/02/2008 Weather updates only during daytime 12/01/2008 Reworked scheduling loop to use Timers (new to me so hopefully correct) 11/29/2008 Added optional weather.com bias 11/28/2008 Initial release """ # Configuration # in minutes update_interval = 6 # True or False: adjust Windows' gamma ramp (affects all displays!) do_adjust_gamma = True # which display to affect: can be 'all' or a single ID: '0' or '1' or '2' or ... # (if you need more than one but not all then you'll need to edit the script) mons = 'all' # brightness levels desired for different hours of day # ranges from 0 (darkest) to 1.0 (brightest) dim_values = ( 0, # 00 midnight 0, # 01 0, # 02 0, # 03 0, # 04 0, # 05 .1, # 06 .60, # 07 .85, # 08 .95, # 09 1.0, # 10 1.0, # 11 1.0, # 12 noon 1.0, # 13 1.0, # 14 1.0, # 15 1.0, # 16 1.0, # 17 .95, # 18 .90, # 19 .85, # 20 .50, # 21 .20, # 22 0 # 23 ) # Advanced configuration # The following brightness ranges define when operations should execute. # It is possible to limit adjustment through the nvidia chipset # if gamma adjustment is preferred for initial dimming toward 0.5 # (this is the recommendation since flickering is a known issue) # range for which to apply gamma ramp adjustment gamma_range = (0.5, 1) # range for which to apply nvidia adjustment nvidia_range = (0, 0.5) # in minutes. use 0 to disable. a reasonable value might be 20 update_interval_weather = 0 # requires a weather location code for weather.com # example: EIXX0014 for Dublin, IRELAND. # go to weather.com, enter a location, and take note of the URL # example: search for Tokyo, and get http://www.weather.com/outlook/travel/businesstraveler/local/JAXX0085?lswe=Tokyo,%20JAPAN&lwsa=Weather36HourBusinessTravelerCommand&from=searchbox_typeahead # -- the location code is JAXX0085 weather_location = 'XXXXXXXX' # override manual values schedule_on_sun = True # End of configuration from threading import Timer from ctypes import windll, c_uint16 import sys from os import environ from os.path import exists from datetime import datetime if update_interval_weather > 0: import re import urllib2 from urllib2 import URLError """ Functions for setting screen display gamma, brightness, contrast """ class Dimmer: def __init__(self): self.previous_gamma = -1 self.previous_nvidia = 1 """ function adapted from http://www.nirsoft.net/vc/change_screen_brightness.html kudos to http://markmail.org/message/lcdqaeu5g6dvgf24 to avoid ctypes problem of nested WORD arrays incidentally i can't get this to work where gammas differ per color channel """ def set_gamma(self, b): r = -2 if b != self.previous_gamma and 0 <= b and b <= 128: b = int(b) r = -1 dc = windll.user32.GetDC(0) if dc: buf = (3 * 256 * c_uint16)() for i in range(256): gamma = i * (b + 128) if gamma > 65535: gamma = 65535 buf[i] = buf[256+i] = buf[512+i] = gamma r = windll.gdi32.SetDeviceGammaRamp(dc, buf) print 'set_gamma', b self.previous_gamma = b windll.user32.ReleaseDC(0, dc) return r """ this nvidia control panel dll function used is in the nvcpl.dll api (google it) too bad google also lists a result saying this dll is deprecated for win vista """ def set_nvidia(self, v): # v between -125 and 0 v = int(v) if windll.nvcpl and self.previous_nvidia != v: windll.nvcpl.dtcfgex('setcontrast %s all %d' % (mons, v)) windll.nvcpl.dtcfgex('setbrightness %s all %d' % (mons, v)) print 'set_nvidia', v self.previous_nvidia = v def set(self, v): v = float(v) if 0 <= v and v <= 1: print 'setting', v if do_adjust_gamma: if v < gamma_range[0]: self.set_gamma(0) elif v > gamma_range[1]: self.set_gamma(1) else: self.set_gamma((v - gamma_range[0])/(gamma_range[1] - gamma_range[0]) * 128) if v < nvidia_range[0]: self.set_nvidia(-125) elif v > nvidia_range[1]: self.set_nvidia(0) else: self.set_nvidia((v - nvidia_range[0])/(nvidia_range[1] - nvidia_range[0])*125-125) """ Get the current weather's light level for daytime """ class WeatherLight: def __init__(self, location): self.location = location self.time = datetime(2001,2,3) """ Consider the current weather condition and determine light level, ignoring time of day """ def getdim(self, icon): if icon == 'na': return -1 # sign up for weather.com xml oap and figure out true icon ID meanings dim = (2,6,11,13,14,15,31,37,38,41,42,43) fair = (20,22,23,24,25,26,33,39) bright = (28,30,32,34,36,44) icon = int(icon) if icon in bright: return 1.0 elif icon in fair: return .96 elif icon in dim: return .93 else: return .87 """ Take weather.com's hour:minute AM/PM format and convert to military time int """ def gettime(self, text): match = re.match(r'(.+):(.+) (.)', text) h = m = 0 if match: h = int(match.group(1)) m = int(match.group(2)) ap = match.group(3) if ap == 'A' and h == 12: h = 0 elif ap == 'P' and h < 12: h = 12 + h return h*100 + m """ Cheap means of getting the first found xml element """ # not the best means? but it should work def getfirst(self, text, tag): m = re.search(r'<%s>([^<>]+)' % (tag, tag), text) return m and m.group(1) or '' def getWeather(self): response = False req = urllib2.Request('http://xoap.weather.com/weather/local/%s?cc=*' % self.location) try: if req: response = urllib2.urlopen(req) xml = response and response.read() or False if xml: sunr = self.gettime(self.getfirst(xml, 'sunr')) suns = self.gettime(self.getfirst(xml, 'suns')) light = self.getdim(self.getfirst(xml, 'icon')) return sunr, suns, light else: print 'Error, could not read XML response' except URLError: print 'Error in requesting weather URL.' print '*** Failed to get weather.' return 800, 1600, 1.0 """ The point of this class, to determine current light level for daytime """ def getLightLevel(self): """ cache file contains expiry date (next day) sunrise sunset cases: cache does not exist: establish cache request weather set sunr, suns, datetime(today) cache exists (always true if requesting didn't fail) expiration based on day change ...expired: establish cache ...not expired: ...nighttime: no not request weather ...daytime: request weather """ cache = r'%s\weather.light' % environ['TEMP'] now = self.time.now() update = False if not exists(cache): print 'Cache not found [1]' f = open(cache, 'w') if f: sunr, suns, light = self.getWeather() f.write('%d %d %d %d %d' % (now.year, now.month, now.day, sunr, suns)) f.close() print 'Cache created. [1]' else: print '*** Failed to write cache [1]' return 1.0 if exists(cache): f = open(cache, 'r') if f: line = f.read() f.close() m = re.match(r'(.+) (.+) (.+) (.+) (.+)', line) if m: if datetime(now.year, now.month, now.day) > datetime(int(m.group(1)), int(m.group(2)), int(m.group(3))): print 'Cache expired: establishing sunr/suns from server' f = open(cache, 'w') if f: sunr, suns, light = self.getWeather() f.write('%d %d %d %d %d' % (now.year, now.month, now.day, sunr, suns)) f.close() print 'Cache created. [2]' else: print '*** Failed to write cache [2]' return 1.0 else: print 'Using cached sunr/sun' sunr = int(m.group(4)) suns = int(m.group(5)) light = 1.0 else: print 'Cache corrupt' f = open(cache, 'w') if f: sunr, suns, light = self.getWeather() f.write('%d %d %d %d %d' % (now.year, now.month, now.day, sunr, suns)) f.close() print 'Cache created. [3]' else: print '*** Failed to write cache [3]' return 1.0 else: print 'Cache not found [2]' now = now.hour*100 + now.minute if sunr <= now and now < suns: print 'Still daytime, getting weather condition from server' sunr, suns, light = self.getWeather() return light """ Something that updates weather and updates screen in separate threads """ class DimmerScheduler: def __init__(self, dimmerInterval, dim_values, weatherInterval, weatherLocation): self.time = datetime(2001,2,3) self.bias = 1.0 self.dimmer = Dimmer() self.dim_values = dim_values self.dimmerInterval = dimmerInterval self.weatherInterval = weatherInterval if self.weatherInterval > 0: self.weather = WeatherLight(weatherLocation) def update_dimmer(self): h = self.time.now().hour m = self.time.now().minute # interpolate final value based on minute a = self.dim_values[h] b = self.dim_values[(h+1) % 24] c = (b - a) * m/60 self.dimmer.set(self.bias * (a+c)) def update_dimmer_loop(self): self.update_dimmer() print '\t%s waiting %2d minutes to update dimmer\n' % (self.time.now(), self.dimmerInterval) Timer(self.dimmerInterval * 60, self.update_dimmer_loop).start() def update_weather(self): if self.weatherInterval > 0: self.bias = self.weather.getLightLevel() print 'weather bias set to %f' % self.bias def update_weather_loop(self): if self.weatherInterval > 0: self.update_weather() print '\t%s waiting %2d minutes to update weather\n' % (self.time.now(), self.weatherInterval) Timer(self.weatherInterval * 60, self.update_weather_loop).start() def update(self): self.update_weather() self.update_dimmer() def update_loop(self): if self.weatherInterval > 0: self.update_weather_loop() self.update_dimmer_loop() if __name__ == '__main__': if len(sys.argv) > 1: f = float(sys.argv[1]) if f < 0: DimmerScheduler(update_interval, dim_values, update_interval_weather, weather_location).update() else: Dimmer().set(f) else: DimmerScheduler(update_interval, dim_values, update_interval_weather, weather_location).update_loop()