Set up nut-upsd and peanut in your homelab

I run my Synology server and network switches off a UPS. I decided I wanted a dashboard for the UPS, here’s how I did set up Network UPS Tools, aka NUT, and the PeaNUT dashboard.

Table of Contents

Prerequisites

  • A UPS compatible with Network UPS Tools, aka NUT.
  • A linux box with a free UPS port. I’m using my Orange Pi 5, but this would work on a Raspberry Pi or any other linux server.
  • A USB-A to USB-B cable to connect the UPS to the linux box

Setup

Connect the UPS to your server

Connect the UPS to your linux server. Check which USB device it’s showing up as by running lsusb.

On my machine, it appears on bus 2. Note how it shows up on the bus, we’ll need it to determine which nut driver to use.

$ lsusb
Bus 006 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 008 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 007 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 002: ID 0764:0501 Cyber Power System, Inc. CP1500 AVR UPS
Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
$

Configure NUT

I looked up the CP1500 on the NUTS Hardware Compatibility List, and that says to use the usbhid-ups driver.

Confirm the driver you need is present

docker run --rm --entrypoint /bin/ls instantlinux/nut-upsd /usr/lib/nut | grep YOURDRIVER

Create a docker-compose.yaml for nut-upsd

Now we can create a docker-compose.yaml file for nut.

version: '3.9'
services:
  nut-upsd:
    image: instantlinux/nut-upsd
    container_name: nut
    environment:
      - TZ=${TZ:-America/Denver}
      - API_PASSWORD=${API_PASSWORD:-'YourPasswordForAPIaccess'}
      # Look up your UPS here https://networkupstools.org/stable-hcl.html to
      # determine which driver to use.
      - DRIVER=usbhid-ups
    devices:
      # Device numbers are subject to change, so map in the whole bus. NUT
      # will automatically find the UPS for you.
      - /dev/bus/usb:/dev/bus/usb
    ports:
      - "3493:3493"
    restart: unless-stopped

Start the container with docker-compose up -d

Confirm UPS was detected by NUT

Confirm that nut detected your UPS. telnet YOURHOST 3493, then enter LIST UPS

It should look similar to

$ telnet localhost 3493
Trying ::1...
Connected to localhost.
Escape character is '^]'.
list ups
BEGIN LIST UPS
UPS ups "UPS"
END LIST UPS

Now confirm that it is reporting reasonable looking metrics by entering list var ups.

Here’s what NUT reported about my UPS:

list var ups
BEGIN LIST VAR ups
VAR ups battery.charge "100"
VAR ups battery.charge.low "10"
VAR ups battery.charge.warning "20"
VAR ups battery.mfr.date "CPS"
VAR ups battery.runtime "1226"
VAR ups battery.runtime.low "300"
VAR ups battery.type "PbAcid"
VAR ups battery.voltage "13.3"
VAR ups battery.voltage.nominal "12"
VAR ups device.mfr "CPS"
VAR ups device.model "CP800AVRa"
VAR ups device.serial "CXPHX2000916"
VAR ups device.type "ups"
VAR ups driver.debug "0"
VAR ups driver.flag.allow_killpower "0"
VAR ups driver.name "usbhid-ups"
VAR ups driver.parameter.pollfreq "30"
VAR ups driver.parameter.pollinterval "2"
VAR ups driver.parameter.port "auto"
VAR ups driver.parameter.synchronous "auto"
VAR ups driver.state "quiet"
VAR ups driver.version "2.8.1"
VAR ups driver.version.data "CyberPower HID 0.8"
VAR ups driver.version.internal "0.52"
VAR ups driver.version.usb "libusb-1.0.26 (API: 0x1000109)"
VAR ups input.voltage "117.0"
VAR ups input.voltage.nominal "120"
VAR ups output.voltage "117.0"
VAR ups ups.beeper.status "enabled"
VAR ups ups.delay.shutdown "20"
VAR ups ups.delay.start "30"
VAR ups ups.load "38"
VAR ups ups.mfr "CPS"
VAR ups ups.model "CP800AVRa"
VAR ups ups.productid "0501"
VAR ups ups.realpower.nominal "450"
VAR ups ups.serial "CXPHX2000916"
VAR ups ups.status "OL"
VAR ups ups.test.result "No test initiated"
VAR ups ups.timer.shutdown "-60"
VAR ups ups.timer.start "-60"
VAR ups ups.vendorid "0764"
END LIST VAR ups

Set up Peanut webui

It’d be nice to have a pretty web dashboard for the UPS. Enter peanut.

Now to add the peanut service to our docker-compose.yaml. Because it’s in the same docker-compose.yaml file (and therefore by default, the same docker network), we can use the nut container name as the NUT_HOST. We’re also going to use depends_on to make it start after the nut container is up and running.

For simplicity, I’m not including how to make PeaNUT accessible via https. If you do want to secure PeaNUT with SSL, I’ve documented how to use nginx-proxy-manager as an SSL reverse proxy here.

Add a PeaNUT stanza to docker-compose.yaml.

# Monitor our UPS
version: '3.9'
services:
  nut-upsd:
    image: instantlinux/nut-upsd
    container_name: nut
    environment:
      - TZ=${TZ:-America/Denver}
      - API_PASSWORD=${API_PASSWORD:-'aPasswordForAPIaccess'}
      # Driver found with this tool https://networkupstools.org/stable-hcl.html
      - DRIVER=usbhid-ups
    devices:
      # Device numbers are subject to change, so map in the bus
      - /dev/bus/usb:/dev/bus/usb
    ports:
      - "3493:3493"
    restart: unless-stopped

  peanut:
    image: brandawg93/peanut:latest
    container_name: peanut
    restart: unless-stopped
    # Don't start peanut until after nut-upsd is running
    depends_on:
      - nut-upsd
    ports:
      - 8080:8080
    environment:
      - NUT_HOST=nut
      - NUT_PORT=3493
      - WEB_PORT=8080

Do a docker-compose down && docker-compose up -d to force docker-compose to recognize the new service, and now you can open https://yourserver:8080 and see a nice dashboard.

Here’s the dashboard for the UPS that drives my network stack & Synology:

peanut-dashboard

Looks like it’s time to get a second UPS and split the load - I’d like to have at least 45 minutes of network & Home Asssitant run time if the power goes out.

Edit: Add link to nginx-proxy-manager post documenting how to put http services like PeaNUT behind an SSL reverse proxy.