Node-RED Installer

From Open Source Controls Wiki
Jump to navigation Jump to search
Selecting Mods to install into Node-RED

The controls architecture is built around Node-RED and Docker, and the Node-RED Installer is the method used to set everything up.

The concept is as follows:

  1. A startup flow is loaded into Node-RED and deployed.
  2. This flow pulls the latest files from the GitHub repository, and creates a Docker container running Node-RED on port 5099, on which it installs a Node-RED Setup flow, with access to all credentials.
  3. The setup flow provides a menu system to select the required Application (Node-RED flows) and Mods (additional Node-RED flows to be added to the application).
  4. As items are selected, they are installed, along with any missing node types and credentials, onto the original Node-RED on port 1880.
  5. Additional Node-RED containers, on ports 5001+, are started to implement isolated services such as data management, that may need separate access rights or updating.
  6. The setup container is closed.


In essence, the Node-RED Installer gives Node-RED the ability to program itself, and create new instances of Node-RED or any other software.


This method has a number of advantages:

  • Entirely handled by Node-RED. Other systems are used by Node-RED, but using the same platform to orchestrate everything makes things more user friendly.
  • A standard simple Node-RED flow can be used as a starting point to install any system.
    https://github.com/heatweb/plumbing-controller/blob/main/flows/flows_install_installer.json
  • Node-RED provides a powerful method to manage containers, with the ability to spin up temporary containers based on logic, running any services needed to boost the systems abilities.
  • Credentials for both Node-RED elements and Docker containers can all be managed by the single setup container, that is killed once the system is up.
  • Better updating control, with the ability to build (and rebuild) custom Node-RED flows from scratch, from a large selection of flow pages that each provide a specific function.
  • Updating is performed live, without closing down existing flows. Variables are kept in memory during updates. The system can then be rebooted (if required) to clear out old variables in memory, but is generally not needed for minor updates or adding functions.
  • Easier maintenance, with a set of maintained core flows (and starting points), expanded and customised as required.
  • Manufacturers can develop their own flows adapted for different functions, place them into the GitHub repository, and make available to controllers at setup.
  • Improved reliability, with services isolated in separate containers. Redundancy can be built into the architecture.
  • Complicated setup procedures, including systems scripts, can be maintained in a Node-RED flow (stored on GitHub), deployed as needed in privileged containers, and removed.

Prerequisites:

  • Node-RED on port 1880 (with Dashboard nodes installed)
  • Docker

Credentials

Credentials are generated during installation of Docker containers., and are stored in /boot/heatweb/credentials folder as individual txt files (name is the cred id, content is the cred).


The following are standard credentials.

  • adminPassword
  • remoteAdminCommand
  • localInfluxToken
  • remoteInfluxServer
  • remoteInfluxBucket
  • remoteInfluxToken
  • localMqttPassword
  • emailServer
  • emailPort
  • emailUser
  • emailPassword


Container Details

The Installer and Composer are run inside a container running Node-RED in Alpine Linux.

It is required to install sudo and to add node-red to the permissions to run sudo without a password prompt.

This has already been done in the heatweb/noderedsetup Docker container. (https://hub.docker.com/repository/docker/heatweb/noderedsetup).

Shell into noderedsetup container (in Portainer) as root user.

apk update
apk add git
git config --global --add safe.directory /home/pi/plumbing-controller
apk --no-cache add sudo
visudo

Add the line

node-red ALL=(ALL:ALL) NOPASSWD: ALL

Press Esc followed by :wq and Enter

Check with:

sudo -lU node-red

Composer

It is possible to build up a Node-RED installation from the various modules using the Installer.

The Composer allows one to pre-define the list of modules to install, the order to install in, and targets.

A Composer file is a JSON file that contains an array of instructions.

A Composer file allows installations to be rebuilt using the latest module versions and as such is an update function.

  • Deploy complete flow
  • Deploy individual flow - if duplicates then duplicate/overwrite/keep/fail
  • Apply changes before deploying
  • Remove a flow
  • Remove a node
  • Update source from Git
  • Copy file
  • Set Boot Config
  • Set Boot Credential
  • Backup all flows
  • Backup all flows, config & credentials
  • Run system command

Composer JSON

The following is an example application, installing the flows_beta_plumbing_controller_1880.json flow, followed by three mods, with a change of serial port on the last mod. The flow_dhw_dashboard.json mod is linked to a specific version in the GitHub history, to prevent it from updating.

[
    {
        "dataSource": "https://raw.githubusercontent.com/heatweb/plumbing-controller/main/flows/flows_beta_plumbing_controller_1880.json",
        "targetHost": "localhost",
        "targetPort": 1880,
        "action": "flows",
        "config": {
            "description": "DHW HIU Controller v2"
        },
        "credentials": {
            "remoteInfluxServer": "https://europe-west1-1.gcp.cloud2.influxdata.com"
        }
    },
    {
        "dataSource": "https://raw.githubusercontent.com/heatweb/plumbing-controller/main/flows/mods/flow_dhw_control.json",
        "targetHost": "localhost",
        "targetPort": 1880,
        "action": "flow"
    },
    {
        "dataSource": "https://raw.githubusercontent.com/heatweb/plumbing-controller/4b6cd4a366e082fe15963598baf85b3893d58d19/flows/mods/flow_dhw_dashboard.json",
        "targetHost": "localhost",
        "targetPort": 1880,
        "action": "flow"
    },
    {
        "dataSource": "https://raw.githubusercontent.com/heatweb/plumbing-controller/main/flows/mods/flow_modbus_novocon.json",
        "targetHost": "localhost",
        "targetPort": 1880,
        "action": "flow",
        "replacements": [
            [
                "ttyS0",
                "ttyAMA4"
            ]
        ]
    }
]

Node-RED

[{"id":"d45724a5b9c70467","type":"tab","label":"Composer","disabled":false,"info":"","env":[]},{"id":"5c0a883d29582acd","type":"inject","z":"d45724a5b9c70467","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"[{\"dataSource\":\"https://raw.githubusercontent.com/heatweb/plumbing-controller/main/flows/flows_beta_plumbing_controller_1880.json\",\"targetHost\":\"localhost\",\"targetPort\":1880,\"action\":\"flows\",\"config\":{\"name\":\"Docker Development Controller v2\"},\"credentials\":{\"remoteInfluxServer\":\"https://europe-west1-1.gcp.cloud2.influxdata.com\"}},{\"dataSource\":\"https://raw.githubusercontent.com/heatweb/plumbing-controller/main/flows/mods/flow_dhw_control.json\",\"targetHost\":\"localhost\",\"targetPort\":1880,\"action\":\"flow\"},{\"dataSource\":\"https://raw.githubusercontent.com/heatweb/plumbing-controller/4b6cd4a366e082fe15963598baf85b3893d58d19/flows/mods/flow_dhw_dashboard.json\",\"targetHost\":\"localhost\",\"targetPort\":1880,\"action\":\"flow\"},{\"dataSource\":\"https://raw.githubusercontent.com/heatweb/plumbing-controller/main/flows/mods/flow_modbus_novocon.json\",\"targetHost\":\"localhost\",\"targetPort\":1880,\"action\":\"flow\",\"replacements\":[[\"ttyS0\",\"ttyAMA4\"]]}]","payloadType":"json","x":190,"y":100,"wires":[["a66678b2ea23686e"]]},{"id":"a66678b2ea23686e","type":"change","z":"d45724a5b9c70467","name":"","rules":[{"t":"set","p":"composer.default","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":500,"y":100,"wires":[[]]},{"id":"c333189a999e8742","type":"inject","z":"d45724a5b9c70467","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"default","payloadType":"str","x":190,"y":600,"wires":[["72309de8a79eeadc"]]},{"id":"72309de8a79eeadc","type":"function","z":"d45724a5b9c70467","name":"Composer","func":"\nvar composition = global.get(\"composer.\" + msg.payload);\n\nif (!composition) { return null; }\n\nflow.set(\"composition\",composition);\nflow.set(\"fetched\", 0);\n\nvar auth = flow.get(\"auth\") || {};\n\nfor (var item in composition) {\n\n    var msg1={};\n    msg1.url = composition[item].dataSource;\n    msg1.index = item;\n    //node.send([msg1,null]);\n\n    var msg2 = null;\n\n    var targetPort = msg.payload.targetPort || 1880;\n    var targetHost = msg.payload.targetHost || \"localhost\";\n    targetHost = targetHost.replace(\"localhost\", \"host.docker.internal\")\n\n    var targ = targetHost + \":\" + targetPort;\n    if (!auth[targ]) {         \n\n        auth[targ]=\"waiting\";\n        flow.set(\"auth\",auth);\n\n        msg2 = {}\n        msg2.targ = targ;\n        msg2.payload = {};\n        msg2.payload['client_id'] = \"node-red-admin\";\n        msg2.payload['grant_type'] = \"password\";\n        msg2.payload['scope'] = \"*\";\n        msg2.payload['username'] = \"admin\";\n        msg2.payload['password'] = global.get(\"credentials.adminPassword\") || \"admin\";\n        msg2.url = \"http://\" + targ + \"/auth/token\";\n        \n    }\n\n    node.send([msg1, msg2]);\n\n}\n\nreturn null;","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":600,"wires":[["986cee236e456e68"],["824ea4cdf5383c85"]]},{"id":"5daabdcf26ba0523","type":"function","z":"d45724a5b9c70467","name":"flows","func":"\n\nif (msg.payload.config) { \n   \n   for (var cf in msg.payload.config) {  global.set(\"config.\" + cf, msg.payload.config[cf]);     }\n   \n   node.send([null, null, msg, null]);\n\n}\nif (msg.payload.credentials) {\n\n   for (var cf in msg.payload.credentials) { global.set(\"credentials.\" + cf, msg.payload.credentials[cf]); }\n\n   node.send([null, null, null, msg]);\n\n}\n\nif (!msg.payload.action) { return [msg,null,null,null]; }\n\n\n\nvar targetPort = msg.payload.targetPort || 1880;\nvar targetHost = msg.payload.targetHost || \"localhost\";\ntargetHost = targetHost.replace(\"localhost\",\"host.docker.internal\")\nvar targ = targetHost + \":\" + targetPort;\n\nvar auth = flow.get(\"auth\") || {};\n\n\nmsg.headers = {};\nmsg.headers.Authorization = \"Bearer \" + auth[targ];\nmsg.headers['Content-type'] = \"application/json\";\nmsg.headers['Node-RED-Deployment-Type'] = \"full\";\n\n\nmsg.url = \"http://\" + targ + \"/\" + (msg.payload.action||\"flow\");\n\n//var cfrom = '\"broker\":\"localhost\"';\n//var cto = '\"broker\":\"localhost\", \"credentials\":{\"username\":\"nodereduser\", \"password\":\"' + global.get(\"noderedmqttpass\") + '\"}';\n\nvar credentials = global.get(\"credentials\")||{};\nvar config = global.get(\"config\") || {};\n\n//var ff = msg.payload;\nvar ff = msg.payload.data;\n\nif (msg.payload.replacements) {\n\n   var ffstr = JSON.stringify(ff);\n\n   var reps = msg.payload.replacements;\n   for (var rep in reps) {\n\n      ffstr = ffstr.replaceAll(reps[rep][0], reps[rep][1]);\n   }\n\n   ff = JSON.parse(ffstr);\n}\n\n\nfunction checkTab(nodeitem) {\n   return nodeitem.type == \"tab\";\n}\n\nfunction filterTab(nodeitem) {\n   return nodeitem.type != \"tab\";\n}\n\nfunction filterMqttBroker(nodeitem) {\n   return nodeitem.type != \"mqtt-broker\";\n}\n\nif (msg.payload.action == \"flow\") {   // individual flows can't contain tabs, and do not want to overwrite mqtt broker\n\n   var tabf = ff.filter(checkTab)[0];\n   tabf.nodes = ff.filter(filterTab).filter(filterMqttBroker);\n   ff = tabf;\n\n}\n\n\nfor (var part in ff) {\n\n   if (ff[part].type == \"heatwebConfig\") { \n      \n      ff[part].name = config.name ; \n      ff[part].description = config.description;\n      ff[part].nodeId = config.nodeId;\n      ff[part].networkId = config.networkId; \n\n   }\n   \n\n   if (credentials.localMqttPassword) {\n\n      if (ff[part].type == \"mqtt-broker\" && (ff[part].broker == \"mqtt\" || ff[part].broker == \"localhost\")) { ff[part].credentials = { \"user\": \"admin\", \"password\": credentials.localMqttPassword }; }\n    \n   }\n\n   if (credentials.adminPassword) {\n\n      if (ff[part].type == \"MySQLdatabase\" && ff[part].host == \"mysql\") { ff[part].credentials = { \"user\": \"root\", \"password\": credentials.adminPassword }; }\n\n   }\n\n   if (credentials.localInfluxToken) {\n\n      if (ff[part].type == \"influxdb\" && ff[part].name == \"local\") { ff[part].credentials = { \"token\": credentials.localInfluxToken }; }\n\n   }\n   if (credentials.remoteInfluxToken) {\n\n      if (ff[part].type == \"influxdb\" && ff[part].name == \"heatweb\") { ff[part].credentials = { \"token\": credentials.remoteInfluxToken }; }\n\n   }\n\n   if (credentials.emailPassword && credentials.emailUser) {\n\n      if (ff[part].type == \"e-mail\") { ff[part].credentials = { \"userid\": credentials.emailUser, \"password\": credentials.emailPassword }; }\n\n   }\n\n   \n\n\n}\n\nmsg.payload = JSON.stringify(ff);\n\nreturn [null, msg, null, null];","outputs":4,"noerr":0,"initialize":"","finalize":"","libs":[],"x":472,"y":806,"wires":[["df5cf78bb5e02533"],["c80016af5fd0c71d"],["0ac93462eb366e2e"],["a05d78c74f90b1fb"]]},{"id":"f4bdd8a482d1cebf","type":"http request","z":"d45724a5b9c70467","name":"","method":"POST","ret":"txt","url":"","tls":"","x":850,"y":800,"wires":[["df5cf78bb5e02533","7af6011b014b1971","24d72b1de9de0132"]]},{"id":"24d72b1de9de0132","type":"function","z":"d45724a5b9c70467","name":"save results","func":"var composition = flow.get(\"composition\");\n\nif (!composition) { return null; }\n\n\ncomposition[msg.index].deployed = msg.payload;\n\n\n\n\n// if (msg.filename) {\n\n//     var fn = msg.filename.replace(/\\//g, \"_\").replace(/\\./g, \"_\");\n//     global.set(\"results.\"+fn, JSON.parse(msg.payload));\n\n// }\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1050,"y":800,"wires":[[]]},{"id":"986cee236e456e68","type":"http request","z":"d45724a5b9c70467","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":650,"y":580,"wires":[["14b1e8754eecdaaf"]]},{"id":"14b1e8754eecdaaf","type":"function","z":"d45724a5b9c70467","name":"compile","func":"\nvar composition = flow.get(\"composition\");\n\nif (!composition) { return null; }\n\ntry {\n\n    composition[msg.index].data = JSON.parse(msg.payload);\n\n} \ncatch (err) {\n\n    if (msg.payload[0]) { composition[msg.index].data = msg.payload; }\n\n}\n\nflow.set(\"composition\", composition);\n\nvar fetched = flow.get(\"fetched\");\nfetched++;\nflow.set(\"fetched\", fetched);\n\nif (fetched < composition.length) { return null; }\n\nflow.set(\"posted\", 0);\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":820,"y":580,"wires":[["3f74b3674bd337f4","4c65e48178c25e22"]]},{"id":"df5cf78bb5e02533","type":"function","z":"d45724a5b9c70467","name":"shift","func":"var posted = flow.get(\"posted\") || 0;\nvar composition = flow.get(\"composition\");\n\n//if (posted = composition.length) { return null; }\n\nvar msg1 = {};\nmsg1.payload = composition[posted]; //.data;\nmsg1.index = 1*posted;\n\nposted++;\nflow.set(\"posted\", posted);\n\n\nif (!msg1.payload) { return null; }\n\nreturn msg1;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":670,"y":720,"wires":[["5daabdcf26ba0523","9fcd3db98350446e"]]},{"id":"9fcd3db98350446e","type":"debug","z":"d45724a5b9c70467","name":"debug 30","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":860,"y":720,"wires":[]},{"id":"3f74b3674bd337f4","type":"debug","z":"d45724a5b9c70467","name":"debug 31","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1000,"y":580,"wires":[]},{"id":"7af6011b014b1971","type":"debug","z":"d45724a5b9c70467","name":"debug 32","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1020,"y":860,"wires":[]},{"id":"916f81d06dcecf50","type":"http request","z":"d45724a5b9c70467","name":"","method":"POST","ret":"txt","url":"","tls":"","x":400,"y":1040,"wires":[["ca67a6abcd19c97b","f6720a2a1363cb64"]]},{"id":"46a3af1257eda101","type":"json","z":"d45724a5b9c70467","name":"","pretty":false,"x":750,"y":1020,"wires":[["4616f775197c9bee"]]},{"id":"ca67a6abcd19c97b","type":"debug","z":"d45724a5b9c70467","name":"debug 33","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":570,"y":980,"wires":[]},{"id":"f6720a2a1363cb64","type":"switch","z":"d45724a5b9c70467","name":"","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"access_token","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":580,"y":1040,"wires":[["46a3af1257eda101"],["69729a475a7416e4"]]},{"id":"097f147bfa40c445","type":"comment","z":"d45724a5b9c70467","name":"Node-RED authentication","info":"","x":230,"y":960,"wires":[]},{"id":"01e6f04422e315bb","type":"inject","z":"d45724a5b9c70467","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":220,"wires":[["5da31f3d874fcd4c"]]},{"id":"5da31f3d874fcd4c","type":"file in","z":"d45724a5b9c70467","name":"","filename":"/boot/heatweb/config.json","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":430,"y":220,"wires":[["03689db0a1229d7d"]]},{"id":"60014b30ddf0f588","type":"change","z":"d45724a5b9c70467","name":"","rules":[{"t":"set","p":"config","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":220,"wires":[[]]},{"id":"03689db0a1229d7d","type":"json","z":"d45724a5b9c70467","name":"","property":"payload","action":"obj","pretty":false,"x":630,"y":220,"wires":[["60014b30ddf0f588"]]},{"id":"307de46a7f2b649a","type":"inject","z":"d45724a5b9c70467","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":300,"wires":[["c38ffe5bfd0dd27d","1d3bfec916ba0477"]]},{"id":"c38ffe5bfd0dd27d","type":"file in","z":"d45724a5b9c70467","name":"","filename":"/boot/heatweb/credentials.json","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":490,"y":300,"wires":[["ad93699b4f67b45e"]]},{"id":"fe3ce37e31818309","type":"change","z":"d45724a5b9c70467","name":"","rules":[{"t":"set","p":"credentials","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":300,"wires":[[]]},{"id":"ad93699b4f67b45e","type":"json","z":"d45724a5b9c70467","name":"","property":"payload","action":"obj","pretty":false,"x":710,"y":300,"wires":[["fe3ce37e31818309"]]},{"id":"68123565cd072358","type":"fs-file-lister","z":"d45724a5b9c70467","name":"","start":"/boot/heatweb/credentials","pattern":"*.txt","folders":"*","hidden":true,"lstype":"files","path":true,"single":false,"depth":"1","stat":false,"showWarnings":true,"x":600,"y":360,"wires":[["7d19b9387d19b985","51f46724bf3c8081"]]},{"id":"7d19b9387d19b985","type":"debug","z":"d45724a5b9c70467","name":"debug 34","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":360,"wires":[]},{"id":"1d3bfec916ba0477","type":"delay","z":"d45724a5b9c70467","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":420,"y":360,"wires":[["68123565cd072358"]]},{"id":"f651936739ae9faf","type":"file in","z":"d45724a5b9c70467","name":"","filename":"filename","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":960,"y":400,"wires":[["d3a616ac9f624f5f"]]},{"id":"51f46724bf3c8081","type":"change","z":"d45724a5b9c70467","name":"","rules":[{"t":"set","p":"filename","pt":"msg","to":"payload","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":400,"wires":[["f651936739ae9faf"]]},{"id":"d3a616ac9f624f5f","type":"function","z":"d45724a5b9c70467","name":"store creds","func":"\nvar fn = msg.filename.substr(msg.filename.lastIndexOf(\"/\")+1);\nfn = fn.substr(0,fn.lastIndexOf(\".\"));\n\nglobal.set(\"credentials.\" + fn, msg.payload.trim());\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1130,"y":400,"wires":[["373d7ae5fa6b6648"]]},{"id":"824ea4cdf5383c85","type":"link out","z":"d45724a5b9c70467","name":"link out 5","mode":"link","links":["bdcf1ddb23718ba9"],"x":535,"y":620,"wires":[]},{"id":"bdcf1ddb23718ba9","type":"link in","z":"d45724a5b9c70467","name":"link in 5","links":["824ea4cdf5383c85"],"x":235,"y":1040,"wires":[["7b9a3510ab4a4db7","916f81d06dcecf50"]]},{"id":"7b9a3510ab4a4db7","type":"debug","z":"d45724a5b9c70467","name":"debug 35","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":380,"y":1100,"wires":[]},{"id":"4616f775197c9bee","type":"function","z":"d45724a5b9c70467","name":"store auth","func":"\n\n\nif (msg.targ) {\n\n    var auth = flow.get(\"auth\") || {};\n    auth[msg.targ] = msg.payload.access_token;\n    flow.set(\"auth\", auth);\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":920,"y":1020,"wires":[[]]},{"id":"69729a475a7416e4","type":"function","z":"d45724a5b9c70467","name":"store auth","func":"\n\n\nif (msg.targ) {\n\n    var auth = flow.get(\"auth\") || {};\n    auth[msg.targ] =  \"\";\n    flow.set(\"auth\", auth);\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":1080,"wires":[[]]},{"id":"4c65e48178c25e22","type":"delay","z":"d45724a5b9c70467","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":820,"y":640,"wires":[["df5cf78bb5e02533"]]},{"id":"8f4761e8bbf1c25e","type":"function","z":"d45724a5b9c70467","name":"Save Credentials","func":"var credentials = global.get(\"credentials\")||{};\n\n// credentials[msg.topic] = msg.payload;\n\n// flow.set(\"credentials\", credentials) || {};\n\nmsg.payload = JSON.stringify(credentials);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":1280,"wires":[["3afe877aa42a27b6"]]},{"id":"3afe877aa42a27b6","type":"file","z":"d45724a5b9c70467","name":"","filename":"/data/credentials.json","filenameType":"str","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"none","x":640,"y":1280,"wires":[["c74d59824e3eb207"]]},{"id":"c74d59824e3eb207","type":"exec","z":"d45724a5b9c70467","command":"sudo mv /data/credentials.json /boot/heatweb/credentials.json","addpay":"","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"","x":1060,"y":1280,"wires":[["08990be77f2da38b"],["08990be77f2da38b"],[]]},{"id":"f7b22618409dca7f","type":"link in","z":"d45724a5b9c70467","name":"link in 6","links":["a05d78c74f90b1fb","da933dfab46e29b5"],"x":245,"y":1280,"wires":[["8f4761e8bbf1c25e"]]},{"id":"a05d78c74f90b1fb","type":"link out","z":"d45724a5b9c70467","name":"link out 6","mode":"link","links":["f7b22618409dca7f"],"x":585,"y":900,"wires":[]},{"id":"0ac93462eb366e2e","type":"link out","z":"d45724a5b9c70467","name":"link out 7","mode":"link","links":["a8aa406333da0583"],"x":635,"y":860,"wires":[]},{"id":"27b89804e5b4427b","type":"function","z":"d45724a5b9c70467","name":"Save config","func":"var config = global.get(\"config\")||{};\n\n// credentials[msg.topic] = msg.payload;\n\n// flow.set(\"credentials\", credentials) || {};\n\nmsg.payload = JSON.stringify(config);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":1200,"wires":[["f4dcfe02b18d8c57"]]},{"id":"f4dcfe02b18d8c57","type":"file","z":"d45724a5b9c70467","name":"","filename":"/data/config.json","filenameType":"str","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"none","x":620,"y":1200,"wires":[["79af6b0a3a958ecb"]]},{"id":"79af6b0a3a958ecb","type":"exec","z":"d45724a5b9c70467","command":"sudo mv /data/config.json /boot/heatweb/config.json","addpay":"","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"","x":1030,"y":1200,"wires":[["61bdb9532553a113"],["61bdb9532553a113"],[]]},{"id":"a8aa406333da0583","type":"link in","z":"d45724a5b9c70467","name":"link in 7","links":["0ac93462eb366e2e"],"x":245,"y":1200,"wires":[["27b89804e5b4427b"]]},{"id":"c80016af5fd0c71d","type":"delay","z":"d45724a5b9c70467","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":670,"y":800,"wires":[["f4bdd8a482d1cebf"]]},{"id":"61bdb9532553a113","type":"debug","z":"d45724a5b9c70467","name":"debug 36","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1420,"y":1200,"wires":[]},{"id":"08990be77f2da38b","type":"debug","z":"d45724a5b9c70467","name":"debug 37","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1420,"y":1280,"wires":[]},{"id":"373d7ae5fa6b6648","type":"trigger","z":"d45724a5b9c70467","name":"","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"250","extend":true,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":1320,"y":400,"wires":[["da933dfab46e29b5"]]},{"id":"da933dfab46e29b5","type":"link out","z":"d45724a5b9c70467","name":"link out 8","mode":"link","links":["f7b22618409dca7f"],"x":1445,"y":400,"wires":[]}]