battery-sleep.sh : Auto-Suspend on Low Battery for FreeBSD

Host: thinkpadt480s | Date: 2026-05-15 | Status: Deployed and running

battery-sleep.sh is a small cron script that suspends FreeBSD to RAM when the battery drops to a threshold (default 20%). It is a companion to the sysutils/battery-shutdown port — that script shuts down at critical levels (~5 minutes remaining); this one sleeps earlier so the battery is not drained while the machine sits unattended.

Context

The sysutils/battery-shutdown FreeBSD port (J. Bacon, 2010) runs from /etc/cron.d/battery-shutdown every minute and handles warnings at 30, 15, and 5 minutes of remaining runtime, plus a hard shutdown at the 5-minute mark. It does not suspend. There is no equivalent port for sleep.

The gap: if the machine is left unplugged and unattended, it will run the battery flat to 0% and shut down hard rather than suspending at 20% and waking up where you left off. On a ThinkPad T480s that means a full discharge cycle every time you forget to plug in.

The fix is a second script that fires once when the battery crosses the sleep threshold, suspends to RAM via acpiconf -s 3, and resets its state when AC power is restored. battery-shutdown is left entirely alone — if the machine wakes from sleep with a critically low battery, cron resumes within a minute and battery-shutdown handles the shutdown.


How it works

The script reads two sysctl values:

hw.acpi.battery.life   # battery percent
hw.acpi.acline         # 1 = on AC, 0 = on battery

If on AC it clears the state file and exits. If on battery and at or below the threshold with no state file present, it touches the state file and runs acpiconf -s 3. The state file prevents re-firing every minute after the threshold is crossed. Clearing it on AC restore means the script will suspend again on the next discharge cycle.

S3 sleep on the T480s draws ~0.5W. If left sleeping for an extended period and the battery drains to critical, battery-shutdown catches it on the next wake: cron resumes, the minute timer fires, and the shutdown script sees critical runtime and halts cleanly.


Installation

1. Copy the script

doas cp battery-sleep.sh /usr/local/sbin/battery-sleep.sh
doas chmod 755 /usr/local/sbin/battery-sleep.sh

2. Add a cron.d entry

printf '* * * * *\troot\t/usr/local/sbin/battery-sleep.sh > /var/log/battery-sleep 2>&1\n' | doas tee /etc/cron.d/battery-sleep

The log at /var/log/battery-sleep shows the current percent and AC state on every run, and logs the suspend event when it fires.

3. Verify

Run the script manually on battery with the threshold temporarily raised to confirm it suspends:

doas rm -f /root/.battery_sleep_state
doas sh -c 'sed "s/sleep_percent=20/sleep_percent=100/" /usr/local/sbin/battery-sleep.sh | sh'

The sed one-liner substitutes the threshold inline without modifying the installed script. The machine should suspend immediately if unplugged.


Configuration

Edit the sleep_percent variable at the top of the script. The default is 20%, which leaves comfortable headroom above battery-shutdown’s critical threshold (~5 minutes remaining, typically 5–10%).

sleep_percent=20

The state file path is /root/.battery_sleep_state. It is created on suspend and removed when AC power is detected.


Interaction with battery-shutdown

Script Threshold Action
battery-sleep.sh ≤ 20% Suspend to RAM (acpiconf -s 3)
battery-shutdown.sh ≤ 5 min runtime Hard shutdown (shutdown -p now)

The two scripts are independent and do not coordinate. battery-shutdown also sends zenity desktop notifications at 30 and 15 minutes of remaining runtime, which will fire before battery-sleep triggers if the battery meter reports time remaining accurately. On some hardware hw.acpi.battery.time returns -1 (unknown); in that case only the percent threshold is meaningful.


battery-sleep.sh

#!/bin/sh

##########################################################################
#   Description
#       Auto-suspend on low battery.
#       Complements battery-shutdown.sh: this script suspends to RAM
#       at a higher threshold so the machine sleeps before the shutdown
#       script's critical level is reached.
#
#       On AC restore the state file is cleared so the script will
#       suspend again on the next discharge cycle.
#
#   History
#   Date        Who         Change
#   May 2026    Matuzalem   Initial version
##########################################################################

sleep_percent=20

state_file=/root/.battery_sleep_state

percent=$( /sbin/sysctl -n hw.acpi.battery.life )
ac_power=$( /sbin/sysctl -n hw.acpi.acline )

printf "Current percent  = $percent\n"
printf "ac_power         = $ac_power\n"

if [ "$ac_power" = "1" ]; then
    rm -f "$state_file"
    exit 0
fi

if [ "$percent" -le "$sleep_percent" ] && [ ! -f "$state_file" ]; then
    printf "Battery at ${percent}%%, suspending to RAM $(date)\n"
    touch "$state_file"
    /usr/sbin/acpiconf -s 3
fi