Suspend on lid closed + battery

If you use FreeBSD 11 on the good ol’ ThinkPad X201, you’ve probably noticed now that suspend and resume work perfectly flawlessly with the latest FreeBSD release.

You probably wish to take advantage of this and suspend your laptop automatically when the lid is closed. Nothing could be easier:

sysctl hw.acpi.lid_switch_state=S3

Save this option /etc/sysctl.conf for it to be permanent.
Close the lid, the laptop goes to sleep, you’re done!
It’s that simple.

But, with this method it will go to sleep each time the lid is closed, completely ignoring whether the laptop is on battery or not. And if you’re like me you probably don’t want this. Instead you want it to suspend itself when the lid is closed and the laptop is on battery. Easy enough! We just have to invoke the mighty power of devd along with a very little shell script.

First we need to tell devd how to react when the lid is closed. Put this in /etc/devd/lid.conf, then restart (service devd restart):

# Notify lid close/open events.
notify 10 {
  match  "system"    "ACPI";
  match  "subsystem" "Lid";
  action "/etc/acpi/lid.sh $notify";
};

Now we will make a script that checks the lid and AC line states and chooses to suspend the laptop when both the lid is closed and AC line is disconnected. This goes in /etc/acpi/lid.sh:

#!/bin/sh

lid="$1" # 0x00 is closed, 0x01 is open (see devd.conf)
acline=$(sysctl -n hw.acpi.acline) # 0 is battery, 1 is online (man acpi)

if [ \( "$lid" = "0x00" \) -a \( "$acline" -eq 0 \) ]
then
  logger "Lid closed on battery. Going to sleep."
  acpiconf -s3
fi

Try it out! Disconnect the AC line, close the lid, it should go to sleep within seconds. However if it doesn’t work you might want to modify the script above a little to check whether lid events are received correctly or not.

Now you may use a modified version of this script to lock xscreensaver when the lid is closed and before the actual suspend. Well OK, I’ve done that for you:

#!/bin/sh
# XScreensaver should be called BEFORE going to sleep to avoid the desktop to be
# shown for a few seconds when the system resumes from sleep.
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

lid="$1" # 0x00 is closed, 0x01 is open (see devd.conf)
acline=$(sysctl -n hw.acpi.acline) # 0 is battery, 1 is online (man acpi)

lock_display() (
  socket="$1"
  display=$(echo "$socket" | tr "X" ":")

  # Temporary pid file for the watching command
  tpid=$(mktemp)

  # Wait until the display is actually locked.
  (timeout 2s xscreensaver-command -display "$display" -watch & echo $! > $tpid) | (
    # Issue the lock command only when we know that
    # the watching pipe is ready.
    xscreensaver-command -display "$display" -lock

    while read line
    do
      line=$(echo $line | cut -d' ' -f 1)

      if [ "$line" = LOCK ]
      then
        # We have to kill the watching command manually before breaking.
        kill -TERM $(cat $tpid)
        break
      fi
    done
  )

  rm $tpid
)

# The X server may not be running
if [ ! -d /tmp/.X11-unix ]
then
  exit 0
fi

# Lock each available display
for socket in $(ls /tmp/.X11-unix)
do
  # Lock the display
  logger "Locking xscreensaver on $socket"
  lock_display $socket &
done

# Wait until every displays are locked
wait

# Now we can suspend if needed
if [ \( "$lid" = "0x00" \) -a \( "$acline" -eq 0 \) ]
then
  logger "Lid closed on battery. Going to sleep."
  acpiconf -s3
fi