A Raspberry Pi-based UPS monitoring bridge that reads power state from an Arduino over serial and broadcasts it to the network via Network UPS Tools (NUT).
Since I couldn't figure out the data cable for my UPS, I decided to use an Arduino and a light sensor to detect when on battery power, and feed that into NUT so the whole network can react to power events.
The Arduino reads an analog light sensor (aimed at the UPS battery indicator LED) every few seconds
and sends the value over serial. A Python script on the Pi reads these values, applies a rolling
average, and writes a NUT state file. NUT's dummy-ups driver reads that file and serves it to
upsd, which broadcasts power state to any NUT client on the network. If power isn't restored
within 10 minutes, upssched escalates the battery status to trigger a graceful shutdown cascade.
Arduino (analog light sensor → serial ADC values)
│
▼
upsmon-bridge.service
(runs upsmon.py via nut-bridge.sh)
│
▼
upsmon.py (windowed average → writes myups.seq)
│
▼
dummy-ups driver (reads myups.seq in dummy-loop mode)
│
▼
upsd (serves on port 3493)
│
▼
upssched (10-min battery timer → escalates OB to OB LB)
│
▼
nut-monitor (watches for OB LB → triggers shutdown)
│
▼
NUT secondary clients (other machines on the network)
When 3 consecutive readings average >= 10 (battery light on), upsmon.py writes OB (on battery)
to the NUT state file. When readings drop back to <= 2 (power restored), it writes OL (on line).
If power isn't restored within 10 minutes, upssched automatically escalates to OB LB (low
battery), triggering the shutdown cascade across all NUT clients.
- Raspberry Pi (any model with USB)
- Arduino Uno running
upsmon.processing, connected via USB (appears as/dev/ttyACM0) - Light sensor aimed at the UPS battery indicator LED
- Arduino sends raw integer ADC values at 9600 baud, one per line
| File | Purpose |
|---|---|
upsmon.processing |
Arduino sketch (upload via Arduino IDE to an Uno) |
upsmon.py |
Python bridge — reads serial, writes myups.seq |
nut-bridge.sh |
Shell wrapper that activates the venv and runs upsmon.py |
myups.seq |
NUT dummy-ups state file (auto-generated and git ignored, do not edit manually) |
requirements.txt |
Python dependencies (pyserial) |
upssched-cmd.sh |
Helper script to force a "low battery" signal into NUT for shutdowns |
| Variable | Default | Description |
|---|---|---|
UPSMON_TTY |
ttyACM0 |
Device name under /dev/ |
UPSMON_BAUD |
9600 |
Serial baud rate |
sudo apt install nut # meta package
sudo mkdir -p /opt/upsmon
sudo chmod 755 /opt/upsmon
sudo chown nut:nut /opt/upsmon
sudo cp -r . /opt/upsmon
cd /opt/upsmon
sudo su -
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
sudo chown -R nut:nut /opt/upsmon
groupmod -a -U nut dialout # add the nut user to be able to read serial data from the Arduino/etc/nut/nut.conf
MODE=netserver
/etc/nut/ups.conf
maxretry = 3
[myups]
driver = dummy-ups
port = /opt/upsmon/myups.seq
mode = dummy-loop
desc = "Homebrew serial UPS"/etc/nut/upsd.conf
LISTEN 0.0.0.0 3493
/etc/nut/upsd.users
[admin]
password = adminpass
actions = SET
instcmds = ALL
[monuser]
password = monuserpass
upsmon primary/etc/nut/upsmon.conf
MONITOR myups@localhost 1 monuser monuserpass primary
SHUTDOWNCMD "/sbin/shutdown -h +0"
HOSTSYNC 15
DEADTIME 15
FINALDELAY 180
NOTIFYFLAG LOWBATT SYSLOG+WALL
NOTIFYFLAG SHUTDOWN SYSLOG+WALL
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
NOTIFYFLAG ONLINE EXEC
NOTIFYCMD /usr/sbin/upssched
/etc/nut/upssched.conf
CMDSCRIPT /opt/upsmon/upssched-cmd.sh
PIPEFN /run/nut/upssched.pipe
LOCKFN /run/nut/upssched.lock
AT ONBATT * START-TIMER onbatt-shutdown 600
AT ONLINE * EXECUTE online
The upsmon.py script runs as its own service, independent of NUT:
sudo cp upsmon-bridge.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now upsmon-bridge.serviceThe nut-monitor systemd service needs a timeout override. Because upsmon.conf has FINALDELAY set to 180, nut-monitor waits 180 seconds before running SHUTDOWNCMD, but systemd's service timeout is shorter and the monitor gets killed. Fix by adding a long TimeoutStopSec to nut-monitor. Create a systemd drop-in:
sudo mkdir -p /etc/systemd/system/nut-monitor.service.d
sudo tee /etc/systemd/system/nut-monitor.service.d/timeout.conf << 'EOF'
[Service]
TimeoutStopSec=300
EOF
sudo systemctl daemon-reloadUpload upsmon.processing to the Arduino Uno via the Arduino IDE, put the light sensor directly next to the UPS light and cover everything with electrical tape to avoid light leakage. Yes, this is a hack, I know, but it works!
# View live logs from the bridge script
journalctl -fu upsmon-bridge.service
# View NUT monitor logs
journalctl -fu nut-monitor
# Check current UPS status
upsc myups@localhost ups.status
# Stop / start / restart the bridge
sudo systemctl stop upsmon-bridge.service
sudo systemctl start upsmon-bridge.service
sudo systemctl restart upsmon-bridge.service| Status | Meaning |
|---|---|
OL |
On line — mains power present |
OB |
On battery — power loss detected |
OB LB |
On battery, low battery — shutdown cascade triggered |
Simulate a power outage without pulling the plug using a test override file:
# Simulate on battery
echo "OB" | sudo tee /opt/upsmon/test-state
# Simulate low battery (triggers FSD and shutdown cascade)
echo "OB LB" | sudo tee /opt/upsmon/test-state
# Restore to mains
sudo rm /opt/upsmon/test-stateThe override file is checked on every write cycle (up to 30 seconds) and takes priority over real serial data.
If power is restored before shutdown completes, NUT will show FSD OL and it might not clear automatically. If needed, restart the NUT stack to recover:
sudo systemctl restart nut-driver@myups.service
sudo systemctl restart nut-server
sudo systemctl restart nut-monitorOn each machine to be shut down by this Pi:
- Install
nut-client - Set
MODE=netclientin/etc/nut/nut.conf - Configure
/etc/nut/upsmon.conf:
MONITOR myups@<pi-ip> 1 upsmon <password> secondary
SHUTDOWNCMD "/sbin/shutdown -h +0"
- Open port 3493 on the Pi:
sudo ufw allow 3493/tcp