My homelab isn’t much – it consists only a server and a NAS. I back-up my notes, movies, photos and documents on the NAS, and serve various services on the server such as my home internet, note app, media server, game servers, and this very site, and, well, among many others. I sync my photos to OneDrive just in case anything physical happens to the setup. However, I worry about corruption – especially one induced by a power loss.

I need a UPS.

I purchased an APC Easy UPS BV650I-MS, a 650VA (375W) line-interactive UPS. According to what is printed on the box, it should provide 80W output, mentioned as ‘notebook computer + network gateway’, for 25 minutes. It’s good enough for my purpose, except that it doesn’t have a USB output for status reporting. At least it is easy on my wallet – it only cost me RM200.

Power interruptions shouldn’t be an issue for me as they tend to be only for a minute or two at most at my place. I barely remember the last time I needed to reach for a candle. But, in any case, I’d like to play it safe.

The plan

To sense a power cut, I’ll need to sense whether there is power from the mains. Some suggested an AC relay and sensing an open switch to indicate a power cut. Well, I don’t have a relay lying around. On the other hand, I do have a few cheap USB power adapters I’ve accumulated over the years.

My idea is to have a device sensing power from the USB power adapter. If power is cut, there should be no power from the adapter. The server is then notified of the power outage through serial interface and powers off the NAS and itself accordingly.

I only wanted to use what I have on hand, such as a few NodeMCU ESP8266 boards, some breadboards, various resistors, and many other modules I have on hand from my embedded IoT course. Not sure why I decided to purchase a bulk of 1M ohm resistors back then; time to put them to good use.

My sketched the following.

Draft plan

So, the important stuff I need:

  1. NodeMCU ESP8266 board
  2. A USB power adapter
  3. USB-A to DC barrel jack
  4. DC barrel jack breakout module
  5. 3x1M ohm resistors for the voltage divider
  6. A breadboard and wires

Simple enough.

All wired up

Here is what the setup looks like. I wished I have a half-length breadboard, but this can do for now.

Hardware

The purpose of the voltage divider is to drop 5V to 3.3V, which is what the NodeMCU ESP8266 expects on its digital input pin. I’ve tried dividing the voltage with the 3x 1M ohm resistors arranged in series, and tapped at 2M ohm, but wiring that to the digital input pin on the board causes voltage to droop to 1.5V. It’s probably due to the pin resistance being lower than the voltage divider setup. Fortunately, setting up 1.5M ohm total resistance and tapping out at 1M ohm is fine.

If you noticed, I’ve set up a buzzer. I wanted the device to emit a tone when the power is out, just like how a UPS would. All that’s left for this is the logic.

‘1’s and ‘0’s

The code for the NodeMCU ESP8266 board is simple. All it does is report ‘1’s and ‘0’s over serial. When power is available, it prints ‘1’. Otherwise, ‘0’.

#define POWER_PIN D1
#define BUZZER_PIN D2
int powerReadout = 0;

void setup() {
  // put your setup code here, to run once:

  Serial.begin(9600);
  pinMode(POWER_PIN, INPUT);
  pinMode(BUZZER_PIN, INPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  
  delay(5000);
  powerReadout = digitalRead(POWER_PIN);
  Serial.println(powerReadout);

  if (!powerReadout) {
    tone(BUZZER_PIN, 4000, 500);
  }
}

Plugging it in

It’s Bash scripting time!!!

When the NodeMCU ESP8266 board is plugged into the server, the serial interface is exposed at /dev/ttyUSB0. However, say, if I have multiple USB-to-Serial boards, the order of ttyUSB* will not be predictable. Therefore, we’ll use /dev/serial/by-path/* instead which depends on which USB port it is plugged into.

I’ve come up with the script below. The script will monitor the output of the NodeMCU ESP8266 board. If a power outage happens, it waits for a certain number of seconds – configured here as 5 minutes – before powering off the NAS and the server. It also logs to stdout when the power outage happens and when power is restored.

#!/bin/bash

POWER_CUT_WAIT_SEC=300
POWER_MONITOR_PATH=/dev/serial/by-path/pci-0000:00:14.0-usb-0:5:1.0-port0

POWER_STATUS=1
TIMESTAMP=0

shutdown() {
    echo "Sending power off signal to NAS."
    ssh [email protected] -o ServerAliveInterval=1 -o ServerAliveCountMax=1 -o StrictHostKeyChecking=no -t "poweroff"
    echo "Powering off. Goodbye."
    poweroff
}

read_monitor() {
    read INPUT < $POWER_MONITOR_PATH || {
        echo "Error reading power status. Device is probably disconnected."
        while [ ! -e $POWER_MONITOR_PATH ]; do
            sleep 15
        done
        echo "Monitoring power status."
        read_monitor
    }
    # echo "POWER_STATUS=$POWER_STATUS INPUT=$INPUT"
}

echo "Monitoring power status."
while true; do
    read_monitor
    if [ "$POWER_STATUS" = "1" ] && [ "$INPUT" = "0" ]; then
        POWER_STATUS=$INPUT
        TIMESTAMP=$(date +%s)
        echo "Power cut detected on $(date -d @$TIMESTAMP). Waiting ${POWER_CUT_WAIT_SEC}s until shutdown."
        read_monitor
        while [ "$INPUT" = "0" ]; do
            if (( $(date +%s) - $TIMESTAMP >= $POWER_CUT_WAIT_SEC )); then
                echo "Power cut timeout exceeded. Executing shutdown sequence."
                shutdown
                break
            fi
            read_monitor
        done
    fi
    if [ "$POWER_STATUS" = "0" ] && [ "$INPUT" = "1" ]; then
        echo "Power restored on $(date). Power was cut for $(( $(date +%s) - $TIMESTAMP )) seconds."
    fi
    POWER_STATUS=$INPUT
done

All that’s left is to make this script into a systemd unit.

[Unit]
Description=Monitors power status from mains and shuts down systems accordingly.
After=multi-user.target

[Service]
Type=simple
Restart=always
User=root
ExecStart=/root/powercut-mon/powercut-mon.sh

[Install]
WantedBy=multi-user.target

Pulling the plug

The right way to test this setup is by pulling the power from the UPS, right?

Monitoring power status.
Power cut detected on Tue Jun 18 01:53:44 AM +08 2024. Waiting 300s until shutdown.
Power cut timeout exceeded. Executing shutdown sequence.
Sending power off signal to NAS.
Connection to larby.internal.hokiyo.com closed.
Powering off. Goodbye.
Connection to dreamer.internal.hokiyo.com closed by remote host.
Connection to dreamer.internal.hokiyo.com closed.

It works. I guess I can now be confident that my homelab will shut off in time, right? The battery for the UPS has a warranty of two years. There are a lot of stories where UPS batteries go bad without them noticing it. That’s where the weakest link is.

In any case, this is good enough. I’ll replace the battery every 3 years.

Homelab

I need to make it more discreet.