Creating a Battery Widget with Automatic ACPI Update for Awesome WM

Tue 21 May 2013 by Javex

Introduction

I recently switched to awesome which will have its own article as the window manager is just awesome. However, I am not done with its configuration and so there is no point in writing an article about it yet.

Today I will just go into detail on a little widget I created for my laptop to display my battery status. Like the rest of my configuration it is largely based on the awesome configuration of mtrokic which provides a great base to start on and hints some nice things possible in the configuration.

However, it is created for awesome 3.4 and since I have 3.5 a lot of updates have to be made. I will explain some in another post but most are just plain adaption of changes between versions.

One particular widget that I liked very much is his battery widget: It displays a neat little icon and the percentage of how much is left on your laptops battery. It has one particular drawback though: It is updated through a timeout while I'd like it to update automatically on plug/unplug (this is both much more elegant and it doesn't have the overhead of a lot of unnecessary polls or slow updates).

Note

If you want regular polls to update the actual battery level (which you will likely want) then you still have polls.

The Original Widget

The original widget is updated every 60 seconds and has the following relevant source code:

-- Battery
batterywidget = widget({ type = "textbox" })

function batteryInfo ()
        os.execute("$HOME/.config/awesome/scripts/battery")
        local iostate = io.open("/tmp/bat_state")
        local iolevel = io.open("/tmp/bat_level")
        local state = iostate:read()
        local level = iolevel:read()
        iostate:close()
        iolevel:close()
        if state == "Discharging" then
                batterywidget.text = "    "..level.." % "
                batterywidget.bg_image = image("/home/mtrokic/.config/awesome/icons/battery_discharge.png")
        elseif state == "Charging" then
                batterywidget.text = "    "..level.." % "
                batterywidget.bg_image = image("/home/mtrokic/.config/awesome/icons/battery_charge.png")
        else
                batterywidget.text = ""
                batterywidget.bg_image = image("/home/mtrokic/.config/awesome/icons/charged.png")
        end
end

batteryInfo()
batterytimer = timer({ timeout = 60 })
batterytimer:add_signal("timeout", function() batterywidget.bm_image = batteryInfo() end)
batterytimer:start()

batterywidget:add_signal("mouse::enter", function() bat = batnoti() end)

This is the original (so awesome 3.4) code for the battery widget which obviously is outdated. The most important part here is the batteryInfo function which updates the widget. You can see the three icons (included in the original configuration) and the value to display. I will now alter this code to make ACPI updates work and also update it to 3.5 while doing so.

Handling ACPI events

When the laptop power supply is connected or disconnected an ACPI event is triggered. Having acpid installed, these events are handled in a file called /etc/acpi/handler.sh which already has a sample included on ArchLinux.

As a result, wen I unplug the power supply, an event is triggered that calls the handler.sh script in which I can handle the ACPI event to call batteryInfo.

Testing this on the command line works with echo 'batteryInfo()' | awesome-client which we can see updates the icon. Of course, in a script executed by root we will use it like this: su javex -c "echo 'batteryInfo()' | awesome-client" which we can also test on the console and which also works perfectly, so we enter it into the script and execute the script itself as root with the correct parameters to test the functionality. Here is an excerpt from the final script:

ac_adapter)
    case "$2" in
        ACPI0003:00)
            case "$4" in
                00000000)
                    su javex -c "echo 'batteryInfo()' | awesome-client"
                ;;
                00000001)
                    su javex -c "echo 'batteryInfo()' | awesome-client"
                ;;
                *)
                    #logger "ACPI action undefined on 4: $4"
                ;;
            esac
            ;;
        *)
            #logger "ACPI action undefined: $2"
            ;;
    esac
    ;;

It is part of a large block of nested switch statements but the most important part here is how the events of connect and disconnect work. Here we call our function. Executing the script with matching values works perfectly. However, the actual event does not update the icon. After a while of searching and some help from the #awesome IRC channel, I found the solution: Because it has no screen/display when it is called, it cannot connect to DBus and thus not issue such a command.

Sending DBus Messages as root

I was presented with the problem that there is no display associated and thus no DBus environment (you can try it from a tty to see the problem):

> su javex -c "echo 'batteryInfo()' | awesome-client"
Failed to open connection to "session" message bus: Unable to autolaunch a dbus-daemon without a $DISPLAY for X11

A user on the #awesome IRC directed my to this unix.stackexchange question which basically tells me to initialize the environment for DBus before sending any messages.

I had to modify the solution a little bit because it seems like ArchLinux does not place the machine-id file in /var/lib/dbus/. After some searching and man page reading I found an even better way: dbus-uuidgen --get. It yields exactly what could be found in machine-id (which I found in /etc/ on ArchLinux by the way) but is even more dynamic.

In the end I wrote a small script to handle this for me so I don't clutter my handler.sh:

dbus_session_file=$HOME/.dbus/session-bus/$(dbus-uuidgen --get)-0
if [ -e "$dbus_session_file" ]; then
    sleep 1
    source "$dbus_session_file"
    export DBUS_SESSION_BUS_ADDRESS DBUS_SESSION_BUS_PID DBUS_SESSION_BUS_WINDOWID
    echo 'batteryInfo()' | awesome-client
fi

And that's it. Modify the handler.sh so it calls our script and we are done: su javex -c "/home/javex/bin/ac_update.sh". Now whenever the AC is plugged or unplugged it will immediately get updated.

Update 2014-01-14

I had a laptop crash and now I had to reinstall my ACPI handler. Unfortunately, I noticed that my above way for connecting to DBUS did not work anymore. As a result I had to find a new way:

awesome_pid=$(pidof awesome)
if [ -n "$awesome_pid" ]; then
    export  $(cat /proc/${awesome_pid}/environ | tr \\0 \\n | grep -E '^DBUS_SESSION_BUS_ADDRESS=')
    echo 'batteryInfo()' | awesome-client
fi

This new way will get the DBUS session used by awesome and then send the message there. While this is shorter and seems to be more stable as it will always watch the awesome DBUS session, I feel like this is a dirty workaround. But it does in fact work and so who cares ;-)


Comments