I picked up three LiitoKala 34B flat-top 18650s to populate the Waveshare UPS Module 3S sitting under my Raspberry Pi 5 home server. The cells are sold as a Panasonic NCR18650B rewrap — the printed cell label reads NCR18650B Li-ion. They came from the LiitoKala official store on AliExpress at about $2/cell after coupons and AliExpress coins (closer to $4 at sticker), which is well under half what genuine retail-channel Panasonic cells go for.
Vendor spec sheet
| Parameter | Value |
|---|---|
| Cell model | NCR18650B (Lii-34B) |
| Capacity | 3300–3400 mAh |
| Nominal voltage | 3.7 V |
| Charge cut-off | 4.2 V |
| Discharge cut-off | 2.5 V |
| Continuous discharge | 10 A |
| Internal resistance | <20 mΩ |
| Cycle life (claimed) | 1000 cycles |
| Weight | 47 g ±1 g |
| Size | 18 × 65+1 mm, flat top |
Two of those numbers deserve an asterisk:
- Capacity 3300–3400 mAh. Genuine NCR18650B is a flat 3400 mAh part. A binned range strongly implies these are B-grade cells — passed Panasonic’s tests but not at the top of the curve. Fine for a UPS, less fine for an EV.
- 1000 cycles. Panasonic’s own datasheet for the NCR18650B is ~500 cycles to 70% of initial capacity. The 1000 figure is almost certainly measured to a more lenient end-of-life threshold (e.g. 60% remaining), or is just vendor optimism. Treat it as marketing.
What you actually get
Three matched-ish cells in a blister pack. Installed straight into the UPS, the pack came up at ~11.2 V — a hair under storage charge. That matters because the Waveshare 3S has no balancing on discharge; if one cell is way out of line you’ll hit the low-voltage cutoff long before the pack is actually empty.
Physically they sit flush in the spring holders. No wrap damage, no dents. The flat-top form factor is correct for this board — button-tops don’t seat reliably in the 3S’s contact plates. Weight came in at 47 g per cell on a kitchen scale, matching the spec.
Three wires, not eight
The Waveshare UPS 3S exposes an 8-pin (2×4) header on its top side — four signal types, each duplicated across the two columns: SCL / SDA, 3V3 / 3V3, GND / GND, 5V / 5V. Only three of those wires go to the Pi 5:
| UPS pin | Pi 5 GPIO header |
|---|---|
| SCL | pin 5 (GPIO 3 / SCL1) |
| SDA | pin 3 (GPIO 2 / SDA1) |
| GND | pin 6 |
| 3V3 / 5V | not connected |

The 3V3 and 5V pins stay deliberately disconnected because the Pi 5 is already powered via USB-C from the UPS’s output port — wiring either rail back to the Pi’s GPIO power pins would create two power sources feeding the same bus and risk back-feeding into the PMIC. The I²C bus just needs SCL, SDA, and a common ground to talk to the on-board INA219, and that’s it.
On the Pi side I also had to set usb_max_current_enable=1 in /boot/firmware/config.txt (so the Pi 5 trusts the UPS’s non-PD output for the full 5 A budget) and force the i2c-dev module to load — dtparam=i2c_arm=on alone enables the controller driver but doesn’t expose /dev/i2c-1 to userspace. With both flags set, i2cdetect -y 1 finds the INA219 at 0x41.
Auto power-off when the pack runs out
A UPS without a clean-shutdown hook is only half a UPS — eventually the boost regulator hits its undervoltage lockout and the Pi takes a hard power-cut, with all the SD-card-corruption risk that implies. The fix is a small daemon that polls the INA219, counts consecutive low-voltage reads, and calls shutdown -h now while there’s still enough headroom in the pack to do it gracefully.
I poll every 10 seconds and trigger shutdown after three consecutive reads below 9.5 V (≈3.17 V/cell, well above Panasonic’s 2.5 V per-cell discharge cutoff). Three reads is just debouncing — a single transient dip from a CPU spike or an SSH login shouldn’t trip the shutdown.
The whole loop fits in /usr/local/bin/ups-shutdown.py:
import smbus, time, subprocess
ADDR, BUS = 0x41, 1
THRESHOLD_V = 9.5
CONSECUTIVE = 3
POLL_INTERVAL_S = 10
def read_voltage(bus):
hi, lo = bus.read_i2c_block_data(ADDR, 0x02, 2)
return ((hi << 8 | lo) >> 3) * 0.004 # INA219 bus-voltage register, 4 mV LSB
bus = smbus.SMBus(BUS)
bus.write_i2c_block_data(ADDR, 0x00, [0x39, 0x9F]) # config: 32V range, 12-bit
bus.write_i2c_block_data(ADDR, 0x05, [0x10, 0x00]) # calibration
low = 0
while True:
v = read_voltage(bus)
if v < THRESHOLD_V:
low += 1
if low >= CONSECUTIVE:
subprocess.run(['/sbin/shutdown', '-h', 'now', 'UPS battery low'])
break
else:
low = 0
time.sleep(POLL_INTERVAL_S)
Wrap it in /etc/systemd/system/ups-shutdown.service:
[Unit]
Description=UPS battery monitor (Waveshare UPS 3S, INA219 @ 0x41)
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/bin/ups-shutdown.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now ups-shutdown and you’re done. Tail the journal with journalctl -fu ups-shutdown to watch low-voltage warnings count up before a real shutdown trigger. To dry-run without waiting for an actual flat pack, temporarily raise THRESHOLD_V above the current pack voltage and restart the service — you’ll see the three-read count and the shutdown fire immediately.
Cascading to USB-powered dependents
One subtlety I hit on the first real low-voltage event: I have a Pi Zero 2W velcroed to the Pi 5, powered from the Pi 5’s USB-A port. When the Pi 5 halts, that USB rail drops — and the Pi Zero takes the same hard power-cut this daemon was meant to prevent, with the same SD-card-corruption risk.
The fix is to extend the shutdown trigger so it SSHes into each USB-powered child and poweroffs it gracefully before the local halt. Each dependent gets a short grace window (I use 30 seconds) of ping polling before the daemon gives up and shuts down anyway — battery time is finite, so the local halt always wins eventually:
DEPENDENTS = [('zero2wAms', '[email protected]', '192.168.1.226')]
DEPENDENT_GRACE_S = 30
def shutdown_dependents():
for label, ssh_target, ping_ip in DEPENDENTS:
subprocess.run(
['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5',
ssh_target, 'sudo', '-n', '/sbin/poweroff'],
timeout=10, check=False,
)
deadline = time.time() + DEPENDENT_GRACE_S
while time.time() < deadline:
r = subprocess.run(['ping', '-c', '1', '-W', '1', ping_ip],
stdout=subprocess.DEVNULL)
if r.returncode != 0:
break # dependent is down
time.sleep(2)
…called from the main loop right before the local shutdown -h now.
Two prerequisites tripped me up the first time: the daemon runs as root, so the SSH key has to live at /root/.ssh/id_ed25519 (not your user’s keyring) and the matching pubkey has to be in the dependent’s authorized_keys. And the dependent needs a sudoers NOPASSWD rule for /sbin/poweroff, otherwise sudo prompts and the SSH session times out without doing anything.
In the UPS — two cycles in

Wired up, the pack started at ~11.2 V at rest (≈60% state of charge), climbing to a full 12.52 V after the first bench charge. The UPS daemon logs voltage and current from the on-board INA219 to a CSV every minute, which gives a clean discharge curve to look at.
I’ve now done two full discharges, with slightly different loads on the Pi 5:
- Cycle 1 — Pi 5 + a Pi Zero 2W (USB-powered off the Pi 5) → 6.6 hours from 12.48 V to 9.46 V.
- Cycle 2 — Pi 5 alone, Pi Zero detached → 8.0 hours from 12.42 V to 9.50 V.

Some things the chart makes obvious that the numbers don’t:
- Discharge is nearly linear, not a Li-ion-textbook S-curve. That’s the 3S boost-regulator’s doing — it pulls more amps from the pack as voltage sags, so the apparent voltage drop looks roughly proportional to time. Useful in practice: pack voltage is a half-decent SoC indicator without integrating current.
- The two curves track closely for the first hour, then fan out as the higher-current cycle 1 pulls voltage down faster.
- The offset between curves is consistent. Adding the Pi Zero costs about 1.4 hours of runtime, in the right ballpark for the Pi Zero 2W’s roughly 1 W contribution.
- No knee at the bottom. Once a cheap rewrap is on its last legs you’d expect a sharp voltage collapse below ~10 V; both cycles glide smoothly to the daemon cutoff. Good sign at cycle two.
The math behind those runtimes: the Pi 5 draws ~6 W AC at the Tapo socket it plugs into. Three cells in 3S series give you 3.4 Ah at 11.1 V nominal — capacities don’t add in series, voltage does, and Ah stays at the single-cell value — so the pack is ~37 Wh total. With the 9.5 V cutoff capturing ~85 % of that and another ~15 % lost to the UPS boost regulator and the USB-C adapter, ~26–28 Wh ends up at the Pi. At 6 W AC that’s a budget of ~5–6 h, which lines up nicely with the 6.6–8 h actually observed.
A secondary oddity: the INA219 current trace consistently reads in the tens of milliamps even during heavy discharge. Integrated over a full cycle it works out to ~8 Wh, vs the ~32 Wh of pack energy that actually moves between 12.5 V and the 9.5 V cutoff — off by ~4×. I suspect a wrong shunt-resistance value baked into the read script’s calibration register write — a follow-up to fix. The voltage trace is unaffected; the daemon’s shutdown decision only cares about voltage.
Two cycles is a long way from a verdict on lifetime, but it’s enough to say nothing alarming has shown up: no DOA cell hiding in the pack, no obvious capacity mismatch between the three, and the curves are repeatable.
What I still can’t speak to
- Cycle life. Whether these actually deliver anywhere near the 1000 cycles printed on the sheet — or even Panasonic’s own 500 — is unknowable at cycle two. Ask me at cycle 50.
- Self-discharge. Will check in a month by leaving a charged pack idle.
- Hot-weather behaviour. Amsterdam in May is benign; July in a poorly-ventilated cupboard will be the real test.
- Internal resistance under load. Vendor claims <20 mΩ; I don’t have a 4-wire rig to verify.
Verdict so far
For a non-critical home-lab UPS where the consequence of a bad cell is “the daemon shuts the Pi down 6 hours earlier than expected,” these are a reasonable bet — and after two clean cycles I’m leaning more positive than I was at unboxing. At ~$2/cell after AliExpress coupons, the economics also make a lot of sense if you’re stocking spares.
I would still not trust them for safety-critical work, EV applications, or anything where a cell failure could ruin your day — for that, pay the premium for genuine cells from a vendor that prints batch codes. But for keeping a Raspberry Pi alive through the occasional 2-minute brownout, three of these and a Waveshare 3S is hard to argue with at the price.
Will revisit at 10 cycles and again at 50.
