Content-Type: text/plain

Recently-released PulseAudio 16 added support for Bluetooth battery level reporting. If you're now checking what year this post was published, I don't blame you. Still, better late than never. Using desktop Linux is like drip-feeding yourself lottery winnings: you don't want to peak too soon and get everything at once.

Until desktop environment indicators gain the ability to display the battery level, the only way to view it is via the command line. For example, with pactl.

$ pactl list cards | grep -F bluetooth.battery
bluetooth.battery = "80%"

Due to impatience and a general unwillingness to wait for slow-moving desktop environments to catch up, we're going to do it ourselves using a simple xfce4-genmon applet.

An xfce4-genmon applet works like so: on an interval run a binary and display its output (text) in the panel. The output format of the binary should conform to XML, and it can also contain tags for text that should appear on hover, or a command to be executed on click. The documentation is short and sweet.

When creating a panel indicator an important consideration is how often it should refresh. This depends on the kind of information shown, how stale it can become before it's no longer useful, and how expensive it is to run (and consequently, which language it's written in).

For this we'll be using one of the slowest languages available, Ruby, because it's convenient and performance is not the most important factor here. Startup time for a Ruby script is approaching sub 40ms and this only needs to run once every 10 seconds to be useful. Battery levels don't update that often, so an even longer interval may be acceptable too.

One of the other new features in PulseAudio 16.0 is -f json to tell pactl to dump JSON. Since we will need to parse its output the timing of this addition is much appreciated.

But which device do we pick? The lazy option is to choose the first Bluetooth device. It's not too common to have multiple Bluetooth audio devices connected at once, so this should suffice.

def get_level
    JSON.parse(`pactl -f json list cards`).map do |device|
        device['properties']['bluetooth.battery']
    end.compact.first
end

Any devices without a Bluetooth battery level will map to nil which is then removed by compact. The first of the battery levels is then returned.

To add a bit of colour, we can also define a function that picks an appropriate one based on how much battery is left.

def colour_of(level)
    if level < 20
        '#f25238'
    elsif level < 60
        '#cfad00'
    else
        '#66b602'
    end
end

That's reddish for <20%, orange for <60%, otherwise green.

We'll also need a function to render the battery level in the chosen colour to standard output as XML.

def render(level, colour)
    str = level

    if level
        str = '<span fgcolor="%s">%s</span>' % [colour, level]
    end

    '<tool></tool><txt>%s</txt>' % str
end

The empty <tool></tool> declaration is there to prevent a default (unnecessary) tooltip appearing on hover.

Finally, the lines of glue that hold it all together.

level = get_level
puts render(level, colour_of(level.to_i))

Combine all these parts and save it as an executable script with a name of your choice, then call it from an xfce4-genmon applet.

Hopefully PulseAudio-consuming software will soon catch up and show battery levels in a more integrated way, but with the slow development pace of xfce4 it's better not to count on it. Until then, this solution will do.

Update: Since switching to Pipewire the Bluetooth battery level is no longer available via pactl. Pipewire registers the Bluetooth device as a power source which means it can be interrogated using upower instead, and most desktop environments already have a power widget that enumerates power sources and shows battery levels. This means a panel indicator may not be necessary, depending on one's needs.

Using xfce4-power-manager the battery level is available but sits behind a mouse click which is not "at-a-glance" enough for my tastes, so I've kept the panel indicator but tweaked it to read the battery level via upower if available, otherwise falling back to pactl.

device = %r[/org/freedesktop/UPower/devices/headset_dev_.*].match(`upower -e`)
if device
    info = `upower -i #{Shellwords.escape(device)}`
    /percentage:\s*(\d+)%/.match(info)[1]
else
    # original code to read from pactl
end