Search terms: UPS · CyberPower · CP1350 · NUT · Network UPS Tools · upsc · upsd · battery runtime · on battery · power failure · driver not connected · usbhid-ups
Installed 2026-08-13. The CyberPower CP1350PFCLCDa (1350 VA / 880 W) is connected by USB to media-server, which reads it with NUT. Every command below runs on media-server — nothing else on the LAN talks to the UPS directly.
| Status | |
|---|---|
| Reading the UPS (battery, load, voltage) | working — verified 2026-08-13 |
Logging power events to /var/log/ups-events.log |
was BROKEN — fixed 2026-08-18 |
Discord alerts to #power-failure |
was BROKEN — fixed 2026-08-18 |
| Automatic clean shutdown on low battery | configured, NOT YET TESTED |
The 2026-08-18 correction — alerts never worked in production
This page previously claimed Discord alerts were “working — test messages delivered 19:19.” They were not. A real mains pull on 2026-08-18 06:13 produced no Discord alert, and the same had been true for every real event since at least 2026-08-14.
Root cause:
upsmondoes NOT run NOTIFYCMD as root.root 1985 upsmon <- privileged parent, used ONLY for the shutdown nut 1988 1985 upsmon <- drops privileges; THIS is what runs NOTIFYCMDAs user
nut, the script could neither write/var/log/ups-events.log(644 root:root) nor read~/.config/discord-webhooks/power.txt(600 aztechguy:aztechguy). It exited at the webhook readability test before ever attempting the POST.The 19:19 test passed because it was run by hand with
sudo— as root. It exercised the script in a security context production never uses. The page even carried the caveat “this tests the notification path, not the shutdown” — correct, but one level too shallow. It tested the notification script, not the notification path.The old claim on this page — "
upsmonruns as root … which is how it can read a mode-600 webhook file" — was the false assumption, copied from the script’s own header comment. It propagated from code comment to documentation to four days of silent failure.
The shutdown path still has never been exercised. The config is in place and the services
are running, but no real power interruption has driven the battery to the low-battery threshold
to prove upsmon triggers a clean shutdown. Until that happens, that row is a claim rather than
a fact — see Testing it for real below.
upsc cyberpower
Prints everything the UPS knows. The lines that matter:
upsc cyberpower | grep -E 'battery.charge|battery.runtime|ups.load|input.voltage|ups.status'
battery.charge: 82 percent
battery.runtime: 8937 SECONDS remaining -- divide by 60
ups.load: 0 percent of 880 W
input.voltage: 120.0 mains voltage as the UPS sees it
ups.status: OL CHRG see the table below
Single values, useful in scripts:
upsc cyberpower battery.charge
upsc cyberpower ups.status
echo "$(( $(upsc cyberpower battery.runtime) / 60 )) minutes left"
ups.status| Flag | Meaning |
|---|---|
OL |
On Line — running on mains. Normal. |
OB |
On Battery — mains has failed |
LB |
Low Battery — shutdown threshold reached |
CHRG |
battery is charging |
DISCHRG |
battery is discharging |
RB |
Replace Battery — the one that matters at 3-5 years |
OVER |
overloaded |
TRIM / BOOST |
actively correcting high / low mains voltage |
OL CHRG is normal after a power cut or a fresh install. Steady state is plain OL.
RBis the sleeper. UPS batteries die silently at 3-5 years, and without a check the first sign is a unit that drops instantly during a real outage. Worth watching for.
watch -n 2 'upsc cyberpower 2>/dev/null | grep -E "battery.charge|battery.runtime|ups.load|ups.status"'
From the laptop, without logging in:
ssh media-server 'upsc cyberpower | grep -E "charge|runtime|load|status"'
battery.runtime at idle is meaningless for planning. With nothing on the battery outlets
it read 8937 seconds (149 minutes).
Measured 2026-08-13 with the load connected: ups.load: 9 (~79 W), battery.runtime 70
minutes. That is more headroom than expected — and the low figure is itself worth a second
look, because the media server alone should draw 80-110 W with four spinning drives. A load
that reads lower than the hardware can explain usually means something is plugged into the
SURGE-ONLY outlets rather than the battery side. Confirm by watching ups.load while
unplugging one device at a time.
Load maths for this unit:
ups.load is a PERCENTAGE of 880 W
ups.load: 20 -> ~176 W
ups.load: 25 -> ~220 W
Total draw across all outlets must stay under 12 A (from the label, surge outlets included). At ~200 W that is about 1.7 A — roughly 14% of capacity.
Three pieces, all on media-server:
/etc/nut/upsd.users upsmon's credentials for upsd (root:nut 640)
/etc/nut/upsmon.conf MONITOR line, shutdown command, NOTIFYFLAGs
/usr/local/bin/ups-notify.sh posts to Discord #power-failure, logs locally
/etc/nut/power-webhook.txt the webhook, readable by nut (root:nut 640)
/var/log/ups-events.log local event log, writable by nut (root:nut 664)
upsmon does NOT run NOTIFYCMD as root. It forks: a privileged parent that exists only to
execute the shutdown, and an unprivileged child — running as user nut — that does the
monitoring and runs your notification script.
ps -eo user,pid,ppid,comm | grep upsmon
root 1985 1 upsmon # privileged parent, shutdown only
nut 1988 1985 upsmon # THIS runs NOTIFYCMD
So every file the notify script touches must be reachable by nut:
| File | Required | Why |
|---|---|---|
/var/log/ups-events.log |
664 root:nut |
nut must write it |
| the webhook | 640 root:nut, in /etc/nut/ |
nut must read it |
A webhook at ~/.config/discord-webhooks/power.txt (mode 600, owned by aztechguy) is
unreadable by nut and the script will exit before it ever reaches the POST. That is
exactly what happened between 2026-08-14 and 2026-08-18 — see the correction box at the top.
Webhook rotation now touches two files. Rotating #power-failure means updating
~/.config/discord-webhooks/power.txt and re-copying it:
sudo install -o root -g nut -m 640 \
~/.config/discord-webhooks/power.txt /etc/nut/power-webhook.txt
Miss the second and alerting dies quietly again.
logrotate will undo this. If a rotate config is ever added for ups-events.log, it must
carry create 664 root nut — the default recreates the file root:root and re-breaks alerting
with no visible symptom.
sudo NOTIFYTYPE=ONBATT /usr/local/bin/ups-notify.sh "TEST -- simulated, no actual outage"
sudo NOTIFYTYPE=ONLINE /usr/local/bin/ups-notify.sh "TEST -- simulated restore"
tail -4 /var/log/ups-events.log
⚠️ This test runs as ROOT. Production runs as
nut.That is the whole trap, and it cost four days of silent alerting. Invoking the script with
sudogives it privileges upsmon never grants it. It will read a mode-600 webhook and write a root-owned log happily — and then fail on both when a real event arrives.A green result here says the script’s logic is sound. It says nothing about whether the script can run at all in production.
To test as
nutactually does:sudo -u nut NOTIFYTYPE=ONBATT /usr/local/bin/ups-notify.sh "TEST -- as the nut user" tail -3 /var/log/ups-events.logIf that lands in
#power-failurewithposted OK, the permissions are genuinely correct. If it doesn’t, the script is broken in production no matter what thesudoversion did.
Each should land in #power-failure with live battery, runtime, load and voltage, and the
local log should record posted OK.
Neither version tests the shutdown. They prove Discord works. They prove nothing about whether
upsmonreacts correctly to a real power cut.
The only fully honest test is pulling the plug, which drives the real state machine and calls the script the way production will. Done 2026-08-18 — see below.
| Event | Meaning |
|---|---|
ONBATT |
mains lost, running on battery |
ONLINE |
mains restored |
LOWBATT |
low-battery threshold — shutdown imminent |
SHUTDOWN / FSD |
shutting down now |
REPLBATT |
battery needs replacing — the 3-5 year sleeper |
COMMBAD / COMMOK / NOCOMM |
lost / regained / never had contact with the UPS |
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
EXECis what runs the script. Omit it from a line and that event is logged to syslog and silently never alerts. It is the easy thing to get wrong, and it fails quietly — which is the worst combination.
/var/log/ups-events.log is written before the Discord attempt and regardless of whether
it succeeds. During a power cut the internet is often the first thing to go, and an alert that
only exists if the network survives is not a record of what happened.
Only meaningful with the battery at 100% (ups.status: OL, no CHRG).
ssh media-server 'watch -n 2 "upsc cyberpower | grep -E \"charge|runtime|status\""'
ups.status: OB DISCHRG, an ONBATT message in
#power-failure, and a wall broadcast.OL and an ONLINE alert.To test the shutdown as well, leave it on battery until the low-battery threshold. At ~9% load that is over an hour, so drive it artificially only if you have a reason to.
A UPS configuration that has never been tested is a guess. Do it while standing beside the machine, not during a real outage.
Mains pulled 06:13, restored 06:54. 40 minutes on battery, load steady at 9–10% (~79 W).
06:13 100% ONBATT fires
06:16 78% surface charge gone within 3 minutes of load
06:24 75% 0.38 %/min
06:32 70% 0.63 %/min
06:40 60% 1.25 %/min <- rate more than TRIPLED
06:48 51% 1.13 %/min
06:52 47% 1.00 %/min
06:54 46% ONLINE, mains restored
Do not extrapolate runtime linearly from
battery.charge. The first ten minutes are the flattest part of the curve. Reading 0.4 %/min early and projecting forward predicted two hours to cutoff; the UPS’s ownbattery.runtimesaid ~51 minutes and was very nearly right.battery.runtimeis the honest number — trust it over your own arithmetic.
Battery verdict: healthy. Projecting the observed rate, 47% → the 10% cutoff lands near 07:29 — roughly 76 minutes total against a predicted 62 minutes at 100%. It beat its own estimate by ~20%, with no sag and no cliff. The estimator errs conservative, which is the direction you want.
Thresholds this unit reports (upsc cyberpower):
battery.charge.low 10 LB asserted here
battery.charge.warning 20
battery.runtime.low 300 seconds (5 min)
Stop a deliberate test at 40% or 30 minutes, whichever comes first — well clear of the warning at 20%, since the clean-shutdown path is still unproven and a full drain would take the media server and the NAS down hard.
ONLINE … input=0.0V transient[ONLINE] UPS cyberpower@localhost on line power | charge=46% runtime=32 min load=10% input=0.0V status=OL DISCHRG
Not a bug. The script samples the UPS at the instant upsmon fires the event, which is
mid-transition — the UPS had already declared OL before its input-voltage reading and load
transfer caught up. Expect one contradictory-looking line per recovery.
systemctl status nut-server # upsd -- publishes UPS data
systemctl status nut-driver@cyberpower # the driver that talks to the USB device
sudo systemctl restart nut-driver@cyberpower
sudo systemctl restart nut-server
Two separate services. nut-server can be running happily while the driver is dead —
which produces Error: Driver not connected from upsc, and is the first thing to check.
Error: Driver not connectedThe driver could not open the USB device. Almost always permissions, and almost always because the UPS was plugged in before NUT was installed — udev applies device rules when a device appears, so rules that arrive later were never applied to a device already present.
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=usb --action=add
sudo systemctl restart nut-driver@cyberpower
sleep 4 && upsc cyberpower
Unplugging and replugging the USB cable does the same thing, since udev fires on the add event.
/dev/hidraw0 disappeared — is that bad?No, that is expected. usbhid-ups claims the USB device through libusb, which detaches the
kernel HID driver and removes /dev/hidraw0 and /dev/usb/hiddev0. Their absence means the
driver has the device, which is the opposite of a fault.
Confirm the UPS is still on the bus:
lsusb -d 0764: # CyberPower's vendor id
systemctl is-active nut-driver@cyberpower
lsusb reports PR1500LCDRT2U. That is a generic string baked into CyberPower’s HID
product id 0764:0601, shared across many models — it is not what you own. NUT queries the
device properly:
upsc cyberpower device.model # CP1350PFCLCDa -- the truth
upsc cyberpower device.serial # CX5RN7002465
Model CyberPower CP1350PFCLCDa
Capacity 1350 VA / 880 W · 12 A total output
Serial CX5RN7002465
Connection USB to media-server (front port)
NUT name cyberpower driver: usbhid-ups
On battery modem · wireless router · media-server · UGREEN NAS
There is no network management card on this model. The RJ45 jacks on the back are a surge pass-through, not an interface — no IP, no web console. They are also commonly rated to only 10/100 Mbps, so running a gigabit link through them would quietly cost you 90% of your throughput. Leave ethernet out of the UPS; surge protection on an indoor patch cable buys nothing.
Remote monitoring on this unit is NUT over the network, which is what the “Network” in Network UPS Tools means — status shared between computers, not a network port on the UPS.
Restore on AC Power Loss → Power On
If an outage outlasts the battery, the server shuts down cleanly, mains returns — and it stays dark until someone presses the button. The default on most boards is “Stay Off.” Checked 2026-08-13: not yet set.