Close

New Python Code Running Under systemd

A project log for Single SuperCapacitor UPS for Raspberry Pi

A last gasp Uninterruptible Power Supply for any Raspberry Pi computer.

bud-bennettBud Bennett 11/13/2018 at 19:350 Comments

I'm no code bunny. Here's the current working code: powerfail.py

#!/usr/bin/env python
'''
This program monitors the status of the PWRGOOD signal from the UPS circuit.
PWRGOOD is normally high. If it goes low then the input power has failed.
This program senses when PWRGOOD falls and then samples it once/second. An
accumulator counts up by 1 if power is bad and counts down by 3 if power is good.
This accounts for the difference in supercap charge current vs. Zero-W load current.
If the accumulator exceeds 20 then it signals the UPS to disconnect the
power by asserting SHUTDOWN. This will still cause a shutdown condition even when
the power is cycling on and off. After it commits the hardware to shutdown it also
sends a shutdown command to the Linux system. The UPS will hold power up for 20 seconds
after it receives the SHUTDOWN signal, which allows the Pi to enter a shutdown state
prior to power removal.
'''
import RPi.GPIO as GPIO
import os, time, sys
import logging
import logging.handlers
#import traceback

# global variables
COUNT_UP = 1
COUNT_DOWN = 3 # this relates to the ratio of charger current divided by load current
COUNT_MAX = 20 * COUNT_UP # how many seconds of power loss before the Pi shuts down
# set the GPIO pin assignments
PWRGOOD = 23
SHUTDOWN = 24

#--logger definitions
logger = logging.getLogger(__name__) 
logger.setLevel(logging.INFO)  # Could be e.g. "TRACE", "ERROR", "" or "WARNING"
handler = logging.handlers.RotatingFileHandler("/var/log/bud_logs/ups_mon.log", maxBytes=1000, backupCount=4) # save 4 logs
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

class MyLogger():
    '''
    A class that can be used to capture stdout and sterr to put it in the log

    '''
    def __init__(self, level, logger):
            '''Needs a logger and a logger level.'''
            self.logger = logger
            self.level = level

    def write(self, message):
        # Only log if there is a message (not just a new line)
        if message.rstrip() != "":
                self.logger.log(self.level, message.rstrip())

    def flush(self):
        pass  # do nothing -- just to handle the attribute for now



# configure the GPIO ports:
GPIO.setmode(GPIO.BCM)
# PWRGOOD is an input with weak pullup
GPIO.setup(PWRGOOD, GPIO.IN, pull_up_down = GPIO.PUD_UP)

def powerfail_detect(count_max, count_up, count_down):
    # if the count exceeds 20 the system will shutdown.
    count = 0
    pwr_flip = False
    while (count < count_max):
        time.sleep(1)
        if (GPIO.input(PWRGOOD)):
            if (pwr_flip):
                logger.info("Power is good")
            count -= count_down
            pwr_flip = False
            if (count <= 0):
                logger.info("Returning to normal status")
                return
        else:
            if (not pwr_flip):
                logger.info("Power has failed")
            count += count_up
            pwr_flip = True
    logger.info("powerfail iminent...shutting down the Pi!\n")

    GPIO.setup(SHUTDOWN,GPIO.OUT)
    GPIO.output(SHUTDOWN,0)
    time.sleep(0.1)
    GPIO.output(SHUTDOWN,1)
    time.sleep(0.1)
    
    os.system("sudo shutdown -h now")
    GPIO.cleanup()
    return

# If this script is installed as a Daemon, set this flag to True:
DAEMON = True # when run as daemon, pipe all console information to the log file
# --Replace stdout and stderr with logging to file so we can run it as a daemon
# and still see what is going on
if DAEMON :
    sys.stdout = MyLogger(logging.INFO, logger)
    sys.stderr = MyLogger(logging.ERROR, logger)

try:
    logger.info("Enabled")
    while True:
        time.sleep(1)
        if (not GPIO.input(PWRGOOD)):
            powerfail_detect(count_max=COUNT_MAX, count_up=COUNT_UP, count_down=COUNT_DOWN)
            
except:
    GPIO.cleanup()  # clean up GPIO on CTRL+C exit

 Note that the log file is located in /var/log/bud_logs/ups_mon.log. It will rotate the log after 1000bytes and keep the latest four logs. You can change all of that by altering the handler params. 

The above script is located in /home/pi/programs/ and runs as a service using systemd on either the Jessie or Stretch version of Raspbian. You can locate it anywhere as long as the service knows the path. The powerfail.service file below is located in /lib/systemd/system/

[Unit]
Description=Powerfail Service
Before=multi-user.target

[Service]
Type=idle
ExecStart=/usr/bin/python -u /home/pi/programs/powerfail.py
Restart=on-failure
RestartSec=10
TimeoutSec=10

[Install]
WantedBy=multi-user.target

Note that the ExecStart line must have full pathnames to both python and the program. In order to activate this service you need to type the following in a terminal shell:

sudo systemctl daemon-reload
sudo systemctl enable powerfail
sudo systemctl start powerfail

Now the service is running and will automatically run the  powerfail.py code upon boot. To check the status of the service simply type:

systemctl status powerfail

Here's an example of the log file output:

2018-11-13 11:38:01,870 INFO     powerfail iminent...shutting down the Pi!

2018-11-13 11:38:30,161 INFO     Enabled
2018-11-13 11:41:25,759 INFO     Power has failed
2018-11-13 11:41:29,767 INFO     Power is good
2018-11-13 11:41:30,772 INFO     Returning to normal status

 I will provide a copy of powerfail.py in the files section of this project.

Discussions