Super Pi Part 3: Node-RED

Posted on April 10, 2021

It would be nice if I had an automated way of renewing my IP address with DuckDNS. I could write a shell script and set up a CRON job to run it every so often, but since I'm interested in home automation, I'm going to use Node-RED instead. They have some documentation on getting started with a Pi. The doc recommends a bash script for installing everything. There's a package in the apt repository, but it seems to be finnicky. I can't get logs working, there's an error message that never goes away, my flows don't seem to run. Anyway, it seems like their suggestion to use the script is well-placed, so I ran the script they provide:

bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)

Once that's done, I enabled and started the service so I could access its UI remotely in a browser:

sudo systemctl enable nodered.service
sudo systemctl start nodered.service

The service can now be accessed at port 1880 on our Pi. Since I have NGINX and Pi-Hole's DNS capabilities working so nicely together, I set up another entry for Node-RED. First, I added a DNS record to Pi-Hole as before, this time with the domain node-red.my.local and the IP (as always) 192.168.0.23.

Next, let's create a config file for Node-RED at /etc/nginx/sites-available/nodered and put the following contents in it:

server {
    listen 80;
    listen [::]:80;
    server_name node-red.my.local;

    location / {
        proxy_pass http://localhost:1880;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

Node-RED is a web app listening on its own port, so our previous strategies for Pi-Hole won't work for Node-RED. Instead, we want to pass requests coming to the pi looking for the domain my.node-red on port 80 to be forwarded to port 1880. I'm also setting some headers to provide additional metadata. It's not necessary or really useful right now, but it's common practice and might be helpful if we need to inspect requests.

Now when we navigate to node-red.my.local in the browser we hit the Node-RED web page as desired!

I mentioned earlier that it would be nice to be able to automate the process of updating DuckDNS with my IP, which seems like a good first flow for Node-RED. Personally, I'm having a hard time with Node-RED's documentation, but my style of reading docs might not be not what Node-RED is trying to accomplish. I don't really want a cookbook, I just want to be able to look up a node and find all the information on it and some examples of how to use it. That aside, the UI seems pretty easy to use. All I have to do to build a flow is drag nodes into the grid from the menu on the left:

The first thing I notice is that some nodes have gray circles on either side; the nodes with gray circles on the right are starting nodes, the ones with circles on the right are terminating nodes. To start our flow, then, I need a starting node, in this case I'm going to use the blue inject node. Here's how I configured it:

I'm not 100% sure if the checkbox at the bottom is necessary, it doesn't seem to work without it. I don't really want to have to wait longer than an hour between my IP address changing and DuckDNS getting notified. Clicking done closes the node.

Next I pulled in an exec node under. This node runs a shell command, and you can take the output of that command and send it to subsequent nodes as a payload. I configured it like so:

In this node, I want to get my router's public IP address. An easy way to do that is reaching out to whatismyipaddress.com. The full command is this:

wget -qO- http://bot.whatismyipaddress.com/ ; echo

I set the Output field to when the command is complete - exec mode, since I want to use the output of the command. Lastly, I named the node Get IP and clicked the Done button. Now, I need to link these two nodes together. On the right side of the inject node in the flow, there's a gray circle that turns orange when I mouse over it. I connected the two nodes by clicking and dragging to the circle on the left side of the exec node. My flow now looks like this:

The sides are important: left is always input, right is always output.

Now that I presumably have an IP address, it's probably a good idea to validate it. To do that, I pulled in a yellow switch node under the function tab and configured it:

The Property field has a cool dropdown that lets me pick the context I want to pull information from. In this case, I want msg, because I'm validating the payload of the previous node; and in the text field I put payload, the property I want to pull from the message. Below this, there's a collection of elements. There's a small add button at the bottom to map multiple output branches. In this case, I have two branches: the payload is either valid or it isn't. If you don't want to do anything with the fail case, then you really only need one condition item. I'm not currently using the failure branch, but I plan to set up a notification that uses it in the future. In the dropdown on the left side of the first branch, I selected the matches regex option and provided this regex in the text field:

\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b

This node's done, time to hook it up to the Get IP node, which has three gray output circles. The top circle is stdout, which is the output of the command and the node I want. I linked this gray circle to the input of the Validate node. The middle node is stderr which contains the error dump as a payload, useful for debugging. The bottom node is return code, which is the exit status of the command (typically 0 for success, 1 for failure).

Now, I need to update DuckDNS. I don't necessarily want to do this every hour. If the IP hasn't changed, there's no point going any further and the flow should really end at this point. To make this happen, I pulled in a function node and configured it:

This node has four tabs: Setup, On Start, On Message, and On Stop. The only thing I need to worry about for these purposes is On Message. Here's the code in a more copy-pastable fashion:

flow.set("lastip", flow.get("lastip") || 'initial');
var currentip = msg.payload.trim();
if (flow.get("lastip") == 'initial') {
    flow.set("lastip", currentip);
    msg.payload = currentip;
    return msg;
}
else if (flow.get("lastip") != currentip) {
    flow.set("lastip", currentip);
    msg.payload = currentip;
    return msg;
}

The story is, I want to store my router's IP address so I can reference it on a later run. However, any variable that I define myself is going to expire after this node completes. Thankfully, Node-RED provides objects for this specific purpose named context, flow, and global. The context object is scoped to the current node, and is probably what I should have used here. The flow object is shared between every node in the flow and global is shared between all the flows I end up making. I can add properties to these state objects using getters and setters.

I want to create a property called lastip which is the last IP that the flow retrieved. If this property hasn't been created yet, I want to give it an initial value of initial. It seems like some extra white space comes in on the message, so I used trim() to clean it up. Next, I decide how to handle the IP: if lastip is initial, that means DuckDNS has never been updated as far as this flow knows. This could happen on a restart of the Pi or maybe a restart of Node-RED. In any case, I definitely want to set my lastip property to the IP coming in from the previous node. Also, I don't expect to see this often, so I don't mind updating DuckDNS whenever we fall into this if block. For that to happen, though, this node has to return a message. If there's no message returned, the flow ends with this node. Using the same msg variable that comes into the node, I set its payload to the currentip and return it.

The only other time I want this node to return a message and continue the flow is when the lastip property on the flow doesn't match the currentip pulled in from the Validate node. If this is the case, I'll do exactly the same thing as the other condition block. This block is now done, so I'll link it's input to the output of the Validate node.

The last component my flow needs is a node to update DuckDNS. There are a couple HTTP nodes available; there's an http in node that is less configurable and doesn't provide everything I need. The one I want is named http request:

After pulling the http request node in, I set the Method to Get in the dropdown, and set the URL text field to https://www.duckdns.org/update?domains=<MYDOMAIN>&token=<MYTOKEN>&ip=. If you're doing the same, you'll need to replace <MYDOMAIN> and <MYTOKEN> with your domain and token from DuckDNS. The Payload dropdown I left as Ignore, it seems the IP address gets stuck onto the request without having to set it up somehow. I set the Return dropdown to a UTF-8 string to use with logging later.

That's the end of the flow for now, here's the finished flow:

I'll probably add some more validation and error handling as I get more familiar with Node-RED, but this gets the job done. It took me a while to figure out that the only way to save changes is to click the deploy button, so I'll click that before I close the flow and call it a post.