Table of Contents

Reverse-Proxying Node-RED and Alexa Philips Hue Emulation Bridge

The Alexa Philips Hue Emulation Bridge for node-red provided by the node-red-contrib-amazon-echo package needs to listen on port 80 for HTTP requests sent by Amazon Alexa devices performing discovery. Instructions for the node-red-contrib-amazon-echo package include setting up a port redirect from port 8080 used by the Amazon Echo Hub node in the node-red interface to port 80. Unfortunately, since port 80 is occupied, a webserver performing reverse proxying cannot be placed in front of node-red without some tricks.

One solution is to use nginx and regular expressions in an attempt to match the user-agent of Amazon Echo devices in order to redirect regular HTTP requests to node-red and HTTP requests from Amazon Alexa devices to the node-red-contrib-amazon-echo listening port on 8080:

map "$http_user_agent" $targetupstream {
  default        http://127.0.0.1:1880;
  "~AEO[A-Z]{2,3} Build\/[A-Z0-9]+" http://127.0.0.1:8080;
}

server {
        listen 80;
        server_name mynoderedserver.tld;

        access_log /var/log/nginx/access.log;

        location / {
                proxy_pass $targetupstream;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }
}

The map at the top is responsible for conditionally proxying requests based on the user agents: by default, all requests to the server mynoderedserver.tld are passed to node-red on port 1880 and requests that match the regular expression ~AEO[A-Z]{2,3} Build\/[A-Z0-9]+ are delivered to port 8080 on which the Amazon Alexa Philips Hue Emulation bridge (provided by node-red-contrib-amazon-echo) is listening.

The result is that a request via a browser to http://mynoderedserver.tld will open up the node-red flow design interface. Similarly, requests to: http://mynoderedserver.tld/ui will open the dashboard provided by node-red-dashboard package. This removes the need to memorize ports and/or IP addresses.

Creating an Elegant Multiple Algorithm Random Number Engine in Node Red

One problem with Node Red is that function nodes are meant to execute JavaScript code but there is no persistence using the function node such that if the JavaScript code is a class then the class within the function node will have to be instantiated each and every time a message is passed to the input of the function node.

It would be nice if Node Red flow would allow for easily adding classes by instantiating the classes when the Node Red flow is deployed (or started) and then re-using the instantiated object every time. There is a way to reuse classes instantiated on startup by using dynamic object instantiation and invocation whilst additionally only using built-in nodes in order to not require any third-party modules.

As an example, the following is an implementation as a Node Red flow of a multiple-algorithm random number engine. The flow can be used by using link in and out nodes after specifying the algorithm to be used.

Creating the Engine on Startup

The following nodes are used to dynamically instantiate the classes that will generate the random numbers:

Selecting the Algorithm and Calling the Engine

After an instance of each class is created and stored, the following flow is used to generate random numbers whenever other flows link using the link-in node and will output the random numbers via the link-out node to be processed.

The nodes within this flow have the following use:

Export

rng.json
[{"id":"bf051c9.452f96","type":"tab","label":"Random Number Generators","disabled":false,"info":""},{"id":"3f5cdd9f.a44f52","type":"debug","z":"bf051c9.452f96","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":970,"y":560,"wires":[]},{"id":"4dffce72.2bcd48","type":"inject","z":"bf051c9.452f96","name":"Begin","topic":"","payload":"","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":"1","x":310,"y":160,"wires":[["986c2654.33466","f79a2199.e417e"]]},{"id":"9f9325d1.023298","type":"function","z":"bf051c9.452f96","name":"Store","func":"/* Push the object instance onto the pool of global variables. */\nflow.set(msg.topic, msg.payload, msg.store);\n","outputs":0,"noerr":0,"x":990,"y":160,"wires":[]},{"id":"986c2654.33466","type":"template","z":"bf051c9.452f96","name":"Class Definition","field":"payload","fieldType":"msg","format":"javascript","syntax":"plain","template":"/* \n   Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,\n   All rights reserved.                          \n \n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions\n   are met:\n \n     1. Redistributions of source code must retain the above copyright\n        notice, this list of conditions and the following disclaimer.\n \n     2. Redistributions in binary form must reproduce the above copyright\n        notice, this list of conditions and the following disclaimer in the\n        documentation and/or other materials provided with the distribution.\n \n     3. The names of its contributors may not be used to endorse or promote \n        products derived from this software without specific prior written \n        permission.\n \n   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n   \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n \n \n   Any feedback is very welcome.\n   http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html\n   email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space)\n\n   Wrapped by Sean McCullough (banksean@gmail.com) into JavaScript,\n   Altered by Wizardry and Steamworks (grimore.org) into a JavaScript class-based variation.\n*/\n\nclass MersenneTwister {\n    // class methods\n    constructor(seed) {\n        if (seed === undefined) {\n            seed = new Date().getTime();\n        }\n        /* Period parameters */\n        this.N = 624;\n        this.M = 397;\n        this.MATRIX_A = 0x9908b0df;   /* constant vector a */\n        this.UPPER_MASK = 0x80000000; /* most significant w-r bits */\n        this.LOWER_MASK = 0x7fffffff; /* least significant r bits */\n\n        this.mt = new Array(this.N); /* the array for the state vector */\n        this.mti = this.N + 1; /* mti==N+1 means mt[N] is not initialized */\n\n        this.init_genrand(seed);\n    }\n\n    /* initializes mt[N] with a seed */\n    init_genrand() {\n        this.mt[0] = s >>> 0;\n        for (this.mti = 1; this.mti < this.N; this.mti++) {\n            var s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30);\n            this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253)\n                + this.mti;\n            /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */\n            /* In the previous versions, MSBs of the seed affect   */\n            /* only MSBs of the array mt[].                        */\n            /* 2002/01/09 modified by Makoto Matsumoto             */\n            this.mt[this.mti] >>>= 0;\n            /* for >32 bit machines */\n        }\n    }\n\n    /* initialize by an array with array-length */\n    /* init_key is the array for initializing keys */\n    /* key_length is its length */\n    /* slight change for C++, 2004/2/26 */\n    init_by_array(init_key, key_length) {\n        var i, j, k;\n        this.init_genrand(19650218);\n        i = 1; j = 0;\n        k = (this.N > key_length ? this.N : key_length);\n        for (; k; k--) {\n            var s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30)\n            this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525)))\n                + init_key[j] + j; /* non linear */\n            this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */\n            i++; j++;\n            if (i >= this.N) { this.mt[0] = this.mt[this.N - 1]; i = 1; }\n            if (j >= key_length) j = 0;\n        }\n        for (k = this.N - 1; k; k--) {\n            var s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);\n            this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941))\n                - i; /* non linear */\n            this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */\n            i++;\n            if (i >= this.N) { this.mt[0] = this.mt[this.N - 1]; i = 1; }\n        }\n\n        this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */\n    }\n\n    /* generates a random number on [0,0xffffffff]-interval */\n    genrand_int32() {\n        var y;\n        var mag01 = new Array(0x0, this.MATRIX_A);\n        /* mag01[x] = x * MATRIX_A  for x=0,1 */\n\n        if (this.mti >= this.N) { /* generate N words at one time */\n            var kk;\n\n            if (this.mti == this.N + 1)   /* if init_genrand() has not been called, */\n                this.init_genrand(5489); /* a default initial seed is used */\n\n            for (kk = 0; kk < this.N - this.M; kk++) {\n                y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);\n                this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1];\n            }\n            for (; kk < this.N - 1; kk++) {\n                y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);\n                this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1];\n            }\n            y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK);\n            this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1];\n\n            this.mti = 0;\n        }\n\n        y = this.mt[this.mti++];\n\n        /* Tempering */\n        y ^= (y >>> 11);\n        y ^= (y << 7) & 0x9d2c5680;\n        y ^= (y << 15) & 0xefc60000;\n        y ^= (y >>> 18);\n\n        return y >>> 0;\n    }\n\n    /* generates a random number on [0,0x7fffffff]-interval */\n    genrand_int31() {\n        return (this.genrand_int32() >>> 1);\n    }\n\n    /* generates a random number on [0,1]-real-interval */\n    genrand_real1() {\n        return this.genrand_int32() * (1.0 / 4294967295.0);\n        /* divided by 2^32-1 */\n    }\n\n    /* generates a random number on [0,1)-real-interval */\n    random() {\n        return this.genrand_int32() * (1.0 / 4294967296.0);\n        /* divided by 2^32 */\n    }\n\n    /* generates a random number on (0,1)-real-interval */\n    genrand_real3() {\n        return (this.genrand_int32() + 0.5) * (1.0 / 4294967296.0);\n        /* divided by 2^32 */\n    }\n\n    /* generates a random number on [0,1) with 53-bit resolution*/\n    genrand_res53() {\n        var a = this.genrand_int32() >>> 5, b = this.genrand_int32() >>> 6;\n        return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0);\n    }\n}\n","output":"str","x":480,"y":80,"wires":[["e0a73b2e.e3e1d"]]},{"id":"e0a73b2e.e3e1d","type":"function","z":"bf051c9.452f96","name":"Instantiation","func":"/* Retrieve the class definition from the passed message. */\nvar classDefinition = msg.payload;\n\n/* Construct the object from the class definition. */\nvar object = eval(`new ${classDefinition}()`);\n\n/* Return the instantiated object. */\nmsg.topic = object.constructor.name;\nmsg.payload = object;\n\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":160,"wires":[["2575ce58.41a362"]]},{"id":"df62633f.d51c38","type":"function","z":"bf051c9.452f96","name":"Method Invoker","func":"var engine = flow.get(msg.payload, 'randomEngine');\nmsg.payload = engine.random();\nreturn msg;\n","outputs":1,"noerr":0,"x":680,"y":400,"wires":[["64ac43e2.5e73bc","aaae4e9d.091358"]]},{"id":"2575ce58.41a362","type":"change","z":"bf051c9.452f96","name":"Storage","rules":[{"t":"set","p":"store","pt":"msg","to":"randomEngine","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":840,"y":160,"wires":[["9f9325d1.023298"]]},{"id":"e5fa9a1b.38e4c","type":"link in","z":"bf051c9.452f96","name":"","links":[],"x":435,"y":400,"wires":[["df62633f.d51c38"]]},{"id":"64ac43e2.5e73bc","type":"link out","z":"bf051c9.452f96","name":"","links":[],"x":895,"y":400,"wires":[]},{"id":"f79a2199.e417e","type":"template","z":"bf051c9.452f96","name":"Class Definition","field":"payload","fieldType":"msg","format":"javascript","syntax":"plain","template":"/*\n * Converted from Lehmer Random Number Generator in LOLCode.\n *   \n * OBTW  Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3  TLDR\n * OBTW X(k+1) = g * X(k) mod n\n * I HAS A COUNTER ITZ 1\n * HOW IZ I WAS_MESS YR NUMBER\n *   I HAS A THING ITZ MAEK NUMBER A NUMBAR\n *     IM IN YR LOOP UPPIN YR ROUNDS WILE DIFFRINT ROUNDS AN NUMBER\n *       THING R MOD OF PRODUKT OF 75 AN SUM OF THING AN COUNTER AN 65537\n *         COUNTER R SUM OF COUNTER AN 1\n *     IM OUTTA YR LOOP\n *   FOUND YR MOD OF THING AN NUMBER\n * IF U SAY SO\n *\n */\n \nclass Lehmer {\n    constructor(g, n, max) {\n        if(g === undefined) {\n            this.g = 75;\n        }\n        \n        if (n === undefined) {\n            this.n = 65537;\n        }\n        \n        if(max === undefined) {\n            this.max = 100;\n        }\n        \n        this.i = 1;\n    }\n    \n    random() {\n        this.i = (this.g * (this.max + this.i++)) % this.n;\n        return this.i;\n    }\n}\n","output":"str","x":480,"y":240,"wires":[["e0a73b2e.e3e1d"]]},{"id":"97dde7ac.c1f06","type":"inject","z":"bf051c9.452f96","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":460,"y":500,"wires":[["df62633f.d51c38"]]},{"id":"aaae4e9d.091358","type":"function","z":"bf051c9.452f96","name":"Normalize [0,1]","func":"const magnitude = Math.ceil(Math.log10(msg.payload + 1));\n\nvar modulo = msg.payload % magnitude;\n\nswitch(Math.abs(parseInt(msg.payload))) {\n    case 0:\n        msg.payload = modulo;\n        break;\n    default:\n        msg.payload = modulo / magnitude;\n        break;\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"x":880,"y":480,"wires":[["3f5cdd9f.a44f52"]]}]

Integrating Jenkins and Node-Red for Job Build Automation

A node-red flow can be crated to automate building jobs via the UI. Instead of statically declaring jobs within node-red, with the help of some clever disposition of nodes, the list of jobs to build can be retrieved via the Jenkins HTTP API, dynamically built and fed into a dropdown selector on the dashboard. Then, when a user selects a job on the dashboard, the job name gets stored among the flow variables to be picked up when the user presses a build button. When the build button is pressed, the job name is retrieved from the dropdown selector on the UI and a POST HTTP request is made to Jenkins to run the job.

jenkins.json
[{"id":"7180fd67.94b164","type":"tab","label":"Jenkins","disabled":false,"info":""},{"id":"8218139e.d32828","type":"ui_button","z":"7180fd67.94b164","name":"","group":"2fd36062.d4104","order":3,"width":0,"height":0,"passthru":false,"label":"Build","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":150,"y":540,"wires":[["59285b3c.629fbc"]]},{"id":"2ccebd98.8406a2","type":"http request","z":"7180fd67.94b164","name":"","method":"POST","ret":"txt","paytoqs":false,"url":"","tls":"","persist":false,"proxy":"","authType":"","x":1230,"y":540,"wires":[["3841dda8.ea9b7a"]]},{"id":"ad14cdf0.2ccba8","type":"credentials","z":"7180fd67.94b164","name":"","props":[{"value":"username","type":"msg"},{"value":"password","type":"msg"},{"value":"jenkins","type":"msg"}],"x":670,"y":540,"wires":[["9e5e36f5.bb0ae"]]},{"id":"5970b615.2af028","type":"ui_dropdown","z":"7180fd67.94b164","name":"Jenkins Jobs","label":"Jobs","tooltip":"Jenkins Jobs","place":"Select option","group":"2fd36062.d4104","order":3,"width":0,"height":0,"passthru":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"","x":980,"y":440,"wires":[["6ffe4097.a8688"]]},{"id":"384a76ed.cd970a","type":"inject","z":"7180fd67.94b164","name":"Initialize","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":280,"y":320,"wires":[["8f19937f.086ad8"]]},{"id":"a95a1739.ce39","type":"http request","z":"7180fd67.94b164","name":"","method":"GET","ret":"txt","paytoqs":false,"url":"","tls":"","persist":false,"proxy":"","authType":"","x":1010,"y":380,"wires":[["f5a411b0.ca704"]]},{"id":"8f19937f.086ad8","type":"credentials","z":"7180fd67.94b164","name":"","props":[{"value":"username","type":"msg"},{"value":"password","type":"msg"},{"value":"jenkins","type":"msg"}],"x":470,"y":380,"wires":[["4a595e2f.95b068"]]},{"id":"4a595e2f.95b068","type":"change","z":"7180fd67.94b164","name":"Authentication","rules":[{"t":"set","p":"headers","pt":"msg","to":"'Basic ' & $base64encode(username & ':' & password)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":380,"wires":[["ab2064a2.a4b978"]]},{"id":"3841dda8.ea9b7a","type":"debug","z":"7180fd67.94b164","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1250,"y":660,"wires":[]},{"id":"f5a411b0.ca704","type":"json","z":"7180fd67.94b164","name":"","property":"payload","action":"obj","pretty":false,"x":1170,"y":380,"wires":[["10c9d4d7.594b4b"]]},{"id":"a8e2cc1e.9e44c8","type":"split","z":"7180fd67.94b164","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":310,"y":440,"wires":[["769ed2a8.094b9c"]]},{"id":"10c9d4d7.594b4b","type":"change","z":"7180fd67.94b164","name":"Get Jobs","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.jobs","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":160,"y":440,"wires":[["a8e2cc1e.9e44c8"]]},{"id":"769ed2a8.094b9c","type":"change","z":"7180fd67.94b164","name":"Get Name","rules":[{"t":"set","p":"payload","pt":"msg","to":"msg.payload.name","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":440,"wires":[["62878d4e.d6909c"]]},{"id":"62878d4e.d6909c","type":"join","z":"7180fd67.94b164","name":"","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":630,"y":440,"wires":[["b18ded1a.7ce8f8"]]},{"id":"b18ded1a.7ce8f8","type":"change","z":"7180fd67.94b164","name":"Set Options","rules":[{"t":"set","p":"options","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":440,"wires":[["5970b615.2af028"]]},{"id":"6ffe4097.a8688","type":"change","z":"7180fd67.94b164","name":"Set Selected Job","rules":[{"t":"set","p":"selectedJob","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1190,"y":440,"wires":[[]]},{"id":"59285b3c.629fbc","type":"change","z":"7180fd67.94b164","name":"Get Selected Job","rules":[{"t":"set","p":"job","pt":"msg","to":"selectedJob","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":540,"wires":[["74a80484.312994"]]},{"id":"9e5e36f5.bb0ae","type":"change","z":"7180fd67.94b164","name":"Authentication","rules":[{"t":"set","p":"headers","pt":"msg","to":"'Basic ' & $base64encode(username & ':' & password)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":540,"wires":[["cdffb7c2.a7b908"]]},{"id":"cdffb7c2.a7b908","type":"change","z":"7180fd67.94b164","name":"Build URL","rules":[{"t":"set","p":"url","pt":"msg","to":"'https://' & username & ':' & password & '@' & jenkins & '/job/' & job & '/build'","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1050,"y":540,"wires":[["2ccebd98.8406a2"]]},{"id":"feccf819.b3eba","type":"inject","z":"7180fd67.94b164","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":640,"wires":[["59285b3c.629fbc"]]},{"id":"74a80484.312994","type":"switch","z":"7180fd67.94b164","name":"","property":"payload","propertyType":"msg","rules":[{"t":"nempty"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":510,"y":540,"wires":[["ad14cdf0.2ccba8"],[]]},{"id":"ab2064a2.a4b978","type":"change","z":"7180fd67.94b164","name":"Build URL","rules":[{"t":"set","p":"url","pt":"msg","to":"'https://' & jenkins & '/api/json?tree=jobs[name]'","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":380,"wires":[["a95a1739.ce39"]]},{"id":"2fd36062.d4104","type":"ui_group","z":"","name":"Jenkins","tab":"a4498652.0868f","disp":true,"width":"6","collapse":true},{"id":"a4498652.0868f","type":"ui_tab","z":"","name":"Vault 101","icon":"dashboard","disabled":false,"hidden":false}]

After importing the template, the credentials nodes have to be updated in order to set the Jenkins username (username), API token (password) and the Jenkins hostname (jenkins).

The main trick involved in populating the dropdown dashboard element is a REST call to Jenkins to retrieve the name of the jobs and then format the response to match the msg.options input parameter of the dropdown dashboard element. When a user selects an job item on the dashboard from the dropdown element, the dropdown node outputs the selected item as msg.payload which is then stored into a flow variable flow.selectedJob. Conversely, when the build button is pressed, the job name is retrieved from the flow variable flow.selectedJob and then the rest of the workflow involves making a REST call to Jenkins in order to start the job.

Persistent Reusable Functions

Since Node-Red does not have a function for every purpose it is sometimes necessary to define javascript functions within function nodes. However, since function nodes cannot route messages out of different outlets, reusing a defined function usually implies copying the code over to other nodes.

In order to define a persistent function, one solution is to use the Node-Red flow.set() function to store the function within the flow (or globally, via global.set()) and then on a different node to pull the function via flow.get() (respectively, global.get()) and invoke the function with the right amount of parameters.

The flow consists of two sections: the upper half contains an injector that feeds into the Function Definition node. In turn, the Function Definition node contains the following code:

///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
function wasKeyValueGet(k, data) {
    if(data.length == 0) return "";
    if(k.length == 0) return "";
    var a = data.split(/&|=/);
    var i = a.filter((e,i) => !(i % 2)).indexOf(k);
    if(i != -1) return a[(2 * i) % a.length + 1];
    return "";
}
 
flow.set('wasKeyValueGet', wasKeyValueGet)
 
return msg;

that defines a wasKeyValueGet function and then stores the function definition within the flow via flow.set().

The lower part of the flow is an example invocation of the wasKeyvalueGet function, consisting in an injector node a Function Invocation node and a debug node to print out the result. The Function Invocation node contains the following code:

var proc = flow.get('wasKeyValueGet')("data", "data=a&p=b")
msg.payload = proc;
return msg;

and the debug node will print out the expected result a on the debug tab.

Here is the full flow export:

[{"id":"e1e1f80f.4787b","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"4bc33115.6d19a8","type":"inject","z":"e1e1f80f.4787b","name":"Initialize","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":200,"y":120,"wires":[["d28e3bed.d65a58"]]},{"id":"d28e3bed.d65a58","type":"function","z":"e1e1f80f.4787b","name":"Function Definition","func":"///////////////////////////////////////////////////////////////////////////\n//    Copyright (C) 2019 Wizardry and Steamworks - License: CC BY 2.0    //\n///////////////////////////////////////////////////////////////////////////\nfunction wasKeyValueGet(k, data) {\n    if(data.length == 0) return \"\";\n    if(k.length == 0) return \"\";\n    var a = data.split(/&|=/);\n    var i = a.filter((e,i) => !(i % 2)).indexOf(k);\n    if(i != -1) return a[(2 * i) % a.length + 1];\n    return \"\";\n}\n\nflow.set('wasKeyValueGet', wasKeyValueGet)\n\nreturn msg;","outputs":1,"noerr":0,"x":390,"y":120,"wires":[[]]},{"id":"5e01a723.e7792","type":"function","z":"e1e1f80f.4787b","name":"Function Invocation","func":"var proc = flow.get('wasKeyValueGet')(\"data\", \"data=a&p=b\")\nmsg.payload = proc;\nreturn msg;\n","outputs":1,"noerr":0,"x":400,"y":240,"wires":[["964d7154.d9d7e8"]]},{"id":"964d7154.d9d7e8","type":"debug","z":"e1e1f80f.4787b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":610,"y":240,"wires":[]},{"id":"c8046e4b.6c057","type":"inject","z":"e1e1f80f.4787b","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":200,"y":240,"wires":[["5e01a723.e7792"]]}]

Slow Graphs in Node-Red

One major problem with Node-Red dashboard graphs is that the graphs start to slow down after about $30000$ data points and the dashboard will start slowing down the browser considerably. To resolve the issue, reduce the number of data points displayed on graphs.

Slow Down Data Rate

Assuming a hypothetical case where there exists a data generator made to output data every second n order to power a gauge to display a battery level and that an additional display would have to be added that displays a history of the battery level.

Given that graphs in Node-Red (and, more generally, javascript) have major issues overloading browser memory with data points such that a large data set would slow down the browser, it would be nice if the gauge display could move every second with updated data, whilst the chart history display would plot data points every minute.

Such a case is problematic to create without having to write code yourself such that the following system uses only built-in nodes with very little help from the function node-red node.

The following is an illustration of a node-red flow that takes as input some value (represented as $1u$) that is generated every second.

The lower half of the flow is trivial, given that the $1u$ item is input directly into the gauge display as intended in order to offer a "live" and "dynamic" display of the data, whilst the upper half takes care to slow down the data rate to $1u$ per minute.

The way that that the data is slowed down, is a little counter intuitive, so all nodes will be described by following the flow; in order:

  1. the $1u$ data is buffered up into an array buffer using the buffer-array node, that is set to contain up to 60 items in a zero filled array that is counter-intuitively output every time the buffer-array node is fed data,
  2. the switch node, determines whether the array buffer supplied by the buffer-array contains any zeros and switches any non-zero array into the lower output connection point while leaving the upper connection point floating for a buffer containing zeroes,
    1. the data is then fed both into the average-array node, a change node that the JSONata expression $average(payload) in order to feed the data into the Battery History chart, thereby averaging the values over the past $60s$ into a single value
    2. similarly, the data is also fed into a function node labeled Generate that is responsible for generating a blank array of zeros via the javascript code Array.from({length: 60}, () ⇒ 0), an array of zeroes that is then split via the split node into individual zeroes and fed back into the buffer-array node in order to contaminate the data such that the follow-up switch node will fail to pass the data until the array stops being corrupted with zeroes

Use Private NPM Repository

In order to use a private NPM repository with node-red, set the npm_config_registry environment variable to the URL of the NPM repository to use. Setting environment variables can be performed from the node-red user interface by navigating to MenuSettingsEnvironment.