UPS
This guide covers enabling UPS (uninterruptible power supply) support on a braid NAS via NUT (Network UPS Tools).
Enabling UPS support (braid.enable = true plus braid.ups.enable = true)
turns on three behaviors:
- Orderly poweroff in a long outage. After
shutdownAfteron battery, or once the charge drops belowshutdownBelowChargeon battery, braid shuts the host down through NUT. The UPS’s own low-battery signal still shuts it down if it comes first. systemd unwindsbraid-online.service’s ExecStop, which runsbraid lockand cleanly unmounts the pool while the battery still has charge to spare. See Shutdown policy. - Preflight refusal without verified utility power.
braid add/remove/remove-missing/replacecheck UPS state at startup and refuse to begin a pool mutation unless the UPS reports verified utility power (OL). This narrows the surface that journal recovery needs to cover. - Live state visibility.
braid ups status(and the TUI Data tab) show the parsedupscoutput: status flags, battery charge, runtime remaining, load, estimated watts, input voltage, and device info.
Scope
v1 supports a single USB-connected UPS on the NAS, monitored by the NAS itself (single-host standalone). Non-USB drivers work through the escape hatch but are not tested.
Minimal config
# configuration.nix
{
braid = {
enable = true;
ups.enable = true;
};
}
Defaults: name = "ups", driver = "usbhid-ups", port = "auto",
shutdownAfter = 300, shutdownBelowCharge = 50. Rebuild and plug the UPS’s
USB cable in; NUT’s auto-detect finds the device.
Override the driver or port for non-USB UPSes:
braid = {
enable = true;
ups = {
enable = true;
name = "myups";
driver = "apcsmart";
port = "/dev/ttyS0";
};
};
Checking status
# Curated human summary
sudo braid ups status
# Machine-readable JSON (stable shape for scripts)
sudo braid ups status --json | jq .
Example human output:
UPS: ups
Status: OL [ok] on utility power
Battery: 100%
Runtime: 30m 0s
Load: 17% (56 W estimated)
Input: 120.0 V (transfer 88-142 V)
Device: APC Back-UPS ES 550G
Battery manufactured: 2023/04/12
Last test: Done and passed
The human Status: line preserves the raw NUT tokens and adds the same
severity model used by the TUI UPS panel: [ok], [warn], [fail], or
[skip]. Tags are colored on an interactive TTY and plain under NO_COLOR or
when output is piped.
The watts line is labeled estimated and omitted entirely if the UPS
does not report both ups.load and ups.realpower.nominal.
The --json output serializes the full parsed model plus braid’s
severity verdict, from the same classifier the
mutation preflight
uses. Only "severity": "online" proves utility power; the
ups status page lists every
value. Distinct error sentinels are emitted for the common non-OK cases:
| Condition | JSON shape | Exit code |
|---|---|---|
| UPS reachable | serialized UpscOutput plus severity | 0 |
| UPS query failed | {"error": "query_failed", "detail": "exit <code>: <stderr>"}. If stderr is blank, detail is just exit <code>. | 1 |
| UPS invocation failed (upsc could not run – missing on PATH, killed by signal, or other runner-level failure) | {"error": "invocation_failed", "detail": "command failed: upsc ups: <reason>"}. When upsc is missing, detail ends with -- is braid.packages.nut on PATH?. Other failures, such as a signal kill, get no hint. | 1 |
| UPS not enabled | {"error": "ups_not_enabled"} | 0 |
TUI UPS panel
braid tui’s Data tab gains a UPS row when UPS support is enabled.
Status text is color-coded by the shared UPS severity model:
- Green – OL (on utility power).
- Yellow – OB (on battery, not yet critical).
- Red – LB / TESTFAIL / COMMBAD / FSD (critical; shutdown imminent or comms-loss).
- DarkGray – UPS query failed, or no UPS state available yet.
The Shutdown column shows the shutdown policy as
braid-ups-guard sees it. On utility power it shows the armed policy
dimmed, such as after 5m 0s or <50%. During an outage it counts down
(in 3m 12s or <50%), turning red in the last minute. It shows
guard down in red when a trigger is set but the guard is not running,
overdue when the timer passed its deadline and no forced shutdown
followed, and off when both options are null. The countdown comes from
the guard’s own clock, so it matches what the guard will do.
During an outage a banner above every tab says how long the UPS has been on battery and when braid shuts down, or why it will not shut down early. It clears once power returns.
The panel polls on the same 5-second cadence as the
TUI fans panel. Press r to refresh both
pool and UPS probes immediately.
Shutdown policy
braid shuts the host down at the first of three moments:
- On battery for
shutdownAfterseconds (default 300). - On battery with
battery.chargebelowshutdownBelowChargepercent (default 50). - The UPS reports low battery (
OB LB) – the firmware’s own last-resort signal.
braid.ups = {
enable = true;
shutdownAfter = 300; # seconds on battery; null turns the timer off
shutdownBelowCharge = 50; # percent; null turns the floor off
};
The timer lets short outages ride through. The charge floor covers an aging battery or a grown load that drains faster than the timer assumes. Low battery is the backstop, and fires late: often with about two minutes of estimated runtime left.
Sizing shutdownAfter
Measure the UPS’s runtime at the NAS’s real load (braid ups status shows
Runtime while on utility power), then:
shutdownAfter <= runtime x 0.5 - clean-shutdown time - 60 s margin
Keeping about half the battery in reserve is kinder to it and leaves room
for an estimate that drifts as the battery ages. If the result is under
about two minutes, the UPS is too small for the load. Setting
battery.runtime.low or battery.charge.low in NUT does not shut down
earlier: NUT raises low battery from those values only with the driver’s
ignorelb flag, which braid does not use because it ignores the UPS’s own
low-battery signal.
How it works
braid-ups-guard.service polls the UPS every few seconds. The clock starts
on the first poll that shows OB and stops only when the UPS reports
utility power (OL); a failed query or an unrecognized status leaves it
running. A guard restart, such as a nixos-rebuild switch, keeps the
outage’s original deadline, and time spent suspended counts.
When a trigger holds, the guard logs one line naming the reason and runs
upsmon -c fsd. From there the shutdown is the same as on low battery:
upsmonsets forced shutdown (FSD) on the UPS, writes/run/killpower, and runssystemctl poweroff.- systemd stops
braid-online.service, whose ExecStop runsbraid lock, which unmounts the pool and closes LUKS. - Late in shutdown,
ups-killpower.servicetells the UPS to cut its output and restore it when mains returns, so a host set to power on after AC loss boots by itself.
# Why did the NAS shut down? (previous boot)
journalctl -b -1 -u braid-ups-guard
If mains returns while the host is shutting down, a UPS that ignores
shutdown.return once on utility power may leave the host off until you
power it on.
Turning it off
Set both options to null to shut down only on low battery, as before;
the guard is then not installed. Under the default upsmon timings
(POLLFREQ = POLLFREQALERT = 5, FINALDELAY = 5) the window between low
battery and poweroff is about 10 seconds plus however long braid lock
takes.
Mutation refusal when utility power is not verified
With UPS enabled, braid add / remove / remove-missing / replace
refuse to start unless upsc returns a non-empty status set that
contains OL and no known blocker. The refusal cases are:
- on-battery (
OB) - a critical flag the TUI paints red (
LB/TESTFAIL/COMMBAD/FSD) OLmissing from an otherwise non-blocking status setupscquery or invocation failure (stopped daemon, unknown UPS name, or another fatal NUT error – the message includesupsc’s stderr when present and omits that clause when stderr is blank)- an empty or missing
ups.status
Known non-critical advisory states such as OL RB, and unknown tokens
co-present with OL and no known blocker, still pass: the OL flag is
the affirmative utility-power proof, not a guarantee of full battery
health. Example refusal:
$ sudo braid add newdisk=/dev/disk/by-id/ata-TOSHIBA_NEW
error: cannot verify UPS is on utility power (UPS reports on-battery)
-- refusing to start add. Check 'braid ups status', restore utility
power, then retry.
Recovery: run braid ups status to confirm, fix the UPS/NUT state,
restore utility power, wait for the status to return to a trusted OL,
and retry the command.
Two clarifications:
braid doctor’sups_daemon: okmeans the configured NUT daemon is reachable; it is not a guarantee that mutating-command preflight will pass. The refusal error fromadd/remove/remove-missing/replaceis the primary channel for the exact mutation-readiness blocker.- The
OLgate assumes the configured NUT driver reportsOLon utility power as documented by NUT. If a device or driver violates that contract, inspect withbraid ups status; the recovery is to fix the NUT driver/config or disablebraid.upsuntil the UPS state can be trusted.
doctor checks
braid doctor adds these UPS-adjacent checks when UPS support is enabled:
- ups daemon – fails if
upscis missing or cannot be spawned, because braid cannot verify the enabled UPS shutdown path. It warns ifupscruns but cannot reach the daemon or exits non-zero. Fix missingupscby checking the braid wrapper/NUT package path; fix daemon reachability withsystemctl status upsd.service. - braid-online – fails (high severity) if the pool is mounted but
braid-online.serviceis not active. Without that service active, the UPS shutdown path does not unmount the pool, and the safety guarantee silently breaks. Fix by runningsystemctl start braid-online.serviceorbraid unlock. - ups guard – fails if
braid-ups-guard.serviceis not active whileshutdownAfterorshutdownBelowChargeis set. Without the guard, early shutdown is off and only the UPS’s own low-battery signal shuts the host down. Fix withjournalctl -u braid-ups-guardandsystemctl start braid-ups-guard.service. Skipped when both options are null. - ups floor – warns if
shutdownBelowChargeis set but the UPS does not reportbattery.charge, so the charge floor can never fire and only the timer and low battery remain. Checkbraid ups status; if the UPS cannot report charge, setshutdownBelowCharge = nulland rely onshutdownAfter. Skipped when the floor is null or the UPS query fails (the ups daemon check reports that).
Both checks skip when UPS support is disabled, and the braid-online check additionally skips when the pool is not mounted (there is nothing for the UPS shutdown path to unmount).
v1 limitation: no async alert
There is no asynchronous notification when the UPS goes on battery or
loses comms in v1. Operators who are not actively watching braid ups status or the TUI will not see those conditions – only the orderly
shutdown (see Shutdown policy) is automatic.
This is deliberate: integrating UPS events into braid’s shared alert
model requires splitting AlertCause by persistence semantics
(latched-until-ack for disk errors, active-while-condition-holds for
live UPS states) and is out of scope for v1. See
decisions/020-ups-integration.md
for the open-question status.
If you need asynchronous UPS notifications today, wire NUT’s
NOTIFYCMD directly – braid does not touch it.
Related
- ADR: UPS integration – scope, shutdown path, preflight contract.
upscman page – the raw command braid parses.power.upsNixOS options – the underlying nixpkgs module braid layers opinionated defaults on.