Skip to content

Instantly share code, notes, and snippets.

@Paraphraser
Last active June 3, 2021 00:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Paraphraser/b63273733062abd25e03ad9a22de886a to your computer and use it in GitHub Desktop.
Save Paraphraser/b63273733062abd25e03ad9a22de886a to your computer and use it in GitHub Desktop.
UPS monitor - back end - proof of concept

UPS monitor - back end - proof of concept

Assumptions

  • There exists a mechanism (unspecified) for acquiring status information from an Uninterruptible Power Supply;
  • That mechanism is available to Node-RED; and
  • There exists a Node-RED flow (unspecified) that is capable of interpreting the UPS status information and making decisions about when devices should power down and in what order.

Task Goal

The goal is to show that decisions made by Node-RED can be distributed via MQTT, so that client systems can react appropriately.

Definitions

Two hosts are involved in this proof of concept:

  1. A "coordinator". Assumed to be a Raspberry Pi running IOTstack with at least the following containers:

    • Mosquitto - the MQTT broker role
    • Node-RED - the MQTT publisher role
  2. The "test device". Assumed to be another Raspberry Pi running a:

    • bash script - the MQTT subscriber role

Key points:

  • There is no dependence on Node-RED. Any device capable of calling mosquitto_pub could be the publisher.
  • There is no requirement for the Mosquitto and Node-RED containers to be running on the same Raspberry Pi. It is simply the most likely arrangement.
  • There is no requirement for the "coordinator" and "test device" to be different machines. It is perfectly possible (and probably desirable) for Node-RED to be able to send a "shut down" signal to the machine it is running on.

It is just simpler to keep things straight in your mind if you assume two hosts, with coordinator and test device roles.

Dependencies

Your test device will need the Mosquitto clients (mosquitto_sub and mosquitto_pub). On Raspbian, you install these with:

$ sudo apt update
$ sudo apt install -y mosquitto_clients

On macOS, use HomeBrew and run:

$ brew update
$ brew install mosquitto 

Note:

  • This installs the Mosquitto broker as well but does not activate it. Just ignore the messages telling you how to activate it.

For Windows, see mosquitto.org/download.

Are you a Windows user?

The scripts shown here should be created on your Raspberry Pi. Please do not make the mistake of selecting the text, copying it into a text editor on your Windows machine, saving the file, and then moving the file to your Raspberry Pi. Unless you take precautions, Windows will add its 0x0d 0x0a (CR+LF) line endings and those will stop things from working properly on your Raspberry Pi.

Script – power_monitor.sh

This script needs to be installed on the "test device". If you have five test devices, it needs to be installed on each one.

  1. Replace the right hand side of the "BROKER" variable with the domain name or IP address of your coordinator (ie the Raspberry Pi running the Mosquitto container under IOTstack).

  2. Assumed installation location: ~/.local/bin/power_monitor.sh.

    if ~/.local/bin does not exist, create it, then logout and login so it gets added to your PATH.

#!/usr/bin/env bash

# set parameters
SCRIPT=$(basename "$0")
TOPIC="grid/begone"
BROKER="iot-hub.local"

# is this script running in the foreground or background?
if [ "$(tty)" = "not a tty" ] ; then

   # background! redirect all output to log
   LOGFILE="$HOME/Logs/$SCRIPT.log"
   mkdir -p $(dirname "$LOGFILE")
   touch "$LOGFILE"
   exec >> "$LOGFILE"
   exec 2>> "$LOGFILE"

   # assume started from crontab @reboot and sleep to
   # give the network time to start up and stabilise
   sleep 15

fi

echo "$(date) $SCRIPT: Listening for $TOPIC from $BROKER"

# infinite loop
while [ 0 ] ; do

   # wait (indefinitely) for exactly one message or an error condition
   LEVEL=$(mosquitto_sub -C 1 -v -h "$BROKER" -t "$TOPIC" -F "%p")
   EXITCODE=$?
   
   # exit code zero means "message received"
   if [ $EXITCODE -eq 0 ] ; then

      # vector on event
      case "$LEVEL" in

        "1" )
          echo "$(date) $SCRIPT: responding to level 1 power-down message"
          ;;

        "2" )
          echo "$(date) $SCRIPT: responding to level 2 power-down message"
          sudo shutdown -h +1 "$SCRIPT scheduling shutdown in one minute"
          ;;

        *)
          echo "$(date) $SCRIPT: Unexpected power-down level = $LEVEL"
          ;;

      esac

    else

       echo "$(date) $SCRIPT: non-zero return code $EXITCODE - will retry"
       sleep 15

    fi

done

echo "$(date) $SCRIPT: Unexpected exit from infinite loop"

The script subscribes to the "grid/begone" topic. It expects a message payload consisting of a single digit:

  • a 1 simply echoes a message to the log
  • a 2 echoes a message to the log and then initiates a shutdown of the machine on which it is running.

Any other number results in an error message appearing in the log.

Unless explicitly killed, the script loops indefinitely so it should ride through transient outages like Mosquitto container restarts.

Crontab

The first three lines in the following should be in every crontab. The key ingredient for this project is the last line. It fires off the script at reboot time.

SHELL=/bin/bash
HOME=/home/pi
PATH=/home/pi/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

@reboot power_monitor.sh

If you don't have a crontab already, just get those lines into a file (eg mycrontab.txt) then load that file as your crontab by:

$ crontab mycrontab.txt

Google is your friend if you need more help than that.

Reboot

A reboot is required to cause cron to activate power_monitor.sh.

$ sudo reboot

After the machine comes up, wait 30 seconds or so then make sure the following log file has been created:

~/Logs/power_monitor.sh.log

You should expect to see at least:

«date & time» power_monitor.sh: Listening for grid/begone from «BROKER»

If you don't see that, check your work.

Key point:

  • The script does not write to the log immediately. Because the script is started during a reboot, it needs to wait for the network to be ready. It does that by sleeping for 15 seconds. Don't be in too much of a hurry.

Node-RED

  1. Add a new empty flow and give it an appropriate name (eg "UPS Responder").

  2. Drag the following nodes onto the canvas from the palette:

    • Two Inject nodes,
    • One debug node
    • One MQTT out node
  3. Wire them up as per the following:

    test flow

  4. The first Inject node should be set:

    • Name: Trigger Level 1
    • msg.payload = [09] 1
    • msg.topic = [az] grid/begone
  5. The second Inject node should be set:

    • Name: Trigger Level 2
    • msg.payload = [09] 2
    • msg.topic = [az] grid/begone
  6. The MQTT Out node should point to "mosquitto" (the container name) on port 1883. See mqtt-in-node if you don't know how to do that. MQTT in and out nodes are configured identically. Give it a name like "Publish" or "grid/begone".

  7. It is a good idea to set the Debug node to output the "complete message object" so that you get to see both the topic and message payload in the debug window.

  8. Click Deploy.

Expected behaviour

  1. On the test device, you can place a "watch" on the log like this:

    $ tail -f tail -f Logs/power_monitor.sh.log
    
  2. When you click the "Trigger Level 1" button on the coordinator, Node-RED will "publish" the equivalent of:

    $ mosquitto_pub -t "grid/begone" -m "1"
    

    The power_monitor.sh script (the "subscriber") running on the test device will receive that message from the broker (the Mosquitto container) and write the following to the log:

    «date & time» power_monitor.sh: responding to level 1 power-down message
    
  3. When you click the "Trigger Level 2" button on the coordinator, Node-RED will initiate the equivalent of this:

    $ mosquitto_pub -t "grid/begone" -m "2"
    

    The power_monitor.sh script running on the test device will receive that message and:

    • write the following to the log:

       «date & time» power_monitor.sh: responding to level 2 power-down message
      
    • initiate a system shutdown, scheduled for one minute in the future.

Tests

  1. Connect to the test device:

    $ ssh test-dev.local
    
  2. Place a watch on the log:

    pi@test-dev:~ $ tail -f Logs/power_monitor.sh.log 
    Wed 02 Jun 2021 11:22:48 PM AEST power_monitor.sh: Listening for grid/begone from iot-hub.local
    
  3. Click the "Trigger Level 1" button in the Node-RED flow. Node-RED debug output is:

    02/06/2021, 11:23:53 pmnode: 78bedf81.87f74
    	grid/begone : msg : Object
    	object
    	_msgid: "c745484a.aec318"
    	payload: 1
    	topic: "grid/begone"
    

    Test device log response is:

    Wed 02 Jun 2021 11:23:53 PM AEST power_monitor.sh: responding to level 1 power-down message
    
  4. Click the "Trigger Level 2" button in the Node-RED flow. Node-RED debug output is:

    02/06/2021, 11:25:37 pmnode: 78bedf81.87f74
    	grid/begone : msg : Object
    	object
    	_msgid: "beaa1894.accae8"
    	payload: 2
    	topic: "grid/begone"
    

    Test device response is:

    Wed 02 Jun 2021 11:25:37 PM AEST power_monitor.sh: responding to level 2 power-down message
    
    Broadcast message from root@test-dev (Wed 2021-06-02 23:25:37 AEST):
    
    power_monitor.sh scheduling shutdown in one minute
    The system is going down for poweroff at Wed 2021-06-02 23:26:37 AEST!
    
    Shutdown scheduled for Wed 2021-06-02 23:26:37 AEST, use 'shutdown -c' to cancel.
    
    Broadcast message from root@test-dev (Wed 2021-06-02 23:26:37 AEST):
    
    power_monitor.sh scheduling shutdown in one minute
    The system is going down for poweroff NOW!
    
    Connection to test-dev.local closed by remote host.
    Connection to test-dev.local closed.
    
    [Wed Jun 02 23:26:37:~] $ 
    

Extending the script

  1. The example script only handles two messages but it is written as a case statement to make it easy to add more levels.

  2. The script can easily do things like shut down IOTstack, as in:

    docker-compose -f "$HOME/IOTstack/docker-compose.yml" down
    
  3. The script is not limited to running commands on the local machine. Suppose you have a device (eg your home router) which is not capable of running a script like power_monitor.sh but is capable of receiving shutdown instructions via ssh. A Raspberry Pi running power_monitor.sh could easily relay such instructions.

  4. The script could be extended to send acknowledgements back to Node-RED by using mosquitto_pub commands.

  5. In the current design, Node-RED publishes to the "grid/begone" topic without using the retain flag. This implies that any subscriber that is in the process of rebooting or not otherwise listening may not hear the message. Adding the retain flag would address this weakness but some care would need to be taken to prevent stale "shutdown" messages from preventing reboots once the triggering UPS condition was resolved.

Extending the concept

This is a proof of concept. Although the power_monitor.sh script "works", it is not particularly elegant and its error-handling is unsophisticated. It also depends on the availability of mosquitto_sub.

More elegant and sophisticated solutions with better error-handling could involve techniques such as are described at Python MQTT publish and subscribe.

Take the example above of a home router. While I can connect to my own home router via ssh, the most I can do is cause it to reboot. It has no concept of shutdown or power-off so, during a mains power outage, it's going to sit there draining power from the UPS.

The solution, of course, is a smart switch, flashed with firmware that subscribes to "grid/begone" and removes power from the router. Contrived scenario? Perhaps. The idea is to help you understand MQTT's role as an enabling technology.

Here's another idea. Suppose you add a "level 0". All it does is publish the equivalent of "I've heard you" via MQTT. Now you have a mechanism by which Node-RED can test that all the devices that should be listening are indeed listening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment