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:

  • the Begin node is used just to jump start the entire flow when the flow is deployed or node-red is restarted,
  • each Class Definition node contains a class that will generate a random number with the only requirement that the class implements at least a random() method call (perhaps there is a way to create an interface in node red such that each class node is supposed to implement the random() function (?)),
  • the Instantiation class uses the JavaScript eval function to dynamically create an instance of the random number generating class,
  • the Storage node defines the storage where an instance of the random number generating class will be stored,
  • Store uses flow.set() method call internal to Node Red in order to store the instantiated object in memory.

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:

  • Method Invoker is responsible for retrieving a stored instance of the engine and then generating a random number; the node calls .random() on a dynamically constructed object such that the requirement is that the classes define a random() method,
  • Normalize [0,1] is responsible for fitting the value generated by the clases within the $[0, 1]$ interval (perhps this could be parametrized further on),
  • the two link-in and link-out nodes should be linked to from other flows in order to process incoming requests for a random number,
  • timestamp and msg are meant just for testing

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.

Practical Debouncer / alarm(3)

Stacking up messages and picking the very first one whilst dropping the rest is possible via the "delay" node that is built-in. However, Node-Red lacks a way to simply queue up a series of messages and select just the last one. There are contributions available such as q-gate that seem very powerful, but this task is very simple to accomplishing using a "function" node and plain JavaScript.

Here is the code of a function node:

var timeout = flow.get('timeout')
clearTimeout(timeout)
timeout = setTimeout(() => {
     node.send(msg);
}, 2000)
flow.set('timeout', timeout)

that will take as input a message, delay it by $2000ms$ ($2s$), reset the delay in case another message comes through and iff. no new message arrives on the input in a timespan of $2s$ then the message is sent via the output.

Debouncers (or, as per programming) alarm(3) are frequently needed to buffer up redundant messages in order to select the very last one, or to be used as watchdog in order ensure the termination of a program.

Filesystem Notification Alternatives

When running node-red in Docker, with a remotely mounted filesystem, inotify does not work properly such that the file watcher nodes will be useless. There is an alternative on recent node-red distributions that will emulate inotify in a platform agnostic way.

In order to achieve the same effect, chokidar is used instead within a function node and the events are mapped to as many function outputs as necessary. The rest of the flow is trivial but the solution is flexible enough and uses built-in nodes without extra requirements except installing the node-red chokidar module.

[{"id":"f3d8802be8142018","type":"function","z":"ba25646f9e8f7a86","name":"Watch","func":"","outputs":3,"timeout":0,"noerr":0,"initialize":"var watcher = chokidar\n    .watch('/projects/iot-distribution-list/outbox/', \n        {\n           persistent: true,\n           ignoreInitial: true,\n           usePolling: true,\n           useFsEvents: false,\n           awaitWriteFinish: {\n               stabilityThreshold: 1000,\n               pollInterval: 100\n           },\n           depth: 1\n        }\n    )\n\nwatcher\n    .on('add', (path) => node.send([{ payload: path }, null, null]))\n    .on('change', (path) => node.send([null, { payload: path }, null]))\n    .on('unlink', (path) => node.send([null, null, { payload: path }]))\n","finalize":"","libs":[{"var":"chokidar","module":"chokidar"}],"x":430,"y":360,"wires":[["90cd598c3a083b64"],[],[]]},{"id":"90cd598c3a083b64","type":"debug","z":"ba25646f9e8f7a86","name":"debug 75","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":580,"y":320,"wires":[]}]

Storing Calendar Data Persistently with Redis

The dashboard_ui controls do not have persistent storage such that values should be stored such that the values will be reloaded when node-red restarts or reloads the flow. One interesting application is to use Redis and store the values due to how close the Redis syntax is to JavaScript. The example above, stores two dates labeled from and to to Redis and reloads the dates when node-red restarts of the flow is reloaded.

Here is the flow export:

[{"id":"4a61c1cd026b426f","type":"ui_date_picker","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"","label":"from","group":"a519123e0b16caf6","order":7,"width":0,"height":0,"passthru":false,"topic":"topic","topicType":"msg","className":"","x":910,"y":460,"wires":[["d093688e169826d0","f1979fb6c50cb285"]]},{"id":"6c49c6a5ef3ad975","type":"redis-command","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","server":"4475d6f04474e5ef","command":"SET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1320,"y":460,"wires":[[]]},{"id":"d093688e169826d0","type":"function","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"sensors/noise/date/from","func":"var key = `sensors/noise/date/from`\nmsg.payload = [key, msg.payload]\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1110,"y":460,"wires":[["6c49c6a5ef3ad975"]]},{"id":"af3c105ffa9ebfdf","type":"ui_date_picker","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"","label":"to","group":"a519123e0b16caf6","order":2,"width":0,"height":0,"passthru":false,"topic":"topic","topicType":"msg","className":"","x":910,"y":640,"wires":[["741a85153a105db9","2fe7dc514ebee4f5"]]},{"id":"741a85153a105db9","type":"function","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"sensors/noise/date/to","func":"var key = `sensors/noise/date/to`\nmsg.payload = [key, msg.payload]\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1100,"y":640,"wires":[["34acce6bac42a6c4"]]},{"id":"34acce6bac42a6c4","type":"redis-command","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","server":"4475d6f04474e5ef","command":"SET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1300,"y":640,"wires":[[]]},{"id":"98dc988b37729e28","type":"inject","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"sensors/noise/date/from","payloadType":"str","x":970,"y":400,"wires":[["6c27e62c2c4d01cb"]]},{"id":"6c27e62c2c4d01cb","type":"redis-command","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","server":"4475d6f04474e5ef","command":"GET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1180,"y":400,"wires":[["22f001a0a430acf4"]]},{"id":"3b54dae832bc6acd","type":"inject","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"sensors/noise/date/to","payloadType":"str","x":960,"y":580,"wires":[["4d95c2246275dcef"]]},{"id":"4d95c2246275dcef","type":"redis-command","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","server":"4475d6f04474e5ef","command":"GET","name":"","topic":"","params":"[]","paramsType":"json","payloadType":"json","block":false,"x":1160,"y":580,"wires":[["1f459178f7c7b890"]]},{"id":"f1979fb6c50cb285","type":"change","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"Set From","rules":[{"t":"set","p":"noiseFrom","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":920,"y":520,"wires":[[]]},{"id":"2fe7dc514ebee4f5","type":"change","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"Set To","rules":[{"t":"set","p":"noiseTo","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":910,"y":700,"wires":[[]]},{"id":"22f001a0a430acf4","type":"function","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"convertDate","func":"msg.payload = moment(new Date(parseInt(msg.payload))).format(\"MM/DD/YYYY\")\nreturn msg\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"moment","module":"moment"}],"x":1350,"y":400,"wires":[["4a61c1cd026b426f"]]},{"id":"1f459178f7c7b890","type":"function","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"convertDate","func":"msg.payload = moment(new Date(parseInt(msg.payload))).format(\"MM/DD/YYYY\")\nreturn msg\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"moment","module":"moment"}],"x":1330,"y":580,"wires":[["af3c105ffa9ebfdf"]]},{"id":"a519123e0b16caf6","type":"ui_group","name":"Noise","tab":"9cd692a098605f26","order":1,"disp":true,"width":"24","collapse":false,"className":""},{"id":"4475d6f04474e5ef","type":"redis-config","name":"docker","options":"{\"port\":6379,\"host\":\"docker.internal\"}","cluster":false,"optionsType":"json"},{"id":"9cd692a098605f26","type":"ui_tab","name":"Sensors","icon":"dashboard","disabled":false,"hidden":false}]

Displaying High-Performance Charts with HighCharts and Hardware Acceleration

One of the main problem governing chart displays in JavaScript, in general, is that a large number of points tends to low down the browser considerably. In effect, a few hundred thousand points will not be able to be rendered by the browser. Whether they are rendered by software such as Grafana or Kibana, the result is that the browser will slow down to a crawl just plotting the points, rendering most such software useless. The typical recommendations are to partition the data and reduce the amount of points such that they can be rendered by the browser.

Fortunately, HighCharts has a "boost" module that allows charts to be rendered using the GPU such that HighCharts is able to display graphs with up to millions of points. Using HighCharts in node-red is fairly trivial, depending on the desired degree of integration. The following is a node-red setup that uses HighCharts and the associated modules to plot a chart that will allow millions of points to be plotted and then even inspected with tooltip popups without any slowdown.

The flow is easy to understand:

  • a dashboard_ui button is used to fetch data from an SQLite database,
  • the data is enriched with chart settings,
  • the data is fed into the Noise node that contains the JavaScript code for HighCharts

In parallel, the JavaScript Includes template node is responsible for injecting a few JavaScript scripts into the header of the page whenever the dashboard is reloaded.

Here is the code within the JavaScript Includes node:

<script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/3.4.4/luxon.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/data.js"></script>
<script src="https://code.highcharts.com/modules/boost.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<script src="https://code.highcharts.com/modules/export-data.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
<script src="https://code.highcharts.com/highcharts-more.js"></script>

The code for the chart display is the following:

<div id="myNoiseChart"></div>
 
<script>
    var myNoiseChart = null
 
    function myChartCreate(msg) {
        myNoiseChart = Highcharts.chart('myNoiseChart', {
            chart: {
                zoomType: 'xy'
            },
            title: {
                text: msg.title
            },
            subtitle: {
                text: msg.subtitle             
            },
            boost: { 
                useGPUTranslations: true,
                seriesThreshold: 1
            },
            xAxis: [{
                type: 'datetime',
                labels: {
                    format: '{value:%Y-%b-%e %H:%M}'
                }
            }],
            yAxis: {
                title: {
                    text: msg.units
                }
            },
            series: [
                {
                    name: 'Noise',
                    type: 'scatter',
                    name: msg.series[0].title,
                    color: 'rgba(0, 0, 0, .1)',
                    data: msg.payload,
                    marker: {
                        radius: 1
                    }
                }
            ]
        })
    }
 
    function myChartUpdate(msg) {
        myNoiseChart.series[0].setData(msg.payload)
        myNoiseChart.redraw()
    }
</script>
 
<script>
    (function(scope) {
        scope.$watch('msg', function(msg) {
            if (msg) {
                if(myNoiseChart != null) {
                    myChartUpdate(msg)
                    return
                }
                myChartCreate(msg)
            }
        });
    })(scope);
</script>

along with the node export of the chart settings:

[{"id":"469a5673b0e44655","type":"change","z":"e1af369bbb851062","g":"f87472e0d03fc4d0","name":"Chart Settings","rules":[{"t":"set","p":"title","pt":"msg","to":"Noise Measurement","tot":"str"},{"t":"set","p":"subtitle","pt":"msg","to":"alt. 30m, reach 30cm","tot":"str"},{"t":"set","p":"units","pt":"msg","to":"db","tot":"str"},{"t":"set","p":"series","pt":"msg","to":"[{\"title\":\"Noise dB, ε=±1.5\"}]","tot":"json"},{"t":"set","p":"error","pt":"msg","to":"1.5","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1200,"y":860,"wires":[["c8bb05bfb43fc056"]]}]

The chart is configured to plot time (x-axis) against data (y-axis) with the particularity that using hardware acceleration sets the chart in "turbo" mode such that the payload should consist of a large array containing small arrays of two elements representing the time and data tuple. Here is an example data array containing some sample data to be plotted:

[
    [ 
        "1722843000",
        5
    ],
    [
        "1722843600",
        8
    ],
    [
        "1722844200",
        4
    ],
 
  ...
 
]

fuss/node-red.txt · Last modified: 2024/08/09 01:36 by office

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.