Intro

For an audio project, I needed a raspberry pi with buffered external power, so the device can do a grecaful shutdown as soon as the external power is unplugged or lost.
There are two ways to realize this:

  • The DYI approach by using large(r) capacitors or
  • A ready to use battery hat

I don’t like the idea of having large electrolytic capacitors that could pop at any time anywhere near my raspberry pi or my other audio equipment, which is why I went for the battery approach.

The key idea is that as soon as the raspberry pi looses external power for whatever reason, the battery buffers operation for long enough so the device can shut down gracefully.
Bonus points if whatever hardware we’re gonna use is able to tell us that external power is available or not.

Hardware

The UPS hat from waveshare comes with a standardized 3.7V rechargeable battery.
It connects to the RPi via pogo connectors.
Make sure to have clean solder joints on the hat connectors.
The hat provides data via I2C bus.

Prerequisites

Also see the wiki: https://www.waveshare.com/wiki/UPS_HAT_(C)

Enable I2C on the RPi

Make sure to enable i2c: https://www.raspberrypi-spy.co.uk/2014/11/enabling-the-i2c-interface-on-the-raspberry-pi/

Get the INA219 Python module

Get the INA219 python module (as mentioned in the wiki, too)

$ sudo apt-get install p7zip
$ wget https://files.waveshare.com/upload/4/40/UPS_HAT_C.7z
$ 7zr x UPS_HAT_C.7z -r -o./
$ cd UPS_HAT_C
$ python3 INA219.py

Scripting

The INA219 module needs to be configured initially

UPS_I2C_ADDR = 0x43
SHUNT_OHMS = 0.1
ina = INA219(SHUNT_OHMS, address=UPS_I2C_ADDR)
ina.configure(ina.RANGE_16V)

which I encapsulated in a UPS class.

The INA219 module enables you to read the hat’s charging current via i2c by calling the ina.current() method.
If the hat has external power, the charging current will be >= 0, i.e. the battery is charging. Even if the battery is fully charged, this current reading will be >= 0.
If the hat looses external power, the charging current will be < 0.
Neither hat or module generate an event on changes, so the current needs to be read by polling.

Note: During my tests, I experienced spurious readings of a current < 0 sporadically, so I recommend not doing a straight

if current < 0.0:
    shutdown()

Because then, the device will shutdown on spurious current readings.
For this reason, I introduced a back-off timer and the device will not shutdown before that timer has elapsed.
If the back-off timer is running and the device reads positive current values again, the timer is stopped.

It’s not the most genius code, but it has been working well and reliable (so far :))

Create a file ups_watchdog.py

#!/usr/bin/env python3

import threading
import time
import os
from ina219 import INA219
from ina219 import DeviceRangeError
import syslog

UPS_I2C_ADDR = 0x43
BACKOFF_TIME = 10 # seconds
POLL_INTERVAL = 1 # seconds

class Timer:
    timer = None
    interval = None
    callback = None
    def __init__(self, interval, callback):
        self.interval = interval
        self.callback = callback
    def my_callback(self):
        self.callback()
    def is_running(self):
        return self.timer != None and self.timer.is_alive()
    def start(self):
        self.timer = threading.Timer(self.interval, self.my_callback)
        self.timer.start()
    def stop(self):
        if (self.timer != None):
            self.timer.cancel()
        self.timer = None

class Log:
    def info(message):
        syslog.syslog(syslog.LOG_INFO, message)
    def warn(message):
        syslog.syslog(syslog.LOG_WARNING, message)
    def error(message):
        syslog.syslog(syslog.LOG_ERR, message)

class UPS:
    ina = None
    cur = 0.0
    cof = False
    def __init__(self):
        self.ina = INA219(0.1, address=UPS_I2C_ADDR)
        self.ina.configure(self.ina.RANGE_16V)
    def is_on_battery(self):
        return self.cur < 0.0
    def current(self):
        return self.cur
    def current_of(self):
        return self.cof
    def read(self):
        try:
            self.cur = self.ina.current()
            self.cof = self.ina.current_overflow()
        except DeviceRangeError as e:
            Log.error(e)

def timer_callback():
    Log.warn("Backoff-Timer expired. Shutting down.")
    os.system("sudo shutdown -h now")

def main():
    timer = Timer(BACKOFF_TIME, timer_callback)
    ups   = UPS()

    Log.info("UPS watchdog is running.")

    while True:
        ups.read()
        if ups.is_on_battery() and not timer.is_running():
            Log.warn("Running on battery. Shutting down in {}s.".format(BACKOFF_TIME))
            Log.warn("UPS current: {} overflow: {}".format(ups.current(), ups.current_of()))
            timer.start()

        if not ups.is_on_battery() and timer.is_running():
            Log.warn("Power is back. Stopping timer.")
            timer.stop()

        time.sleep(POLL_INTERVAL)

if __name__ == "__main__":
    main()

Systemd Service

/etc/systemd/system/upswatch.service

[Unit]
Description=RPi UPS HAT Watchdog service
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=5
User=user
ExecStart=/home/user/UPS_HAT_C/ups_watchdog.py

[Install]
WantedBy=multi-user.target
$ sudo systemctl enable upswatch.service
$ sudo systemctl start upswatch.service
Last modified: 02.06.2024