Tutorial: Sizing a Raspberry Pi UPS and Wiring the Automatic Shutdown

Contents
A UPS must supply the Raspberry Pi under load, report a mains outage and retain enough energy for an orderly shutdown. Battery capacity alone answers none of these three questions. Usable output energy, peak current capability and cooperation between the UPS and operating system determine the result.
The calculation below is a planning example. The shutdown logic requires a documented, compatible UPS with a mains-failure output that pulls a GPIO signal to ground. Battery pack, charger and protection circuit must match the manufacturer’s approved configuration.

1. Measure the complete load
A power supply’s rating describes available capacity, not Pi consumption. Measurement at the 5 V input includes the Pi and USB devices powered through that input. The meter must support the USB-C supply arrangement and required current. Separately powered equipment needs a separate energy budget.
On Pi 5, vcgencmd pmic_read_adc exposes internal measurements. USB consumption and loads connected directly to 5 V are absent from the sum of PMIC rails. That sum therefore cannot represent total input power and is not a general measurement method for both Pi 4 and Pi 5.
Sizing requires idle, normal workload and the highest intended load, including storage activity and startup peaks. The UPS output must supply the corresponding current without an unacceptable voltage drop. A large battery cannot compensate for an undersized converter.
2. Calculate energy and reserve explicitly
Watt-hours are approximately amp-hours multiplied by nominal voltage. Two example cells rated at 3.4 Ah and 3.7 V each contain 25.16 Wh in total. The approved pack’s actual nominal voltage matters, not its full-charge voltage. A cell-level mAh rating cannot establish USB runtime without voltage and conversion information.
This example assumes 80 percent usable cell energy and 88 percent converter efficiency. The first factor represents the selected design case, including age, temperature and usable discharge range. Neither factor is a universal measurement. A further 1 Wh at the output is reserved for shutdown.
E_cells = 2 × 3.4 Ah × 3.7 V = 25.16 Wh
E_output = 25.16 Wh × 0.80 × 0.88 = 17.71264 Wh
E_runtime = 17.71264 Wh − 1 Wh = 16.71264 Wh
t_runtime = E_runtime / P_load| Assumed load | Calculated time until reserve |
|---|---|
| 6 W | about 2 h 47 min |
| 9 W | about 1 h 51 min |
| 12 W | about 1 h 24 min |
The power levels are calculation cases, not typical Pi configurations. At a constant 12 W, 1 Wh provides five minutes. An assumed 60-second shutdown consumes 0.2 Wh at that load. Actual shutdown duration needs measurement with the intended services, drives and possible timeouts.
Usable energy already measured at the 5 V output includes converter losses. Applying efficiency again would subtract the same loss twice. A discharge cutoff already reflected in a capacity test also requires no second arbitrary deduction.
3. Distinguish mains failure from a critical battery
A mains-failure signal permits a delay for brief interruptions. A critical battery may require immediate shutdown instead. A fixed 60-second timer is suitable only when remaining energy covers the delay and shutdown, including at monitor startup and after repeated outages. A suitable UPS needs an independent protection circuit; a device-specific battery monitor may provide an earlier emergency path.
The MAX17048 estimates state of charge for a single-cell system; the related MAX17049 supports two cells in series. The INA219 measures current, voltage and power and does not independently provide battery capacity in percent. Estimated charge does not remove the need to test at high load. A universal 20-percent threshold guarantees no particular remaining runtime.
4. Choose a simple overlay or a delayed monitor
The gpio-shutdown overlay generates a KEY_POWER input event. A component such as systemd-logind handles shutdown. Without a working operating-system handler, shutdown is not guaranteed, especially during every boot phase. GPIO assignment, polarity and wiring must match the board. The overlay and the following monitor must not claim the same line simultaneously.
The monitor uses the libgpiod 2 Python interface. An older 1.x installation is incompatible. The chip device and line offset must be identified with the locally available GPIO tools and board documentation. A physical header pin number is not a line offset; gpiochip0 and offset 17 are not universal settings.
The electrical assumption is a documented 3.3 V-compatible open-drain output: low means mains failure, high means mains present, with common ground and an appropriate pull-up. A broken wire can appear as mains present because of the pull-up; this example does not detect that fault. A 5 V signal must not connect directly to the GPIO. Other signal arrangements need appropriate electrical adaptation and verified logic.
#!/usr/bin/env python3
import argparse
import math
import subprocess
import time
class OutageTimer:
def __init__(self, delay):
if not math.isfinite(delay) or delay <= 0:
raise ValueError("delay must be positive and finite")
self.delay = delay
self.since = None
self.previous = None
def sample(self, failed, now):
if type(failed) is not bool or not math.isfinite(now):
raise ValueError("invalid sample")
if self.previous is not None and not 0 <= now - self.previous <= 5:
raise RuntimeError("monitoring gap exceeds 5 seconds")
self.previous = now
if not failed:
self.since = None
return False
if self.since is None:
self.since = now
return now - self.since >= self.delay
def monitor(read_failed, delay, action, clock=time.monotonic, sleep=time.sleep):
timer = OutageTimer(delay)
while True:
failed = read_failed() # Read immediately, including at startup.
if timer.sample(failed, clock()):
action()
return
sleep(1)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--chip", required=True)
parser.add_argument("--line", required=True, type=int)
parser.add_argument("--delay", type=float, default=60)
parser.add_argument("--poweroff", action="store_true")
args = parser.parse_args()
OutageTimer(args.delay)
if args.line < 0:
parser.error("line offset must be non-negative")
import gpiod
from gpiod.line import Bias, Direction, Value
settings = gpiod.LineSettings(direction=Direction.INPUT,
active_low=False, bias=Bias.PULL_UP)
with gpiod.request_lines(args.chip, consumer="ups-watch",
config={args.line: settings}) as request:
last = [None]
def read_failed():
value = request.get_value(args.line)
if value not in (Value.ACTIVE, Value.INACTIVE):
raise RuntimeError("unknown GPIO state")
failed = value == Value.INACTIVE # Physical low = mains failure.
if failed != last[0]:
print("UPS mains_failed=" + str(failed), flush=True)
last[0] = failed
return failed
def action():
print("UPS delay reached; poweroff=" + str(args.poweroff), flush=True)
if args.poweroff:
subprocess.run(["/usr/bin/systemctl", "--no-block", "poweroff"],
check=True, timeout=10)
monitor(read_failed, args.delay, action)
if __name__ == "__main__":
main()The first state is read immediately, so a failure already present at startup also starts the timer. An observed high state resets the delay. Timing uses a monotonic clock. Samples are approximately one second apart; short transitions between samples may go undetected. The example therefore does not prove an uninterrupted outage and is not a real-time protection function.
Read errors and observed sampling gaps above five seconds terminate the process with an error. Restarting resets the timer, so repeated failures need an alert. Without --poweroff, the program only logs the reached threshold and exits. This mode supports an initial controlled signal check but never requests shutdown.
5. Enable the service after signal checks
The verified script belongs at /usr/local/lib/ups_watch.py and the following unit at /etc/systemd/system/ups-watch.service. The file /etc/ups-watch.conf must define UPS_CHIP as the verified device path and UPS_LINE as the verified offset. Only administrative access should permit script or configuration changes. The unit explicitly includes --poweroff and can shut down the computer.
[Unit]
Description=UPS mains-failure monitor
StartLimitIntervalSec=60
StartLimitBurst=3
[Service]
Type=simple
EnvironmentFile=/etc/ups-watch.conf
ExecStart=/usr/bin/python3 /usr/local/lib/ups_watch.py --chip ${UPS_CHIP} --line ${UPS_LINE} --delay 60 --poweroff
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetA signal check without the shutdown option precedes reloading systemd configuration and deliberately enabling the service. An additional After=multi-user.target does not belong here: combined with membership of that target, a cyclic startup order can result. Limited restarts handle transient failures; a persistently failed service needs separate alerting.
6. Test shutdown and recovery together
systemctl --no-block poweroff requests shutdown. The call confirms neither completion nor removal of the physical 5 V supply. A UPS shutdown handshake must allow the operating system sufficient time. Bootloader settings that reduce halted power consumption do not replace this sequence.
PSU_MAX_CURRENT=5000 is not a general UPS setting either. Declaring a current capability cannot create a supply capable of delivering 5 A. Current-limit changes require an appropriately documented hardware configuration.
Acceptance testing needs a verified recovery path and a controlled test load. The plan covers a short outage, a long outage, startup without mains, low battery, another outage before full recharge, and mains returning during or after shutdown. Deliberately running a production filesystem until voltage collapses is unnecessary. Remaining output energy can be measured separately with a suitable test load.
Previous-boot logs require available persistent storage. Missing entries in journalctl -b -1 do not prove abrupt power loss; volatile journals disappear too. Logs, observed shutdown and measured output voltage provide complementary evidence.
Ageing depends on chemistry, temperature, state of charge and usage. A fixed doubling of battery life from an arbitrary reduction in charging voltage cannot be inferred. Charging and discharge limits must follow the approved manufacturer profile. Repeated capacity and shutdown tests within an appropriate maintenance schedule provide a basis for timely replacement.
Questions and answers
Does a halted Pi restart automatically when mains returns?
Not necessarily. If the UPS keeps the 5 V rail active throughout, no fresh power-on transition occurs. Reliable recovery requires a designed output power cycle or a compatible wake controller. Mains returning before and after the UPS output turns off are separate test cases.
Is the 60-second delay sufficient protection?
Only with verified remaining energy for the delay and complete shutdown. An almost empty battery may fail sooner. Battery protection, battery-state reporting and failure alerts remain necessary.