#!/usr/bin/python # -*- coding: utf-8 -*- """ infotext.py v0.2 Simply prints data and stats from various sources. Most useful as input program to xscreensavers that simply takes some text and displays it. Currently implemented: * MPD playing status * OpenWeatherMap weather data and forecast * System stats: Local time, date, cpuload, load average, memory info Written specifically for RaspberryPi, but useable on other systems too. Usage: Edit the configuration variables below to suit your needs. You may want to apply for an app key (APPID) at http://openweathermap.org. Then configure xscreensaver to use any text-displaying screensaver (e.g. Phosphor, Apple2, StarWars, ...). Then, in the xscreensavers configuration dialog's advanced tab, configure this program as text source. Raspbian package dependencies: python python-mpd xscreensaver xscreensaver-data xscreensaver-data-extra Optional GL screensavers (slow on a raspi!): xscreensaver-gl xscreensaver-gl-extra libgl1-mesa-swx11 Published under the terms of the MIT License: Copyright (c) 2013 Dave Kliczbor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ ### configuration variables MAXLINES = 17 MAXLEN = 51 SHOWMPD = True SHOWWEATHER = True SHOWCPUTIME = True SHOWTIME = False SHOWMEM = False SHOWFORTUNE = True WEATHERCITY = 'dortmund' WEATHERAPPID = '' MPDHOST = 'localhost' MPDPORT = 6600 FORTUNEOPTS = [ '-s', '-o', '-n', '%d' % (MAXLEN*5,) ] # the following config variables probably do not need to be changed WEATHERDATAURL = 'http://api.openweathermap.org/data/2.5/weather?q=%s&units=metric&APPID=%s' WEATHERFORECASTURL = 'http://api.openweathermap.org/data/2.5/forecast/daily?q=%s&units=metric&cnt=3&APPID=%s' SAVEDIR = '/run/shm' ### module imports import mpd import pickle import time import os import textwrap from json import load as jsonload from urllib2 import urlopen from subprocess import check_output from pprint import pprint ### some functions def timelist(): """Reads cpu/process stats from /proc/stat and returns them """ statfile = file("/proc/stat", "r") t = statfile.readline().split(" ")[2:6] statfile.close() for i in range(len(t)) : t[i] = int(t[i]) return t def deltatime(): """Reads cpu/process stats from last run, shifts them, adds current stats from timelist(). Returns currents and last stats """ x = [0,0,0,0] try: f = open(SAVEDIR+'/cpuload', 'rd') x = pickle.load(f) f.close() except: pass y = timelist() try: f = open(SAVEDIR+'/cpuload', 'wb') pickle.dump(y, f) f.close() except: print("ERR: cannot save cpuload data in %s/cpuload" % SAVEDIR) for i in range(len(x)) : y[i] -= x[i] return y def cpuload(): """Calculates average cpu load since last run and returns it as percentage """ dt = deltatime() return 100 - (dt[len(dt) - 1] * 100.00 / sum(dt)) def load(): """Reads load average of last minute from /proc/loadavg and returns it as string """ r = "" try: r = " ".join(file('/proc/loadavg').readline().split(" ")[0:1]) except: pass return r def meminfo(): """Returns /proc/meminfo as dict """ return dict([ [ b.strip(":") for b in a if len(b) > 0 and b != 'kB' ] for a in [ l.strip().split(" ") for l in file("/proc/meminfo").readlines() ] ]) def u(s): """Makes unicode from ascii string without conking out on error """ return unicode(s, 'utf-8', errors='ignore') def wday(offset_from_today=0): """Returns day of week as string, either the current day or with an offset (days) into the future Ex.: Return day of week of tomorrow: wday(1) """ return ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'][(time.localtime().tm_wday + offset_from_today) % 7] def numf(n): """Formats a number between -999 and 9999 to 2-4 characters. Numbers < 10.0 are returned with one decimal after the point, other numbers as integers. Ex.: 0.2341 -> '0.2', 9.0223 -> '9.2', 11.234 -> '11', -5.23 -> '-5.2' """ if abs(n) < 10.0: return "%.1f" % n else: return "%.0f" % n def carddir(deg): """Returns the cardinal direction string from degrees (0° = N, 90° = E, ...) """ return ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'][int(((deg+11.25)%360)/22.5)] def fetch_weather_data(): """Fetches weather data from openweathermap.org. Data is cached in /run/shm, updates after 1000 seconds. Returns a dict/list data structure derived from the JSON data. """ c = None try: f = open(SAVEDIR+'/weather', 'rd') c = pickle.load(f) f.close() except: pass if c is None or c['checktime'] < time.time() - 1000.0: try: weatherdata = urlopen(WEATHERDATAURL % (WEATHERCITY, WEATHERAPPID)) forecastdata = urlopen(WEATHERFORECASTURL % (WEATHERCITY, WEATHERAPPID)) c = jsonload(weatherdata) c['forecast'] = jsonload(forecastdata) c['checktime'] = time.time() try: f = open(SAVEDIR+'/weather', 'wb') pickle.dump(c, f) f.close() except Exception as e: print('ERR: cannot save weather in %s/weather' % SAVEDIR) except Exception as e: print("ERR: cannot fetch weather: " + e.message) return c def fetch_mpd_data(): """Connects to an MPD instance and fetches the status and current song data. Cannot cope with authenticated connections (yet). """ mpc = mpd.MPDClient() d = None try: mpc.connect(MPDHOST, MPDPORT) d = dict(mpc.status().items() + mpc.currentsong().items()) mpc.disconnect() except: pass return d ### format weather data def interpret_rain(c): if 'rain' in c: for k,v in c['rain'].iteritems(): return "rain %.0fmm/%s" % (v,k) elif 'snow' in c: for k,v in c['snow'].iteritems(): return "snow %.0fmm/%s" % (v,k) else: return "no rain" def count_leading_whitespace(s): count = 0 for c in s: if c == " ": count += 1 elif c == "\t": count += 4 else: break return count def rewrap(p): #print repr(p) pars = p.splitlines() outpars = [] outtext = "" indent = -2 for par in pars: cur_indent = count_leading_whitespace(par) if ": " in par or ":\t" in par: indent = cur_indent outpars.append((indent,par.strip(),)) elif indent-1 <= cur_indent <= indent+1: outpars[-1] = (indent, (str(outpars[-1][1]) + (par.strip() if len(outpars[-1][1]) < 1 else " " + par.strip())), ) else: indent = cur_indent outpars.append((indent,par.strip(),)) #print repr(outpars) for parindent,par in outpars: #print repr(parindent) #print repr(par) outtext += textwrap.fill(par, width=MAXLEN, replace_whitespace=True, initial_indent=" "*parindent, subsequent_indent=" "*parindent).replace(" ", " ") + "\n" return outtext def fortune(): text = check_output(['fortune'] + FORTUNEOPTS).rstrip() #print repr(text) paragraphs = text.split("\n\n") out = "\n".join([ p if all([len(l) < MAXLEN for l in p.splitlines()]) else rewrap(p) for p in paragraphs ]) #print repr(out) return out.replace("\t", " ").splitlines() ### main program if __name__ == '__main__': # the output string list # this list actually will contain tuples: (separation priority, output string). # separation priority is used in the output postprocessing below to insert optional empty lines # to fill the screen as near as possible to MAXLINES. In output postprocessing, first the lines # with priority 1 will receive an empty line before and maybe after themselves, then the lines # with priority 2, and so on. Priority 0 will never be expanded with empty lines. out = [] ### format MPD data if SHOWMPD: d = fetch_mpd_data() if d is not None and "state" in d and d["state"] == 'play': # mpd is playing? full data output! try: # trying to interpret the mpd data ct = int(float(d["elapsed"])) t = 1 try: t = int(d["time"]) out.append((1,u"[mpd playing #%s/%s %02d:%02d/%02d:%02d %d%%]" % ( d["song"], d["playlistlength"], ct/60, ct - ((ct/60)*60), t/60, t - ((t/60)*60), int(ct*100/t) ))) except: # an exception from the try block above most likely means that d['time'] does not # contain a simple integer, which in turn most likely means that we are streaming out.append((1,u"[mpd playing #%s/%s streaming %02d:%02d]" % ( d["song"], d["playlistlength"], ct/60, ct - ((ct/60)*60), ))) t = '' if (not 'title' in d) and ('file' in d): t = os.path.splitext(os.path.basename(d['file']))[0] else: t = d['title'] if not 'artist' in d and ' - ' in t: # no artist, but a title -> streaming or bad tagging # anyway, if we have ' - ' in title, we repair that by splitting up the title string out += [ (2,u" %s" % u(s)) for s in reversed(t.split(' - ')) ] else: out.append((2,u" %s" % u(t))) if "artist" in d: out.append((2,u" %s" % u(d["artist"]))) if "album" in d: out.append((2,u" %s" % u(d["album"]))) except Exception as e: # something went very wrong in the program code above print("ERR b/c of mpd data: " + str(e)) elif d is not None and "state" in d: # mpd found, but not playing out.append((1,u"[mpd status: %s]" % d["state"])) else: # the mpd is a lie out.append((1,u"[the mpd at %s:%d is a lie]" % (MPDHOST, MPDPORT))) if SHOWWEATHER: try: c = fetch_weather_data() out.append((1,u"[openweather %d°C %s]" % (c['main']['temp'],c['weather'][0]['description']))) if 'main' in c: out.append((2,u" %d°C < T < %d°C %s hum %d%%" % (c['main']['temp_min'], c['main']['temp_max'], interpret_rain(c), c['main']['humidity']))) if 'deg' in c['wind']: out.append((2,u" wind %sm/s %s %d°" % (numf(c['wind']['speed']), carddir(c['wind']['deg']), c['wind']['deg']))) else: out.append((2,u" wind %sm/s" % numf(c['wind']['speed']))) if 'gust' in c['wind']: out[-1][1] += " gust %sm/s" % numf(c['wind']['gust']) if 'list' in c['forecast']: fore = u'' for dn,d in enumerate(c['forecast']['list'][1:]): fore += u' %s %.0f/%.0f°C %s' % (wday(dn+1), d['temp']['min'], d['temp']['max'], d['weather'][0]['main']) out.append((2,fore)) except: #pprint(c) pass emptyline = (0,u'') if SHOWFORTUNE: try: out.append(emptyline) for l in fortune(): out.append((0, l )) except Exception as e: out.append((1, u"Sorry, no fortune %s" % e.message)) ### output postprocessing and system info if len(out) < MAXLINES - int(SHOWCPUTIME) - int(SHOWTIME) - int(SHOWMEM): # if we don't fill the screen, we add another empty line in front of the system stats out.append(emptyline) # append time, date, cpu load, load average if SHOWCPUTIME: out.append((0,u"[%02d:%02d %02d.%02d.%02d cpu %.1f%% load %s]" % (time.localtime().tm_hour, time.localtime().tm_min, time.localtime().tm_mday, time.localtime().tm_mon, time.localtime().tm_year % 100, cpuload(), load()))) # append time, date if SHOWTIME: out.append((0,u"[%02d:%02d %02d.%02d.%04d]" % (time.localtime().tm_hour, time.localtime().tm_min, time.localtime().tm_mday, time.localtime().tm_mon, time.localtime().tm_year))) # append memory information if SHOWMEM: try: mi = meminfo() out.append((0,u"[mem %s free %s cache %s]" % (mi['MemTotal'], mi['MemFree'], mi['Cached']) )) except: pass # insert empty lines according to "separation priority" highestpriority = max([ p for p,s in out]) # we do a loop for each existing priority, once with insertafter disabled, once with insertafter enabled for prio,insertafter in [ (p,b) for p in xrange(1, highestpriority+1) for b in (False, True) ]: # we create a new list to take the elements ("new out") nout = [] for e in out: # in the following blocks, we always maintain the condition that no two empty lines may follow each other if e[0] == prio: # if the current list element has the separation priority we're currently checking, # we add a new empty line to the new list if len(nout) > 0 and nout[-1] is not emptyline: nout.append(emptyline) # then we add the element itself nout.append(e) # on second run with the same prio, insertafter is True, so we append an empty line afterwards if insertafter: nout.append(emptyline) else: # if the current element has not the priority we're checking, it's just added to the new list # after checking that it's not an empty line following another empty line if not ( e is emptyline and len(nout) > 0 and nout[-1] is emptyline ): nout.append(e) if len(nout) < MAXLINES: # okay, did we exceed the limit? if not, we set 'out' to our new list and let the process repeat itself out = nout else: # else we break out of the for loop and let the garbage collector pick up the new list that got too long break ### output printing # print all output lines in latin-1 encoding, because xscreensaver is not UTF8-aware m( for p,l in out: print( l[0:MAXLEN].encode("latin-1", 'ignore') ) # print separator line print(u"_" * MAXLEN)