Wattometer: a simple Gnome applet (Part 2)

Summary

  • Creating a basic applet
  • Registering the applet by writing a server file
  • Testing the applet
  • The Meter class – making the code OO
  • Adding a timer
  • Adding the wattage
  • Files to download

Last time we looked at the basics of Python, and how Linux allows access to system information via file-like objects inside the /dev filesystem. We then looked at writing a Python function which got the battery information and calculated the power drain in watts.

Now, we’re going to actually build a panel applet to display this value at regular intervals, and write the special file Gnome needs to be able to run the applet and insert it in a panel.

The wattometer.py file

Let’s start our code off with a whole bunch of imports, for the various Python libraries we’ll need; and the get() method we wrote last time. We’ll put all this into a file called wattometer.py:

#!/usr/bin/env python

import gnomeapplet
import sys
import pygtk
pygtk.require('2.0')
import gtk
import gtk.glade
import gnome.ui
import os.path
import re
import gobject

def get():
    f = open('/proc/acpi/battery/BAT0/state')
    for x in f.readlines():
        if x.find('present rate')>=0:
            d,data=x.split(':')
            ma,d = data.strip().split()
            ma = float(ma)/1000.0
        if x.find('present voltage')>=0:
            d,data=x.split(':')
            mv,d = data.strip().split()
            mv = float(mv)/1000.0
    f.close()
    return ma*mv

A basic applet

Gnome uses the Bonobo object model to connect components together. This lets us have many different programs, displaying data in their own way inside components belonging to other programs. And this is how panel applets work: your applet is a Bonobo component, displaying its data as part of the Gnome Panel app.

To write a Bonobo applet, you need to provide a function which Bonobo will call. We’ll call this the “factory function.” It takes a Bonobo container widget and the activation IID (a unique string for every bonobo object type, we can ignore it here.) We pass this function to a special gnomeapplet method called bonobo_factory(). This will set up the container widget for us and then call our function.

Inside our factory function we should create our GTK widgets, and add them to the applet, and tell the applet (and its widgets) to show itself.

Our code will therefore go something like this for a very simple applet:

def factory(applet,iid):
    widget = gtk.Label("test!")
    applet.add(widget)
    applet.show_all()
    return gtk.TRUE # indicate success!
      
gnomeapplet.bonobo_factory("OAFIID:GNOME_Wattometer_Factory", 
    gnomeapplet.Applet.__gtype__, 
    "wattometer", "0", factory)

This is pretty much the simplest panel applet there is, displaying a single label with the word “test!” in it. Everything in here should be straightforward, apart from all those arguments to bonobo_factory(). These are:

  • The bonobo-activation iid of the factory (more on this later.)
  • The type of the object, which will be an applet – bonobo factories are actually used all over Gnome for all sorts of things, but here we want an applet. We get the type using Python’s reflection mechanism.
  • A description.
  • A version.
  • The factory function we’re providing.

Of course, if you run this, it won’t do anything – we haven’t told Gnome about it.

Registering the applet

We now need to tell Gnome about the applet. We do this by providing a server file and putting it into /usr/lib/bonobo/servers/, where it will be read by the bonobo activation server daemon. Here’s our file, which we should call GNOME_Wattometer.server:

<oaf_info>
 
<oaf_server iid="OAFIID:GNOME_Wattometer_Factory"
            type="exe"
            location="/home/white/misccode/wattometer/wattometer.py">
 
        <oaf_attribute name="repo_ids" type="stringv">
                <item value="IDL:Bonobo/GenericFactory:1.0"/>
                <item value="IDL:Bonobo/Unknown:1.0"/>
        </oaf_attribute>
        <oaf_attribute name="name" type="string" value="Wattometer"/>
        <oaf_attribute name="description" type="string" value="Wattage applet"/>
</oaf_server>
 
<oaf_server iid="OAFIID:GNOME_Wattometer"
            type="factory"
            location="OAFIID:GNOME_Wattometer_Factory">
 
        <oaf_attribute name="repo_ids" type="stringv">
                <item value="IDL:GNOME/Vertigo/PanelAppletShell:1.0"/>
                <item value="IDL:Bonobo/Control:1.0"/>
                <item value="IDL:Bonobo/Unknown:1.0"/>
        </oaf_attribute>
        <oaf_attribute name="name" type="string" value="Wattometer"/>
        <oaf_attribute name="description" type="string" value="Wattage applet"/>
        <oaf_attribute name="panel:category" type="string" value="Utility"/>
        <oaf_attribute name="panel:icon" type="string" value="iconimage.png"/>
</oaf_server>
</oaf_info>

Complicated, yes? However, it’s pretty much a standard format for applets. You can see it registers two servers: the first is the executable (type “exe”), which is our Python program. The second is the applet factory itself, which is told about the exe so it can use our program to fill itself with our widgets.

The only things you need to worry about are:

  • Making sure the IID’s are unique. The IID of the factory is the IID which we should use in our program’s call to bonobo_factory().
  • The name and description in the factory block
  • Making sure the factory’s location attribute points to the exe
  • Making sure the exe’s location attribute points to the location of the python program

You might also want to change category and image, too.

Once you’ve set this up correctly, and copied it into the servers directory, the Gnome Panel’s “Add To Panel…” dialog will contain the new applet. Naturally, you’ll have to be an administrator to be able to put the server file into its new home!

Testing the applet

If you just run this program from the command line it won’t do much: it’s not being called from inside the activation server, so there’s no applet framework for it to plug its widgets into. We’ll modify our code so that we can run it:

def factory(applet,iid):
    widget = gtk.Label("test!")
    applet.add(widget)
    applet.show_all()
    return gtk.TRUE # indicate success!
      
if __name__ == '__main__':   # testing for execution

    if len(sys.argv) > 1 and sys.argv[1] == '-d': # debugging
        mainWindow = gtk.Window()
        mainWindow.set_title('Applet window')
        mainWindow.connect('destroy', gtk.main_quit)
        applet = gnomeapplet.Applet()
        factory(applet, None)
        applet.reparent(mainWindow)
        mainWindow.show_all()
        gtk.main()
        sys.exit()
    else:
        gnomeapplet.bonobo_factory("OAFIID:GNOME_Wattometer_Factory", 
                             gnomeapplet.Applet.__gtype__, 
                             "wattometer", "0", factory)

Let’s look at those changes.
First, we use the “if name=main” trick to make sure we’re actually being run from the command line. If the code is just being included in another program, we won’t do anything. It’s not entirely necessary here, but it’s good practice. Then, we look to see if we’ve been called with a “-d” argument. If so, instead of going the normal route of plugging into an existing widget (the applet,) we

  • create a window
  • create an applet widget
  • call our factory, creating our widgets inside the applet
  • make the applet part of the window
  • show everything
  • run the GTK main loop until we exit

So now, if we run the program with

we’ll see a tiny window pop up with our applet inside it, just like a normal application. There are no title bar buttons, so you’ll have to close it by killing the process. Ctrl-C from the command line should do it.

The Meter class

To keep everything tidy, we’re going to put all the user interface code into a class called Meter. All our factory function will do is create a new Meter object. Add the following code, and change the factory method as shown:

class Meter:
    # this is the constructor 
    def __init__(self,applet,iid):
        self.applet = applet
        self.ebox = gtk.EventBox()
        self.label = gtk.Label("test")
        self.ebox.add(self.label)
        applet.add(self.ebox)
        applet.connect("destroy",self.cleanup)
        applet.show_all()
    def cleanup(self,e):
        pass # pythonese for "do nothing"
      
def factory(applet, iid):
    Meter(applet,iid)
    return True

If you’re new to Python, note the following oddities of Python’s object syntax:

  • Methods definitions always have self as their first argument, even though this isn’t passed in in the normal way
  • Methods and instance variables must be preceded by self. when accessed from a method
  • The constructor is called __init__
  • Like all variables in Python, we don’t need to declare instance variables – just setting them to values inside the constructor will do that.

Two functional changes have also been made here: the label is created inside an EventBox, so we can catch clicks and the like; and the applet now calls a cleanup method when it’s closed – which does nothing, but it’s always a good idea to have it there in case you need to add something to it.

Everything’s now nicely contained, and we can start to build the Meter functionality.

Adding a timer

It’s not much use just displaying the wattage once; we need to show it every few seconds. To do this, we’ll use a timeout timer, which will call a given method at regular intervals.

We start a timer by adding a method called settimeout() to our class:

    def settimeout(self,delay):
        if self.source>=0:
            gobject.source_remove(self.source)
        self.source=gobject.timeout_add(delay*1000, # delay in milliseconds
                                        self.timeout_callback,
                                        self)

What this method does is destroy a timer if one already exists, and then create a new one. That way, we can call the method several times if we want to change the interval.

We’ll also need to add some code to our constructor to initialise the source value and start the timer. We’ll also initialise a counter for testing purposes:

    def __init__(self,applet,iid):
        ...
        self.counter = 0
        self.source  = -1
        self.settimeout(2) # 2 seconds

We’ll use the counter in our callback method:

  
    def timeout_callback(self,event):
        self.counter = self.counter+1
        self.label.set_label("count %d" % self.counter)
        return True

Note the rather excellent “%” operator or doing string formatting in Python! Here I’m using "count %d" % self.counter to turn the counter into a string like “count 1”.

If you run this you’ll find that the label says “test!” for the first five seconds. This is because the correct data isn’t put there until the timeout runs. This is no good for our purposes, so we’ll put the code to change the label into a separate method which we’ll run from both the constructor and the timer tick:

    def __init__(self,applet,iid):
        ...
        self.counter = 0
        self.source  = -1
        self.settimeout(2) # 2 seconds
        self.setlabel()
        
    def setlabel(self): 
        self.label.set_label("count %d" % self.counter)
        
    def timeout_callback(self,event):
        self.counter = self.counter+1
        self.label.set_label("count %d" % self.counter)
        return True

Run this with -d, or even as an applet if you’ve installed the server file, and you’ll see a slowly increasing count. Nearly there!

Adding the wattage

Finally, we’ll want to actually call our get() function to output the ever-changing wattage. Get rid of the counter, and replace the new setlabel() method. One little quirk – if the system reports we’re using a very small about of battery power it doesn’t mean that we’ve suddenly gone into Hyper-Efficient Battery Usage Mode, it means that we’re plugged into the mains. Although it could be argued that this is a Hyper-Efficient Battery Usage Mode… anyway, we’ll check for that case:

  
    def setlabel(self):
        v = get()
        if v<0.001:
            s = "(charging)"
        else:
            s = "%0.1fW" % v
        self.label.set_label(s)
        return True

Note how we’re formatting the wattage: the format string %0.1f means we should use one decimal place of precision.

And we’re done! You now have a working wattage applet!

Files

You can download the files developed in these articles as a zip file from here. They are:

  • wattometer.py
  • GNOME_Wattometer.server, which should be edited to change the location of wattometer.py and put into /usr/lib/bonobo/servers
  • wattometer2.py, an improved version which uses a drop-down menu to allow the user to change the sampling rate

Copyright © Found
Jim Finnis' personal blog

Built on Notes Blog Core
Powered by WordPress