In my ongoing
quest to find a good home automation software framework to run the
hardware I built, I discovered
Node-Red. Node-Red is the self-professed "visual tool for wiring the Internet of Things". Sounds promising.
Node-Red is a graphical programming environment based on node.js. (
Node.js is basically server-side javascript). Since it's built on node.js, Node-Red can run anywhere node.js runs. It's all
open source, and was started by IBM.
The interface to Node-Red runs completely in a web-browser, so there's no need to install anything on your development machine, and it is even usable from a mobile browser.
It works by connecting wires between different functional nodes, kind of like the picture above. There are lots of different nodes that do various things, from interfacing to a
WeMo light switch to accessing emails from a Gmail account.
There is no built-in UI in Node-Red, but I think that might actually be an advantage. It has numerous methods of connecting to a custom UI, and this way, you don't restrict users to a default UI that might not suit their application.
I got interested in Node-Red because I saw
several different people using it to do home automation with JeeNodes.
Installation
I mostly followed the instructions
here to install Node-Red to an old laptop running
Xubuntu.
First I installed the Node.js package manager and Node.js itself:
sudo apt-get install npm
sudo apt-get install node
Then I downloaded the
latest release zip file from nodered.org and unzipped to ~/dev
cded into that dir and ran '
npm install --production'.
Next up I launched Node-Red it by doing '
nodejs red.js'. Running '
node red.js' doesn't work since the debian packages install it as
nodejs to avoid a conflict. Eventually I ended up symlinking
node to
nodejs to make things easier:
ln -s /usr/bin/nodejs /usr/bin/node
And with that Node-Red seems to be up and running.
Node-Red ships with a pretty good set of basic nodes, but there is also a git repo that has
even more nodes, so I thought I'd try that out. I installed the extra nodes by doing '
cd nodes; git clone https://github.com/node-red/node-red-nodes.git'.
It should be noted that many of these additional nodes rely on node.js libraries and until those libraries are installed, the new node will not appear in the Node-Red palette. As an example, if I want to use the suncalc node to generate an event at sunrise every day, I would need to install the suncalc library by doing '
npm install suncalc'.
Hello World
I ran through the initial
tutorial and was successful. Programs in Node-Red are called "flows". The basic design pattern in Node-Red is: drop and config nodes, wire them together, hit deploy. Pretty simple, and even better, it actually works.
The debugging is a little simplistic in that you can print values to a debug log. But again, it's simple, and it works, so I can't complain too much.
Flows can be exported as a
json string, and imported by the same mechanism. For the rest of this post I'll post the exported json for the flows I've used.
Controlling execution
The Node-Red documentation has a good "hello world" tutorial, and has a tutorial on
creating new plugin nodes, but it's a little sparse beyond that. I did some experiments to figure out how the nodes in Node-Red actually execute. Here's what I've figured out.
First off, Node-Red is completely event based. Nothing executes unless an external event triggered it. The "delay" and "trigger" nodes also allow timing based events to be generated.
The wires between the nodes are there to symbolize a data connection to the previous node. More specifically the wires are really a graphical representation of how javascript Objects get passed to and from nodes. These are usually referred to as
msg objects and their contents can be somewhat arbitrary but most often contain a data member called
payload.
So how can you control whether a node executes, like you can with an "if" statement in most text based languages? If you return a "null" for any of the outputs then any of the
subsequent nodes attached to that output don't execute. A way to demonstrate that is by creating a function
node that has multiple outputs which means you return an array of
msg
objects. Any element in the array that has a value of
null will prevent the connected node from executing. By the way, this is exactly how a "switch" node works. Check out the "demo switch" node in the example flow below.
Here's the example flow I'll be using for the rest of the post:
[{"id":"f64d219e.53a208","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":439,"y":454,"z":"d5a6cdcf.6203b","wires":[["5863bf74.888a3"]]},{"id":"5863bf74.888a3","type":"function","name":"demo switch","func":"msg.payload = \"hello\";\n//return [null, msg];\nreturn [msg, msg];","outputs":"2","x":628,"y":452,"z":"d5a6cdcf.6203b","wires":[["6d0fe6ba.2d2e88"],["d6a0f7c1.e949e8"]]},{"id":"6d0fe6ba.2d2e88","type":"debug","name":"demo out 1","active":true,"console":"false","complete":"false","x":821,"y":407,"z":"d5a6cdcf.6203b","wires":[]},{"id":"d6a0f7c1.e949e8","type":"debug","name":"demo out 2","active":true,"console":"false","complete":"false","x":820,"y":486,"z":"d5a6cdcf.6203b","wires":[]},{"id":"1ec662d2.3b7c25","type":"http in","name":"","url":"/test","method":"get","x":450,"y":277,"z":"d5a6cdcf.6203b","wires":[["dd8cd3d3.17b08"]]},{"id":"9c30b932.4da1e","type":"debug","name":"test URL","active":true,"console":"false","complete":"false","x":850,"y":189,"z":"d5a6cdcf.6203b","wires":[]},{"id":"6b3281a7.0a29c8","type":"http response","name":"","x":841,"y":276,"z":"d5a6cdcf.6203b","wires":[]},{"id":"dd8cd3d3.17b08","type":"function","name":"Gen response","func":"var resp = \"Hello \" + msg.payload.name;\nmsg.payload = resp;\nreturn msg;","outputs":1,"x":665,"y":276,"z":"d5a6cdcf.6203b","wires":[["6b3281a7.0a29c8","9c30b932.4da1e"]]},{"id":"7a4aeee7.85b51","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":424.5182189941406,"y":573.5229034423828,"z":"d5a6cdcf.6203b","wires":[["fbd25db9.042da"]]},{"id":"fbd25db9.042da","type":"function","name":"count","func":"if (context.hasOwnProperty(\"counter\"))\n\tcontext.counter += 1;\nelse\n\tcontext.counter = 0;\n\ncontext.global.counter = context.counter + 1;\n\nmsg.payload = \"local counter = \" + parseInt(context.counter, 10);\n\nreturn msg;","outputs":1,"x":602.5182647705078,"y":572.522876739502,"z":"d5a6cdcf.6203b","wires":[["caaa07a5.3555f8"]]},{"id":"caaa07a5.3555f8","type":"debug","name":"","active":true,"console":false,"complete":false,"x":782.5182189941406,"y":577.5229034423828,"z":"d5a6cdcf.6203b","wires":[]},{"id":"36a0278c.c95fd8","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":424.5182189941406,"y":638.5228900909424,"z":"d5a6cdcf.6203b","wires":[["4b22fcbc.b4dd04"]]},{"id":"4b22fcbc.b4dd04","type":"function","name":"get global count","func":"msg.payload = \"global counter = \" + parseInt(context.global.counter, 10);\n\nreturn msg;","outputs":1,"x":603.5182189941406,"y":634.5229034423828,"z":"d5a6cdcf.6203b","wires":[["f51d7f6a.0ae28"]]},{"id":"f51d7f6a.0ae28","type":"debug","name":"","active":true,"console":false,"complete":false,"x":782.5182762145996,"y":633.5228900909424,"z":"d5a6cdcf.6203b","wires":[]}]
Local and Global variables
Within
a function node there is a way to persist data between calls to that
node that is both local to that node and global to all nodes. It's
documented pretty well
here. I created a quick demonstration of globals using a couple of function nodes.
First web service
Just to try out what I'd learned I used some techniques from
here, and got a working test URL using http in and http response nodes! Just pass a "name" parameter to the URL and it will print a Hello message. Something like this: "http://site/test?name=Ken"
Conclusions
I really liked working with Node-Red, and I think I'll probably continue using it.
Pros
- Open source and actively developed by IBM
- Web-based development
- Graphical programming/rule development
- https and basic http authentication is supported. See here for more info.
- Mobile-friendly interface
Cons
- No built-in UI
- No built-in database for sensor readings, though there are nodes to interface with various DB formats