A long time ago, I covered a UPS extension board for RaspberryPi from 52pi. It was a great option at the time to keep your favourite board from shutting down abruptly on power loss. Unfortunately, the unexpected shutdown was still on the cards as the board had no way of letting the Pi know it is about to run out of power. That’s all about to change thanks to Waveshare’s UPS HAT(Amazon, AliExpress, ThePiHut).
Power loss protection with Waveshare UPS HAT
A recent power cut at work, which lasted almost an hour, reminded me that despite living in a country where power cuts rarely happen, it’s good to have your buttocks covered. After all, pulling the plug on a Raspberry Pi board can result in data loss and what’s worse, data corruption.
I think ThePiHut find out about that, as a day later I received a tempting email listing various Raspberry Pi items in stock, including the Waveshare UPS HAT. Who could resist the price of £21 for a 18650 powered board with interesting features?
The board screws up to the bottom of Raspberry Pi 4, 3B+/3 and supplies 5V with up to 5A peak current to keep the board operational even at the most demanding loads, with USB peripherals at work. Unlike the old 52pi counterpart, apart from 2 pogo pins delivering the power, Waveshare UPS HAT uses 2 extra pogo pins to communicate the UPS data via I2C. This design doesn’t obstruct the 40-pin header so you can slap extra HATs on your Raspberry Pi board.
The board requires 2 x 18650 batteries which output 8.4V to keep everything powered. To keep your board protected Waveshare UPS HAT is equipped with overcharge/discharge protection, over current protection, short circuit protection, and reverse protection – in case you decide to change the batteries without paying attention to the orientation. As a bonus, you also have an extra USB-A power to charge or power up another device. Waveshare UPS HAT comes with a dedicated 8.4V/2A charger and uses a DC jack to deliver the power. Once installed, you can ditch the Raspberry Pi power supply.
UPS but how long?
It’s near impossible to answer this question upfront, as it depends on a couple of conditions. First, you have to take the batteries capacity. 18650 comes in various flavours and Pi’s runtime will depend on the mAh stored.
Secondly, the power use of the board will change based on the number of peripherals connected and the amount of power Raspberry Pi needs to perform the computational operations. The board can draw as little as 300mAh for Raspberry Pi 3B in idle state to 3 times as much for the latest board during power-intensive benchmarks.
You should expect around 2-4h based on your load and the cells included. The good news is that your Raspberry Pi will be aware of how much juice Waveshare UPS HAT has, and can perform a shutdown safely when the power levels get critical. I fitted mine with 1200mAh cells and run a CPU intensive benchmark to see how long would the board last. With the current draw oscillating between 0.6-0.8mAh, the board shut the system down (I have it set at 25%) after exactly 2h.
The batteries were charged up again 2:20min later.
In use
Attach the Waveshare UPS HAT according to instructions, and power everything up. To receive the power usage data from the HAT, you have to enable the I2C interface in sudo raspi-config. Then try out the default Waveshare’s script to read the data from the board:
sudo apt-get install p7zip
wget https://www.waveshare.com/w/upload/4/4a/UPS_HAT_B.7z
7zr x UPS_HAT_B.7z -r -o./
cd UPS_HAT_B
python3 INA219.py
The script works on the latest version of RaspberryPi OS (Bullseye) but if you come across any issues like:
The installed code will give you access to the following metrics:
Load Voltage
Current (charge and discharge rate)
Power Usage
Battery level in %
PSU Voltage (optional)
Shunt Voltage (optional)
Waveshare UPS HAT for Raspberry Pi 4
The sample code doesn’t include the safe shutdown routine, so you have to implement it yourself. I added a quick and dirty IF statement that shuts down my Raspberry Pi board at 30% power. Why 30%? It’s not healthy to discharge the cells, and I’m more concerned about safe shut-down than keeping the board running without an external source of power. After all, if the power supply is down, chances are, my entire automation has no power as well, leaving me only with the battery-powered ZigBee devices.
Safe shut-down at 30%
#!/usr/bin/python3
import smbus
import time
import sys
from subprocess import call
import requests
import json
# Config Register (R/W)
_REG_CONFIG = 0x00
# SHUNT VOLTAGE REGISTER (R)
_REG_SHUNTVOLTAGE = 0x01
# BUS VOLTAGE REGISTER (R)
_REG_BUSVOLTAGE = 0x02
# POWER REGISTER (R)
_REG_POWER = 0x03
# CURRENT REGISTER (R)
_REG_CURRENT = 0x04
# CALIBRATION REGISTER (R/W)
_REG_CALIBRATION = 0x05
class BusVoltageRange:
"""Constants for ``bus_voltage_range``"""
RANGE_16V = 0x00 # set bus voltage range to 16V
RANGE_32V = 0x01 # set bus voltage range to 32V (default)
class Gain:
"""Constants for ``gain``"""
DIV_1_40MV = 0x00 # shunt prog. gain set to 1, 40 mV range
DIV_2_80MV = 0x01 # shunt prog. gain set to /2, 80 mV range
DIV_4_160MV = 0x02 # shunt prog. gain set to /4, 160 mV range
DIV_8_320MV = 0x03 # shunt prog. gain set to /8, 320 mV range
class ADCResolution:
"""Constants for ``bus_adc_resolution`` or ``shunt_adc_resolution``"""
ADCRES_9BIT_1S = 0x00 # 9bit, 1 sample, 84us
ADCRES_10BIT_1S = 0x01 # 10bit, 1 sample, 148us
ADCRES_11BIT_1S = 0x02 # 11 bit, 1 sample, 276us
ADCRES_12BIT_1S = 0x03 # 12 bit, 1 sample, 532us
ADCRES_12BIT_2S = 0x09 # 12 bit, 2 samples, 1.06ms
ADCRES_12BIT_4S = 0x0A # 12 bit, 4 samples, 2.13ms
ADCRES_12BIT_8S = 0x0B # 12bit, 8 samples, 4.26ms
ADCRES_12BIT_16S = 0x0C # 12bit, 16 samples, 8.51ms
ADCRES_12BIT_32S = 0x0D # 12bit, 32 samples, 17.02ms
ADCRES_12BIT_64S = 0x0E # 12bit, 64 samples, 34.05ms
ADCRES_12BIT_128S = 0x0F # 12bit, 128 samples, 68.10ms
class Mode:
"""Constants for ``mode``"""
POWERDOW = 0x00 # power down
SVOLT_TRIGGERED = 0x01 # shunt voltage triggered
BVOLT_TRIGGERED = 0x02 # bus voltage triggered
SANDBVOLT_TRIGGERED = 0x03 # shunt and bus voltage triggered
ADCOFF = 0x04 # ADC off
SVOLT_CONTINUOUS = 0x05 # shunt voltage continuous
BVOLT_CONTINUOUS = 0x06 # bus voltage continuous
SANDBVOLT_CONTINUOUS = 0x07 # shunt and bus voltage continuous
class INA219:
def __init__(self, i2c_bus=1, addr=0x40):
self.bus = smbus.SMBus(i2c_bus);
self.addr = addr
# Set chip to known config values to start
self._cal_value = 0
self._current_lsb = 0
self._power_lsb = 0
self.set_calibration_32V_2A()
def read(self,address):
data = self.bus.read_i2c_block_data(self.addr, address, 2)
return ((data[0] * 256 ) + data[1])
def write(self,address,data):
temp = [0,0]
temp[1] = data & 0xFF
temp[0] =(data & 0xFF00) >> 8
self.bus.write_i2c_block_data(self.addr,address,temp)
def set_calibration_32V_2A(self):
"""Configures to INA219 to be able to measure up to 32V and 2A of current. Counter
overflow occurs at 3.2A.
..note :: These calculations assume a 0.1 shunt ohm resistor is present
"""
# By default we use a pretty huge range for the input voltage,
# which probably isn't the most appropriate choice for system
# that don't use a lot of power. But all of the calculations
# are shown below if you want to change the settings. You will
# also need to change any relevant register settings, such as
# setting the VBUS_MAX to 16V instead of 32V, etc.
# VBUS_MAX = 32V (Assumes 32V, can also be set to 16V)
# VSHUNT_MAX = 0.32 (Assumes Gain 8, 320mV, can also be 0.16, 0.08, 0.04)
# RSHUNT = 0.1 (Resistor value in ohms)
# 1. Determine max possible current
# MaxPossible_I = VSHUNT_MAX / RSHUNT
# MaxPossible_I = 3.2A
# 2. Determine max expected current
# MaxExpected_I = 2.0A
# 3. Calculate possible range of LSBs (Min = 15-bit, Max = 12-bit)
# MinimumLSB = MaxExpected_I/32767
# MinimumLSB = 0.000061 (61uA per bit)
# MaximumLSB = MaxExpected_I/4096
# MaximumLSB = 0,000488 (488uA per bit)
# 4. Choose an LSB between the min and max values
# (Preferrably a roundish number close to MinLSB)
# CurrentLSB = 0.0001 (100uA per bit)
self._current_lsb = .1 # Current LSB = 100uA per bit
# 5. Compute the calibration register
# Cal = trunc (0.04096 / (Current_LSB * RSHUNT))
# Cal = 4096 (0x1000)
self._cal_value = 4096
# 6. Calculate the power LSB
# PowerLSB = 20 * CurrentLSB
# PowerLSB = 0.002 (2mW per bit)
self._power_lsb = .002 # Power LSB = 2mW per bit
# 7. Compute the maximum current and shunt voltage values before overflow
#
# Max_Current = Current_LSB * 32767
# Max_Current = 3.2767A before overflow
#
# If Max_Current > Max_Possible_I then
# Max_Current_Before_Overflow = MaxPossible_I
# Else
# Max_Current_Before_Overflow = Max_Current
# End If
#
# Max_ShuntVoltage = Max_Current_Before_Overflow * RSHUNT
# Max_ShuntVoltage = 0.32V
#
# If Max_ShuntVoltage >= VSHUNT_MAX
# Max_ShuntVoltage_Before_Overflow = VSHUNT_MAX
# Else
# Max_ShuntVoltage_Before_Overflow = Max_ShuntVoltage
# End If
# 8. Compute the Maximum Power
# MaximumPower = Max_Current_Before_Overflow * VBUS_MAX
# MaximumPower = 3.2 * 32V
# MaximumPower = 102.4W
# Set Calibration register to 'Cal' calculated above
self.write(_REG_CALIBRATION,self._cal_value)
# Set Config register to take into account the settings above
self.bus_voltage_range = BusVoltageRange.RANGE_32V
self.gain = Gain.DIV_8_320MV
self.bus_adc_resolution = ADCResolution.ADCRES_12BIT_32S
self.shunt_adc_resolution = ADCResolution.ADCRES_12BIT_32S
self.mode = Mode.SANDBVOLT_CONTINUOUS
self.config = self.bus_voltage_range << 13 | \
self.gain << 11 | \
self.bus_adc_resolution << 7 | \
self.shunt_adc_resolution << 3 | \
self.mode
self.write(_REG_CONFIG,self.config)
def getShuntVoltage_mV(self):
self.write(_REG_CALIBRATION,self._cal_value)
value = self.read(_REG_SHUNTVOLTAGE)
if value > 32767:
value -= 65535
return value * 0.01
def getBusVoltage_V(self):
self.write(_REG_CALIBRATION,self._cal_value)
self.read(_REG_BUSVOLTAGE)
return (self.read(_REG_BUSVOLTAGE) >> 3) * 0.004
def getCurrent_mA(self):
value = self.read(_REG_CURRENT)
if value > 32767:
value -= 65535
return value * self._current_lsb
def getPower_W(self):
self.write(_REG_CALIBRATION,self._cal_value)
value = self.read(_REG_POWER)
if value > 32767:
value -= 65535
return value * self._power_lsb
if __name__=='__main__':
# Create an INA219 instance.
ina219 = INA219(addr=0x42)
while True:
bus_voltage = ina219.getBusVoltage_V() # voltage on V- (load side)
shunt_voltage = ina219.getShuntVoltage_mV() / 1000 # voltage between V+ and V- across the shunt
current = ina219.getCurrent_mA() # current in mA
power = ina219.getPower_W() # power in W
p = (bus_voltage - 6)/2.4*100
if(p > 100):p = 100
#shutdown happens when battery is below 25% and the device is actively discharging
if(p < 25 and current/1000 < -0.30):
call("sudo shutdown --poweroff", shell=True) #safe poweroff at 30 percent
sys.exit()
if(p < 0):p = 0
# INA219 measure bus voltage on the load side. So PSU voltage = bus_voltage + shunt_voltage
#print("PSU Voltage: {:6.3f} V".format(bus_voltage + shunt_voltage))
#print("Shunt Voltage: {:9.6f} V".format(shunt_voltage))
print("Load Voltage: {:6.3f} V".format(bus_voltage))
print("Current: {:9.6f} A".format(current/1000))
print("Power: {:6.3f} W".format(power))
print("Percent : {:3.1f}".format(p))
print("Shutdown at 25%")
print("")
#if you want to send the data via REST here is how
#data = {"load": round(bus_voltage, 2), "current": round(current/1000, 2), "power": round(power, 2), "battery": round(p, 2)}
#data = json.dumps(data)
#URL = "http://home.local:1880/power"
#headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
#response = requests.post(URL, data, headers, auth=("user", "pass"))
time.sleep(30)
When triggered, it will schedule the shutdown, exit out the script and display a message:
Shutdown scheduled for Sat 2022-01-22 16:23:54 GMT, use 'shutdown -c' to cancel.
The system will terminate a minute later in a safe and planned manner.
It would be nice to see a simple battery indicator LED included on the PCB, but knowing that you can get the data from the board, you can always design your own indicator.
Final thoughts
Waveshare UPS HAT (Amazon, AliExpress, ThePiHut) is a nice improvement over the other UPS board I had. It keeps the server alive long enough to either survive a power cut or me accidentally blow up the main fuse or shut down safely when the power isn't restored in time. With dozen or so battery-operated ZigBee sensors, I have the incentive to log all that data even during the shutdown, and do my best to keep the system and collected data intact. After all, better safe than sorry, and £21 is a fair price to pay for that peace of mind. All I need now is a much better case! Let me know if you have any questions in this Reddit thread.
While you could wake up your PC from a mobile directly, having a dedicated server capable of doing so is the best solution. The reason is simple. You can hook up as many devices as you wish with a single endpoint. This is why Raspberry Pi is perfect for this.
From time to time my Internet grinds to a stop. Since Raspberry Pi 4 comes with a 1Gbps Ethernet, I decided to take advantage of it and create a reporting system in NodeRED that will monitor and report when the ISP is not keeping the contractual agreements. Works with Alexa, Google Home, Android and Windows 10.
This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.
Strictly Necessary Cookies
Strictly Necessary Cookie should be enabled at all times so that we can save your preferences for cookie settings.
If you disable this cookie, we will not be able to save your preferences. This means that every time you visit this website you will need to enable or disable cookies again.