Connecting a UPS to the Rapberry Pi & Custom Shutdown with Wake On LAN


Raspberry Pi Logo A UPS (uninterruptible power supply) is an electrical device that provides emergency power in the event of a power failure. Most modern UPSes can connect to a computer via USB and made to execute a self shutdown in the event that power has been lost.

In this blog post I’ll show you how to do a little more with your UPS by customizing APCUPSD – the program needed to communicate with your UPS. Read on!



Motivation

I currently have a APC ES550G and have it plugged into a Raspberry Pi which I bought here. This particular UPS provides backup power for the three devices that follow. Only one is physically connected to the UPS, the other two are remote machines.

Please take note of the user/ip/mac for your remote machines, you’ll need this info a bit later. Of course this will be different in your case. Also, you’ll need Python installed which shouldn’t be a problem if you’re using a Raspberry Pi.

These are the aforementioned devices:

  1. Raspberry Pi – Running Raspbian (Connected to the UPS)
  2. A second Raspberry Pi – Running OpenELEC
    • user: root
    • ip: 192.168.1.11
    • mac address: 90:9D:78:66:A6:4C
  3. A ZBox (Intel Atom box) – Running Kodibuntu
    • user: kodi
    • ip: 192.168.1.12
    • mac address: 1F:4E:59:E6:A9:75

When a power failure event is triggered, I’d like for the first Raspberry Pi to shut off the other two devices (consequently itself as well). Shutting off the Raspberry Pi that’s connected to the UPS is quite trivial, APCUPSD will do that for us. The two other devices – which are separate machines connected to the same switch – present a real problem. Moreover, the ZBox (Kodi) is actually set to sleep after 5 minutes and it’ll need waking up via WOL (Wake-On-LAN). For this to work, we’ll need to create a script that will be able to execute remote commands from the Raspberry Pi.

On to it!

Step 1 – Install APCUPSD

Ensure that your UPS is connected to your Raspberry Pi via USB prior to this step.

Let’s install the software.

sude apt-get update
sudo apt-get install apcupsd

Next we’ll need to change the config file. Make the appropriate changes by editing the apcupsd.conf file.

sudo nano /etc/apcupsd/apcupsd.conf
#
#UPSCABLE smart
UPSCABLE usb
#
#UPSTYPE apcsmart
#DEVICE /dev/ttyS0
UPSTYPE usb
DEVICE

Restart apcupd.

sudo service apcupsd start

And ensure that all is well by issuing the apcaccess command. You should see real-time data for your UPS.

apcaccess

Step 2 – Install SSH Keys

As I mentioned earlier we need to setup SSH keys in order to be able to execute remote commands without a password prompt. These are the steps you follow.

cd ~/
mkdir .ssh
chmod go-rwx .ssh
cd .ssh
ssh-keygen -b 1024 -t rsa -f id_rsa -P ""
cd .ssh ; ls -la

You should now see yours SSH keys, the private key is id_rsa and the public key is id_rsa.pub. Important thing to note is that the private key will need to stay where it is.

 -rw-------   1 bobby  staff   887B Mar  1 11:35 id_rsa 
 -rw-r--r--   1 bobby  staff   239B Mar  1 11:35 id_rsa.pub

Firstly, copy the contents of id_rsa.pub file to a temporary location. This is a sample.

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC5IBdipImrYb0jm4i9BlSiim7SnkvW/6K6eSHP+OH1o2Aesg82EaLwE8mTayn7ftrAdEi4zc+UMK3APex0STfxZ8VPABvSj0+7H0LX1hCeyGIS+ing8CEKMS70j8ItJV2EUSlDs91K17+/6VI91e5pjVI3CFkI228bb8cmNhuE6w== bobby@desktop.localdomain

With that done, will we need to boot into each remote machine and copy this public key into a file called authorized_keys in the user’s ~/.ssh directory.

cd ~/.ssh
touch authorized_keys
nano authorized_keys
(Copy contents of id_rsa.pub)
cd ~
chmod 700 .ssh
chmod 600 .ssh/authorized_keys

Log off. Now, let’s verify if you can simply run the hostname command from the computer connected to the UPS.

ssh pi@192.168.1.12 "hostname"

If you get back the hostname, all is well!

Step 3 – Modify APCUPSD doshutdown

APCUPSD uses various events when handling the UPS. Namely, the file /etc/apcupsd/apccontrol lists those events. If you’re curious, inspect it for yourself.

nano /etc/apcupsd/apccontrol

We’ll concern ourselves with three particular events: onbattery, offbattery, and doshutdown.

    onbattery)
        echo "Power failure on UPS ${2}. Running on batteries." | ${WALL}
    ;;
    offbattery)
        echo "Power has returned on UPS ${2}..." | ${WALL}
    ;;
    ...
    doshutdown)
        echo "UPS ${2} initiated Shutdown Sequence" | ${WALL}
        ${SHUTDOWN} -h now "apcupsd UPS ${2} initiated shutdown"
    ;;
    ...

One thing of particular interest that I’d like to point out, if APCUPSD finds a corresponding file (named appropriately to match the event) in the configuration directory – usually in /etc/apcupsd/ – it’ll execute that script prior to moving on with the commands within the event. That is to say, if I create a script with filename /etc/apcupsd/doshutdown, then the commands within that file will be executed first prior to the following commands (provided that my script end with a exit 0).

echo "UPS ${2} initiated Shutdown Sequence" | ${WALL}
${SHUTDOWN} -h now "apcupsd UPS ${2} initiated shutdown"

In fact, we’ll exploit this functionality now.

Create the file /etc/apcupsd/doshutdown with the following contents. Pay special attention to the highlighted lines which are for my specific setup. Lines 13, 14 need to be edited to match your IP/MAC configuration. More so, lines 44, 46 need to be edited with the appropriate user as well (/usr/bin/ssh USER@IP). Of course, edit the file to suit your case as needed.

!/bin/bash
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#

declare -A pc_array
dir=/etc/apcupsd
pc_array["192.168.1.11"]="90:9D:78:66:A6:4C"
pc_array["192.168.1.12"]="1F:4E:59:E6:A9:75"

log="$dir/doshutdown.log"
myprint(){
   echo $(date +'%F %T'): $@ | tee -a $log;
}
myprint "Starting script wol";
for i in "${!pc_array[@]}"
do
   echo "Waking: $i with MAC: ${pc_array[$i]}"
   /usr/bin/python ${dir}/WakeOnLan.py "${pc_array[$i]}"
   count=10
   while [[ $count -ne 0 ]] ; do
      /bin/ping -c 1 ${i} > /dev/null
      rc=$?
      if [[ $rc -eq 0 ]] ; then
         count=1
      else
         myprint "sending packet";
         /usr/bin/python ${dir}/WakeOnLan.py "${pc_array[$i]}"
      fi
      ((count = count - 1))
   done

   if [[ $rc -eq 0 ]] ; then
      myprint "Success, $i awake";
      myprint "Sleeping..."
      sleep 5
      myprint "Sending command to poweroff to $i"
      if [[ $i == "192.168.1.11" ]]; then
         /usr/bin/ssh root@${i} "shutdown -h now" > /dev/null
      else
         /usr/bin/ssh kodi@${i} "sudo shutdown -h now" > /dev/null
      fi
      rc=$?
      if [[ rc -eq 0 ]] ; then
         myprint "Command ran successfully: $rc"
      else
         myprint "Command failed: $rc"
      fi
   else
      myprint "Failure, timed out."
   fi
done
exit 0

Finally make sure the script is execuatable.

sudo chmod +x /etc/apcupsd/doshutdown

One more thing, the above script in fact invokes another script (this time a Python script) to issue a WOL (Wake-On-LAN) packet. That’ll need to be created as well. I found this script a while back but it very much still works.

sudo nano /etc/apcupsd/WakeOnLan.py
#!/usr/bin/env python

# Wake-On-LAN
#
# Copyright (C) 2002 by Micro Systems Marc Balmer
# Written by Marc Balmer, marc@msys.ch, www.msys.ch/
# This code is free software under the GPL

import struct, socket, time, sys

ethernet_address=sys.argv[1]

# Construct a six-byte hardware address

addr_byte = ethernet_address.split(':')
hw_addr = struct.pack('BBBBBB', int(addr_byte[0], 16),
int(addr_byte[1], 16),
int(addr_byte[2], 16),
int(addr_byte[3], 16),
int(addr_byte[4], 16),
int(addr_byte[5], 16))

# Build the Wake-On-LAN "Magic Packet"...

msg = '\xff' * 6 + hw_addr * 16

# ...and send it to the broadcast address using UDP

#time.sleep(2)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.sendto(msg, ('<broadcast>', 9))
s.close()

Again, make sure the script is execuatable.

sudo chmod +x /etc/apcupsd/WakeOnLan.py

Step 4 – Going a litte further!

While we’re here, we’ll create the following two Python scripts such that we can be alerted via email upon power failure and return of power.

Backup the original files.

sudo cp /etc/apcupsd/onbattery /etc/apcupsd/onbattery.original
sudo cp /etc/apcupsd/offbattery /etc/apcupsd/offbattery.original

Let’s start with the onbattery event (when power has been lost).

sudo nano /etc/apcupsd/onbattery

Here’s the file. Please Alter GMAIL_ADDRESS, GMAIL_PASSWORD, TO_EMAIL@WHATEVER.COM (lines 11,12,15) to suit your needs.

#!/usr/bin/env python

import smtplib
import syslog
from email.mime.text import MIMEText

syslog.openlog('[UPS]')
def log(msg):
    syslog.syslog(str(msg))

GMAIL_ADDRESS = 'GMAIL_ADDRESS@gmail.com'
GMAIL_PASSWORD = 'GMAIL_PASSWORD'

from_email = GMAIL_ADDRESS
to_emails = ["TO_EMAIL@WHATEVER.COM"]  # cell phone address

msg_subject = "ALERT: UPS Power Failure"
msg_text = "Auto Notification from UPS ES 550G "

log(msg_subject)

msg = MIMEText(msg_text)
msg['Subject'] = msg_subject
msg['From'] = from_email
msg['To'] = ", ".join(to_emails)
s = smtplib.SMTP_SSL('smtp.gmail.com', '465')
s.login(GMAIL_ADDRESS, GMAIL_PASSWORD)
s.sendmail(from_email, to_emails, msg.as_string())
s.quit()
sudo chmod +x /etc/apcupsd/onbattery

Do the same for the offbattery event (when power has been restored).

sudo nano /etc/apcupsd/offbattery

Now create the file.

#!/usr/bin/env python

import smtplib
import syslog
from email.mime.text import MIMEText

syslog.openlog('[UPS]')
def log(msg):
    syslog.syslog(str(msg))

GMAIL_ADDRESS = 'GMAIL_ADDRESS@gmail.com'
GMAIL_PASSWORD = 'GMAIL_PASSWORD'

from_email = GMAIL_ADDRESS
to_emails = ["TO_EMAIL@WHATEVER.COM"]  # cell phone address

msg_subject = "OK: UPS Power Recovered"
msg_text = "Auto Notification from UPS ES 550G "

log(msg_subject)

msg = MIMEText(msg_text)
msg['Subject'] = msg_subject
msg['From'] = from_email
msg['To'] = ", ".join(to_emails)
s = smtplib.SMTP_SSL('smtp.gmail.com', '465')
s.login(GMAIL_ADDRESS, GMAIL_PASSWORD)
s.sendmail(from_email, to_emails, msg.as_string())
s.quit()

Step 5 – Let’s test it!

To quickly test this, one needs only to modify the TIMEOUT (seconds) variable in in the config file. I set it to 10 seconds.

sudo nano /etc/apcupsd/apcupsd.con
# If during a power failure, the UPS has run on batteries for TIMEOUT
# many seconds or longer, apcupsd will initiate a system shutdown.
# A value of 0 disables this timer.
#
#  Note, if you have a Smart UPS, you will most likely want to disable
#    this timer by setting it to zero. That way, you UPS will continue
#    on batteries until either the % charge remaing drops to or below BATTERYLEVEL,
#    or the remaining battery runtime drops to or below MINUTES.  Of course,
#    if you are testing, setting this to 60 causes a quick system shutdown
#    if you pull the power plug.
#  If you have an older dumb UPS, you will want to set this to less than
#    the time you know you can run on batteries.
TIMEOUT 10

Restart apcupd.

sudo service apcupsd start

Now force a power failure by pulling out the UPS power cable from the outlet. After 10 seconds the doshutdown event should run. Concurrently, you can watch the syslog and ensure you see the appropriate events pass by.

tail -f /var/log/syslog

Well, that covers it. Hope you enjoyed this particular post. As always, comments are welcome. Enjoy and hack on!

Leave a comment

Your email address will not be published. Required fields are marked *