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:
- Raspberry Pi – Running Raspbian (Connected to the UPS)
- A second Raspberry Pi – Running OpenELEC
- user: root
- ip: 192.168.1.11
- mac address: 90:9D:78:66:A6:4C
- 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!