Battery Notifications with udev

Sunday, Jul 31, 2022
#unix #tips

My laptop’s charging cable is 7 meters long. When I first left the US I needed to buy a new AC cable so I could plug my laptop in without an adapter. I dropped by the old electronics store and picked up a simple 5ft power cable. Turns out it was 5 meters and I’m an idiot. As a result I tend to always have my laptop plugged in while at home since I never struggle to find a close enough port.

I don’t have any sort of task bar on my computers. I stopped using one years ago for the extra screen real estate and to minimize distractions. Instead I press super+escape to open a notification that displays various important vitals.

laptop vitals notification

I also have notifications popup in certain circumstances, changing the brightness or volume, connecting or disconnecting from the internet, and one that shows me the time every half hour so I can look away from the computer screen and also generally be aware of how much time I’m spending on my computer.

Yet, over the last few years I never setup the most useful one of all: showing my battery percent as it drops below 20% and 5%. I knew I could do it quite easily by “polling” the battery percentage every few minutes and seeing if it ever dropped below certain percentages, but the idea of wasting battery … to check my battery constantly seemed incredibly silly to me. I also knew that (most) batteries do in fact report their percentage to the kernel as it drops below certain percentages so that you could do cpu throttling or simply hibernate the computer at say 5%. Finding out “the correct” way to listen to these events proved quite challenging. At first I thought maybe I needed to setup file watches on stuff like /sys/class/power_supply/BAT0/energy_now but I think that file “changes” (or rather polls the battery) every time you read it.

Eventually, I remembered about udev, which I’ve actually used before for automounting drives, but for whatever reason didn’t realize how many different kinds of “events” it’s able to watch (basically everything). These days udev is part of systemd, so on void I’m actually using a fork called eudev maintained by the folks at gentoo and alpine. You configure it by creating .rules files in /etc/udev/rules.d/ which allow you to run a command (or a few other predefined actions) whenever a specified event happens.

There’s an extremely useful monitoring tool: udevadm monitor -p which lets you see each event that gets triggered live on your system. You can test it out by plugging in/out your charger and seeing the events that get triggered. After waiting around my laptop eventually had a battery discharge notification like this:

UDEV  [132776.072723] change   /devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:08/PNP0C09:00/PNP0C0A:00/power_supply/BAT0 (power_supply)
ACTION=change
DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:08/PNP0C09:00/PNP0C0A:00/power_supply/BAT0
DISPLAY=:0
POWER_SUPPLY_CAPACITY=80
POWER_SUPPLY_CAPACITY_LEVEL=Normal
POWER_SUPPLY_CYCLE_COUNT=0
POWER_SUPPLY_ENERGY_FULL=52290000
POWER_SUPPLY_ENERGY_FULL_DESIGN=50800000
POWER_SUPPLY_ENERGY_NOW=42080000
POWER_SUPPLY_MANUFACTURER=SMPWJ
POWER_SUPPLY_MODEL_NAME=00HW003
POWER_SUPPLY_NAME=BAT0
POWER_SUPPLY_POWER_NOW=20530000
POWER_SUPPLY_PRESENT=1
POWER_SUPPLY_SERIAL_NUMBER=  245
POWER_SUPPLY_STATUS=Discharging
POWER_SUPPLY_TECHNOLOGY=Li-ion
POWER_SUPPLY_TYPE=Battery
POWER_SUPPLY_VOLTAGE_MIN_DESIGN=15200000
POWER_SUPPLY_VOLTAGE_NOW=15253000
SEQNUM=8087
SUBSYSTEM=power_supply
USEC_INITIALIZED=981355595
XAUTHORITY=/home/kota/.Xauthority

I started by creating /etc/udev/rules.d/60-battery.rules and writing a basic rule, which matches events on the power_supply subsystem, with the POWER_SUPPLY_TYPE variable set to “Battery”:

SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_TYPE}=="Battery", RUN+="/usr/local/bin/battery.sh"

The RUN+= command allows you to run a program after the event is triggered. It doesn’t execute them under the context of a shell and doesn’t pass your normal environment variables. So it’s always safest to start your scripts with the correct shebang #!/bin/sh and to specify the full path to your program.

The first version of my battery script was very simple just enough that I could see that it was being called by the correct events. I’m using a program called battery which I wrote a while ago and already use in my manually triggered notifications. It prints the current battery percentage by dividing /sys/class/power_supply/BAT0/energy_now with /sys/class/power_supply/BAT0/energy_full and rounding to the nearest percent. Most batteries on recent kernels will do this automatically for you in /sys/class/power_supply/BAT0/capacity so you can probably just read that file instead.

#!/bin/sh

b=$(battery)
echo "$b" >> /tmp/battery-events.txt

This had a few immediate issues. First, it was seemingly getting triggered at very random percentages. Some batteries will report every single percentage drop while others (such as mine) will only report a few key ones, but it doesn’t really make sense for it to report at like 84, 59, 44, and other random values in an unrepeatable manner. After a tiny bit of investigation I realized my wireless mouse was sending out discharge events, which is super cool, but confusing. The other problem was that it would trigger at 20% when my laptop was charging not just discharging.

All I needed to do was make the udev rule slightly stricter by matching ENV{POWER_SUPPLY_NAME}=="BAT0" and ENV{POWER_SUPPLY_STATUS}=="Discharging" which both come from the udevadm monitor output.

SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_TYPE}=="Battery", ENV{POWER_SUPPLY_NAME}=="BAT0", ENV{POWER_SUPPLY_STATUS}=="Discharging", RUN+="/usr/local/bin/battery.sh"

It was all working perfectly, so I modified my script to use notify-send rather than just writing the percent to a file:

#!/bin/sh

b=$(battery)
if [ "$b" -lt 30 ]
then
	notify-send -u critical "b = $b-"
else
	notify-send "b = $b-"
fi

…aaaand it didn’t work. According to the arch wiki you need to set a few variables in the udev rule which notify-send relies on to know which display session to use for the notification. I guess this is in case you have a multihead setup?

# Rules for draining battery notices
SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_TYPE}=="Battery", ENV{POWER_SUPPLY_NAME}=="BAT0", ENV{POWER_SUPPLY_STATUS}=="Discharging", ENV{DISPLAY}=":0", ENV{XAUTHORITY}="/home/kota/.Xauthority" RUN+="/usr/bin/su kota -c /usr/local/bin/battery.sh"

With these final changes everything works great! I even went ahead and setup a rule to match when I plug in/out the charger and display a notification for that too. The only interesting difference is that my script reads one of the variables from the udev match which is pretty convenient:

/etc/udev/rules.d/60-powersupply.rules

# Rules for switching to battery or to power supply
SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_NAME}=="AC", ENV{POWER_SUPPLY_ONLINE}=="0", ENV{DISPLAY}=":0", ENV{XAUTHORITY}="/home/kota/.Xauthority" RUN+="/usr/bin/su kota -c /usr/local/bin/charger.sh"
SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_NAME}=="AC", ENV{POWER_SUPPLY_ONLINE}=="1", ENV{DISPLAY}=":0", ENV{XAUTHORITY}="/home/kota/.Xauthority" RUN+="/usr/bin/su kota -c /usr/local/bin/charger.sh"

/usr/local/bin/charger.sh

#!/bin/sh

b=$(battery)
if [ "$POWER_SUPPLY_ONLINE" -eq 1 ]
then
	notify-send "b = $b+"
else
	notify-send "b = $b-"
fi