Close

Final Python Code...we're finished.

A project log for Single-cell Li-Ion Powered UPS for Raspberry Pi

A simple yet complete UPS solution for most Raspberry Pi embedded applications, using a single-cell Li-Ion Battery.

bud-bennettBud Bennett 09/10/2017 at 19:430 Comments

This is (hopefully) the last log. The project is complete at this point.

There are some bits of code that is necessary to get the UPS working properly. Paul Versteeg upgraded the code to include a logging capability and also provided a service file for systemd so it runs nicely in the background. 

UPS_POWERFAIL.PY

#!/usr/bin/python3
#-------------------------------------------------------------------------------
# Name:        ups_powerfail.py
# Purpose:
#
# Author:      Bud Bennett (modified by Paul Versteeg)
#
# Created:     31-08-2017
# Copyright:   (c) Bud Bennett 2017
# Licence:     <your licence>
#-------------------------------------------------------------------------------
 
 
 
'''
This program monitors the status of the UPS via the i2c interface and
manages the Raspberry Pi shutdown when the UPS signals that the battery
is exhausted. It lets the UPS determine when the battery is exhausted.
If the UPS battery voltage (VBAT) is greater than 3.2V, the status is checked
every minute. If VBAT < 3.2V then the checking interval is shortened to 1 second.
When the UPS asserts BATLOW = 1, the UPS will wait 5 seconds for the Raspberry Pi
to acknowledge by setting the SHUTDN bit. If the Pi sets SHUTDN = 1 then the UPS immediately
begins a 20 second timer to terminate the power. If the Pi has not set the SHUTDN bit
within the 5 second period then the UPS begins the 20 second timer irregardless.
So the Pi must initiate it's shutdown sequence immediately after receiving the BATLOW
signal from the UPS and sending/confirming the SHUTDN acknowledgement. The Pi can also
shutdown the UPS at any time by sending the Shutdown Command (0x04) to the UPS.
'''
 
try:
    from smbus import SMBus
except ImportError as e:
    print("Python module smbus not found, install it first")
    # Install the i2c interface using the following instructions:
    # sudo apt-get install i2c-tools
    # sudo apt-get install python-smbus
    # sudo raspi-config , goto Interfacing Options, enable i2c
    # sudo reboot # to make the i2c interface active
    # now check to see if the USB i2c device is present at address 36
    # sudo i2cdetect -y 1
    sys.exit(0)
 
import time, os
from datetime import datetime
import logging
import logging.handlers
import traceback
import sys
 
# Global flag to print more information
DEBUG = True
# If this script is installed as a Daemon by systemd, set this flag to True:
DAEMON = True # do not send stuff to the console
 
i2c = SMBus(1)  # set interface to i2c-1 (newer RPi's)
ups_address = 0x36
command = 0x00
VBAT = 5
bus_fail = 0
pgood_flag = 1
 
 
#--logger definitions
# save daily logs for 7 days
LOG_FILENAME = "path-to-ups_mon.log"  # log file path
LOG_LEVEL = logging.INFO  # Could be e.g. "TRACE", "ERROR", "" or "WARNING"
logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="midnight", backupCount=7)
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
 
 
def  write_log(mode, msg=""):
    '''
    Create a log file with information during the running of this app.
    Log information with a qualifier to easier sort it.
 
    '''
 
    if mode == "info" or mode == "message" or mode == "trace" :
        logger.info(msg)
        return
 
    if mode == "debug" :
        logger.debug(msg)
        return
 
    if mode == "error" :
        logger.error(msg)
        return
 
    if mode == "warning" :
        logger.warning(msg)
        return
 
    if mode == "critical" or mode == "exception" :
        logger.critical(msg)
        return
    # catch all
    if (DEBUG) and (not DAEMON) : print( "---catch all---"), mode
    logger.critical(msg)
    return
 
 
def read_ups_data(address=0x36, cmd=0x00):
 
    bus_fail = 0
 
    if (DEBUG) and (not DAEMON) : print('Sending command {0:02x}'.format(command))
    # write_log("trace", "Sending command {0:02x}".format(command))
 
    try:
        word = i2c.read_word_data(ups_address, command)
    except Exception as e: # except all exceptions
 
        if (DEBUG) and (not DAEMON): print('I2C exception')
        write_log("error", "read_ups: I2C exception")
        bus_fail = 1
 
    if bus_fail == 1:
        VBAT = 5.0
        BATLOW = 0
        SHUTDN = 0
        PWRGOOD = 1
    else:
        byte1 = 255 & word
        byte2 = word >> 8
        PWRGOOD = byte1 & 0x01
        BATLOW = (byte1 & 0x02) >> 1
        SHUTDN = (byte1 & 0x04) >> 2
        if byte2 != 0:
            VBAT = 2.048/byte2 * 255
        else:
            VBAT = 5.0
            if (DEBUG) and (not DAEMON): print('read_ups: ADC value error')
            write_log("error", "ADC value error")
        bus_fail = 0
 
 
    if (DEBUG) and (not DAEMON):
        print('PWRGOOD = {0}'.format(int(PWRGOOD)))
        print('BATLOW = {0}'.format(int(BATLOW)))
        print('SHUTDN = {0}'.format(int(SHUTDN)))
        print('VBAT = {0:.2f}\n'.format(VBAT))
 
    return PWRGOOD, BATLOW, SHUTDN, VBAT, bus_fail
 
 
def main():
    global command, pgood_flag
 
    time_now = datetime.now()
    if (DEBUG) and (not DAEMON) : print("{0} Powerfail is active.".format(time_now))
    write_log("trace","*** Powerfail is active")
 
    try:
        while True:
            # read the UPS
            PWRGOOD, BATLOW, SHUTDN, VBAT, bus_fail = read_ups_data(ups_address, cmd=command)
            
            # de-clutter log -- only send powerfail info once
            if (not PWRGOOD and pgood_flag):
                   time_now = datetime.now()
                   pgood_flag = 0
                   if (DEBUG) and (not DAEMON) : print("{0} Powerfail: Power failed.".format(time_now))
                   write_log("trace", "Power failed: PWRGOOD = {0} BATLOW = {1} SHUTDN = {2} VBAT = {3:.2f}".format(PWRGOOD, BATLOW, SHUTDN, VBAT))
            elif(PWRGOOD and not pgood_flag):
                   time_now = datetime.now()
                   pgood_flag = 1
                   if (DEBUG) and (not DAEMON) : print("{0} Powerfail: Power restored.".format(time_now))
                   write_log("trace", "Power restored: PWRGOOD = {0} BATLOW = {1} SHUTDN = {2} VBAT = {3:.2f}".format(PWRGOOD, BATLOW, SHUTDN, VBAT))
                   
            # if the UPS has set BATLOW or VBAT < 3V, then send SHUTDN command to initiate UPS shutdown
            if (BATLOW or (VBAT < 3.0 and not PWRGOOD)):
                if (DEBUG) and (not DAEMON) : print('Powerfail: Sending shutdown command.')
                write_log("trace", "Sending shutdown command to UPS: PWRGOOD = {0} BATLOW = {1} SHUTDN = {2} VBAT = {3:.2f}".format(PWRGOOD, BATLOW, SHUTDN, VBAT))
                command = 0x04
                PWRGOOD, BATLOW, SHUTDN, VBAT, bus_fail = read_ups_data(ups_address, cmd=command)
                                                                
            # confirm that the UPS received the shutdown command and then shutdown the Pi
            if (SHUTDN and command == 0x04):
                if (DEBUG) and (not DAEMON) : print('UPS confirmed, shutting down now!')
                write_log("trace", "UPS confirmed, shutting down now!")
                os.system("sudo shutdown -h now")
                while True:
                    time.sleep(10)
 
            # check UPS status at 1 minute intervals until battery voltage drops below 3.2V, then
            # decrease interval to 1 second.
 
            if (VBAT < 3.2):
                time.sleep(1)
            else:
                time.sleep(60) # use 10 seconds during testing, 60 when running normally
 
    except KeyboardInterrupt:
        if (DEBUG) and (not DAEMON): print ("\nCtrl-C Terminating")
 
    except Exception as e:
        sys.stderr.write("Got exception: %s" % e)
        if (DEBUG) and (not DAEMON): print(traceback.format_exc())
        write_log("exception", str(traceback.format_exc()))
        os._exit(1)
 
if __name__ == '__main__':
    main()

 ups_powerfail.service File:

# This service installs a python script that communicates with the UPS hardware.
# It also provides logging to a file
# the ups_powerfail.service is located in /lib/systemd/system/
# To test, use sudo systemctl start|stop|status powerfail
# To install during the boot process, use: sudo systemctl enable ups_powerfail
# If this file gets changed, use: sudo systemctl daemon-reload
# If the Python script is changed, use : sudo systemctl restart ups_powerfail
 
[Unit]
Description=UPS Powerfail Service
Requires=basic.target
After=multi-user.target
 
[Service]
ExecStart=/usr/bin/python path-to-ups_powerfail.py
Restart=on-failure
RestartSec=10
TimeoutSec=10
 
# The number of times the service is restarted within a time period can be set
# If that condition is met, the RPi can be rebooted
# WARNING:
# Only use these options with a working system!
#StartLimitBurst=4
#StartLimitInterval=180s
# actions can be none|reboot|reboot-force|reboot-immidiate
#StartLimitAction=reboot
 
# The following are defined the /etc/systemd/system.conf file and are
# global for all services
#
#DefaultTimeoutStartSec=90s
#DefaultTimeoutStopSec=90s
#
# They can also be set on a per process here:
# if they are not defined here, they fall back to the system.conf values
#TimeoutStartSec=2s
#TimeoutStopSec=2s
 
[Install]
WantedBy=multi-user.target

I ran several power failure cycles on my Raspberry Pi Model B, with the Raspbian sketch OS installed, a few days ago. This is the output of the log file.

Log Output:

2017-09-07 16:55:43,089 INFO     *** Powerfail is active
2017-09-07 16:56:43,153 INFO     Power failed: PWRGOOD = 0 BATLOW = 0 SHUTDN = 0 VBAT = 3.55
2017-09-07 16:59:43,288 INFO     Power restored: PWRGOOD = 1 BATLOW = 0 SHUTDN = 0 VBAT = 3.68
2017-09-07 17:00:43,301 INFO     Power failed: PWRGOOD = 0 BATLOW = 0 SHUTDN = 0 VBAT = 3.53
2017-09-07 17:12:07,007 INFO     Sending shutdown command to UPS: PWRGOOD = 0 BATLOW = 0 SHUTDN = 0 VBAT = 2.98
2017-09-07 17:12:07,009 INFO     UPS confirmed, shutting down now!
2017-09-07 17:14:14,191 INFO     *** Powerfail is active
2017-09-07 22:06:27,317 INFO     Power failed: PWRGOOD = 0 BATLOW = 0 SHUTDN = 0 VBAT = 4.08
2017-09-07 23:30:32,978 INFO     Sending shutdown command to UPS: PWRGOOD = 0 BATLOW = 0 SHUTDN = 0 VBAT = 2.98
2017-09-07 23:30:32,980 INFO     UPS confirmed, shutting down now!

FIN.

Discussions