Skip to content

Basic uibuilder RIOT example displaying values, switch and select box

Colin Law edited this page Oct 26, 2017 · 5 revisions

Here is an example using RIOT with uibuilder to show values from MQTT (or elsewhere) and send on messages from a switch and select box.

In the dependencies section of ~/.node-red/package.json add (along with whatever you normally have):

"dependencies": {
   "node-red-contrib-uibuilder": "*",
   "riot": "^3.7.3"
 }

In the module.exports part of your settings.js file, add this:

 uibuilder: {
   userVendorPackages: ['riot'],
   debug: false
 }

In ~/.node-red run

npm install node-red-contrib-uibuilder riot

Restart node-red.
Copy the flow below and import it into a tab in node-red, and deploy. Browse to <ip>:1880/home you should see the default uibuilder welcome page, and deploying it should have created a ~/.node-red/uibuilder/home folder, and in the src subfolder, it should have created index.css, index.html, index.js and manifest.json. If you want a url other than home then change it in the uibuilder node and re-deploy.

Flow:

[{"id":"7be939b5.af86c8","type":"uibuilder","z":"251597c3.8396e8","name":"","topic":"","url":"home","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"debugFE":false,"x":310.5,"y":163,"wires":[["dbee3905.275c7"]]},{"id":"c3d15ed.7ce93a","type":"debug","z":"251597c3.8396e8","name":"","active":true,"console":"false","complete":"payload","x":662,"y":76,"wires":[]},{"id":"e52b1aa4.1090e8","type":"inject","z":"251597c3.8396e8","name":"topic/one On","topic":"topic/one","payload":"On","payloadType":"str","repeat":"","crontab":"","once":false,"x":397,"y":354,"wires":[["738e30b9.71b4e"]]},{"id":"b42be742.a8c12","type":"inject","z":"251597c3.8396e8","name":"topic/one Off","topic":"topic/one","payload":"Off","payloadType":"str","repeat":"","crontab":"","once":false,"x":401,"y":385,"wires":[["738e30b9.71b4e"]]},{"id":"dbee3905.275c7","type":"switch","z":"251597c3.8396e8","name":"","property":"command","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","outputs":2,"x":448.5,"y":163,"wires":[["738e30b9.71b4e","c3d15ed.7ce93a"],["4f0391b0.b04868"]]},{"id":"4f0391b0.b04868","type":"function","z":"251597c3.8396e8","name":"Topic repeater","func":"// Given messages containing topic/payload values this saves the latest payload for each topic\n// and passes the message on.\n// If it receives a message with msg.command set to 'reload' it retransmits each saved pair as\n// individual messages.\n// If it receives a message with msg.command set to 'reset' it removes all saved data\nif (msg.command && msg.command === 'reload') {\n    // a reload message so re-send all messages saved\n    var keys = context.keys();\n    for (var i = 0; i < keys.length; i++) {\n        node.send({topic: keys[i], payload: context.get(keys[i])});\n    }\n    msg = null;\n} else if (msg.command && msg.command === 'reset') {\n    // a reset command so remove all saved messages\n    var keys = context.keys();\n    for (var i = 0; i < keys.length; i++) {\n        context.set(keys[i], undefined);\n    }\n    msg= null;\n} else {\n    // a normal message so add/update the payload for this topic, provided there is a topic\n    if (msg.topic) {\n        context.set(msg.topic, msg.payload);\n    }\n}\n// pass on the message unless it has been nulled\nreturn msg;\n","outputs":"1","noerr":0,"x":338.5,"y":265,"wires":[["7be939b5.af86c8"]]},{"id":"1bceb41a.73e694","type":"inject","z":"251597c3.8396e8","name":"topic/two 123.456","topic":"topic/two","payload":"123.456","payloadType":"num","repeat":"","crontab":"","once":false,"x":419,"y":440,"wires":[["738e30b9.71b4e"]]},{"id":"faab15e6.f82ec8","type":"inject","z":"251597c3.8396e8","name":"topic/two -73.5","topic":"topic/two","payload":"-73.5","payloadType":"num","repeat":"","crontab":"","once":false,"x":409,"y":472,"wires":[["738e30b9.71b4e"]]},{"id":"1c8cf52.85b7a0b","type":"link in","z":"251597c3.8396e8","name":"Simulated  MQTT In","links":["738e30b9.71b4e"],"x":83.5,"y":264,"wires":[["4f0391b0.b04868"]]},{"id":"738e30b9.71b4e","type":"link out","z":"251597c3.8396e8","name":"Simulated MQTT Out","links":["1c8cf52.85b7a0b"],"x":737.5,"y":136,"wires":[]},{"id":"a4c7955b.911278","type":"comment","z":"251597c3.8396e8","name":"Simulated MQTT Out","info":"","x":664.5,"y":184,"wires":[]},{"id":"1911ff6b.e20249","type":"comment","z":"251597c3.8396e8","name":"Simulated MQTT In","info":"","x":95,"y":209,"wires":[]}]

Replace index.html with the following:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">

  <!-- See https://goo.gl/OOhYW5 -->
  <link rel="manifest" href="manifest.json">
  <meta name="theme-color" content="#3f51b5">

  <!-- Used if adding to homescreen for Chrome on Android. Fallback for manifest.json -->
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="application-name" content="Node-RED UI Builder">

  <!-- Used if adding to homescreen for Safari on iOS -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <meta name="apple-mobile-web-app-title" content="Node-RED UI Builder">

  <!-- Homescreen icons for Apple mobile use if required
      <link rel="apple-touch-icon" href="/images/manifest/icon-48x48.png">
      <link rel="apple-touch-icon" sizes="72x72" href="/images/manifest/icon-72x72.png">
      <link rel="apple-touch-icon" sizes="96x96" href="/images/manifest/icon-96x96.png">
      <link rel="apple-touch-icon" sizes="144x144" href="/images/manifest/icon-144x144.png">
      <link rel="apple-touch-icon" sizes="192x192" href="/images/manifest/icon-192x192.png">
  -->

  <title>Node-RED UI Builder</title>
  <meta name="description" content="Node-RED UI Builder">

  <link rel="icon" href="images/node-blue.ico">

  <!-- OPTIONAL: Normalize is used to make things the same across browsers. Index is for your styles -->
  <link rel="stylesheet" href="vendor/normalize.css/normalize.css">
  <link rel="stylesheet" href="index.css">
</head>
<body>
  <!-- the riot application tag contains everything -->
  <application></application>

  <!-- Socket.IO is loaded only once for all instances -->
  <script src="/uibuilder/socket.io/socket.io.js"></script>

  <!-- include riot.js and the compiler Note no leading / -->
  <script type="text/javascript" src="vendor/riot/riot+compiler.min.js"></script>

  <script src="uibuilderfe.min.js"></script>
  <script src="index.js"></script>
  <!-- application and other tags are specified in external files -->
  <script data-src="dashboard-tags.tag" type="riot/tag"></script>
  <script data-src="application-tags.tag" type="riot/tag"></script>
</body>
</html>

Replace index.js with:

/*global document,window,uibuilder */

/**
 *   uibuilder: The main global object containing the following...
 *     Methods:
 *       .onChange(attribute, callbackFn) - listen for changes to attribute and execute callback when it changes
 *       .get(attribute)        - Get any available attribute
 *       .set(attribute, value) - Set any available attribute (can't overwrite internal attributes)
 *       .msg                   - Shortcut to get the latest value of msg. Equivalent to uibuilder.get('msg')
 *       .send(msg)             - Shortcut to send a msg back to Node-RED manually
 *       .debug(true/false)     - Turn on/off debugging
 *       .uiDebug(type,msg)     - Utility function: Send debug msg to console (type=[log,info,warn,error,dir])
 *     Attributes with change events (only accessible via .get method except for msg)
 *       .msg          - Copy of the last msg sent from Node-RED over Socket.IO
 *       .sentMsg      - Copy of the last msg sent by us to Node-RED
 *       .ctrlMsg      - Copy of the last control msg received by us from Node-RED (Types: ['shutdown','server connected'])
 *       .msgsReceived - How many standard messages have we received
 *       .msgsSent     - How many messages have we sent
 *       .msgsCtrl     - How many control messages have we received
 *       .ioConnected  - Is Socket.IO connected right now? (true/false)
 *     Attributes without change events
 *           (only accessible via .get method, reload page to get changes, don't change unless you know what you are doing)
 *       .debug       - true/false, controls debug console logging output
 *       ---- You are not likely to need any of these ----
 *       .version     - check the current version of the uibuilder code
 *       .ioChannels  - List of the channel names in use [uiBuilderControl, uiBuilderClient, uiBuilder]
 *       .retryMs     - starting retry ms period for manual socket reconnections workaround
 *       .retryFactor - starting delay factor for subsequent reconnect attempts
 *       .ioNamespace - Get the namespace from the current URL
 *       .ioPath      - make sure client uses Socket.IO version from the uibuilder module (using path)
 *       .ioTransport - ['polling', 'websocket']
 */

// this is needed if you want to bundle everything up with webpack
if (typeof require != "undefined") {
  var uibuilder = require('node-red-contrib-uibuilder/nodes/src/uibuilderfe.js')
  var riot = require('riot')
  require("./index.css")
  require("./application-tags.tag")
}

// wait for the document to be loaded
window.onload = function() {
    // mount all the tags
    riot.mount('*')

    // Catch new messages from node red
    uibuilder.onChange('msg', function(newMsg){
      // all we need to do is tell riot to update all the data
      riot.update()
    })

    // Catch Socket.IO connect/disconnect events, connected will be true or false
    uibuilder.onChange('ioConnected', function(connected){
      if (connected) {
        // Send a reload message to indicate a browser window
        // has opened so all the topics will be reloaded.
        // Since we are using riot we also have to do that when all the tags
        // have been mounted, in the top level tag on mount (application-tags.tag)
        // as here may be too early if it is the initial connect. It won't matter
        // that this is done twice
        uibuilder.send({command: 'reload', payload: 'Browser connected'})
      }
      riot.update()
    })

}

Create the file application-tags.tag which contains the application:

if (typeof require != "undefined") {
  var uibuilder = require('node-red-contrib-uibuilder/nodes/src/uibuilderfe.js')
  require("./dashboard-tags.tag")
}
// define the tag that contains everything
<application>
  <div id="app">
    <h4>{this.ioConnected}</h4>

    <!-- a text value driven by a topic -->
    <p>Text value, topic/one: <dblt-value topic="topic/one"></dblt-value></p>
    <!-- a numeric value driven by a topic, displayed to two decimal places -->
    <p>Numeric value, topic/two: <dblt-value topic="topic/two" dp="2"></dblt-value></p>
    <!-- a switch with current value -->
    <p>Switch, current value: <dblt-value topic="topic/three"></dblt-value>&nbsp; <dblt-switch topic="topic/three"></dblt-switch></p>

    <!-- a dropdown selection, first define the values and text -->
    <script>
      this.mode_selections = [
        {value: '0', text: 'Off'},
        {value: '1', text: 'Intermittent'},
        {value: '2', text: 'On'}
      ]
    </script>
    <div>Mode: <dblt-select topic = "topic/four" selections = {mode_selections}></dblt-select></div>

  </div>
  <script>
    // this is done when riot.update() is called, we just have to refresh any data that
    // is shown by the application tag
    this.on('update', function() {
      this.ioConnected = uibuilder.get("ioConnected") ? "Connected" : "Disconnected"
    })

    // include this in just the top level tag of the application
    this.on('mount', function() {
      // The application tab is mounted, so all its children should be, send a reload
      // message to get node-red to send us the latest values
      uibuilder.send({command: 'reload', payload: 'Browser connected'})
      this.ioConnected = uibuilder.get("ioConnected") ? "Connected" : "Disconnected"
      this.update()
     })

  </script>
</application>

And finally, dashboard-tags.tag which contains the definition of the low level objects:

if (typeof require != "undefined") {
  var uibuilder = require('node-red-contrib-uibuilder/nodes/src/uibuilderfe.js')
}

<!--
  Given a topic this displays the current payload value for that topic
  If dp is provided then the value is coerced to a number and then formatted with
  the specified number of decimal places
-->
<dblt-value>
  <span>{this.payload || "."}</span>

  <script>
  this.on('update', function() {
    var msg = uibuilder.get("msg")
    if (msg.topic == opts.topic) {
      this.payload = opts.dp ? Number(msg.payload).toFixed(opts.dp) : msg.payload
    }
  })
  </script>
</dblt-value>

<!--
  Given a topic and an array of selections this displays a select box with the
  text for the value passed in the payload as the current selection. If the user
  changes the selection that the value is sent in a message with the topic.
  If the extras option is provided then the sent message will containg the property extras
  with the value given. This can be useful, for example, for specifying alternative MQTT servers.
  The selections array can be setup and the tag invoked using something like
  <script>
    this.mode_selections = [
      {value: '0', text: 'Off'},
      {value: '1', text: 'Intermittent'},
      {value: '2', text: 'On'}
    ]
  </script>
  <dblt-select topic = "some/topic" selections = {mode_selections}></dblt-select>
-->
<dblt-select>
  <select value={selected} onchange={ selection_changed } >
    <option each={ item in opts.selections } value={item.value}>{item.text}</option>
  </select>

  <script>
    this.selected = ''

    this.on('update', function() {
      var msg = uibuilder.get("msg")
      if (msg.topic == opts.topic) {
        this.selected = msg.payload
      }
    })

    this.selection_changed = function(e) {
      this.selected = e.target.value
      uibuilder.send({topic: opts.topic, payload: e.target.value, extras: opts.extras})
    }

  </script>
</dblt-select>

<!--
  Given a topic this displays a switch showing the state from the payload value for that topic
  The default expected is "On" and "Off" payload values.
  If the switch is clicked then a message is sent with the specified topic and the payload
  containing the new state.
  Options on and off may be provided to provide alternative on/off strings
  If the extras option is provided then the sent message will containg the property extras
  with the value given. This can be useful, for example, for specifying alternative MQTT servers.
-->
<dblt-switch>
  <label class="switch">
    <input type="checkbox" checked="{this.checked}" onclick={switch_clicked}>
      <span class="slider round"></span>
    </input>
  </label>

  <script>
  this.on_text = opts.on || "On"
  this.off_text = opts.off || "Off"
  this.on('update', function() {
    var msg = uibuilder.get("msg")
    if (msg.topic == opts.topic) {
      this.checked = msg.payload === this.on_text
    }
  })

  this.switch_clicked = function(e) {
    this.checked = !this.checked
    uibuilder.send({topic: opts.topic, payload: (this.checked ? this.on_text : this.off_text), extras: opts.extras})
  }
  </script>

  <style>
  /* The switch - the box around the slider */
  .switch {
    position: relative;
    display: inline-block;
    width: 36px;
    height: 20px;
  }

  /* Hide default HTML checkbox */
  .switch input {display:none;}

  /* The slider */
  .slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ccc;
    -webkit-transition: .4s;
    transition: .4s;
  }

  .slider:before {
    position: absolute;
    content: "";
    height: 15px;
    width: 15px;
    left: 3px;
    bottom: 3px;
    background-color: white;
    -webkit-transition: .2s;
    transition: .2s;
  }

  input:checked + .slider {
    background-color: #2196F3;
  }

  input:focus + .slider {
    box-shadow: 0 0 1px #2196F3;
  }

  input:checked + .slider:before {
    -webkit-transform: translateX(15px);
    -ms-transform: translateX(15px);
    transform: translateX(15px);
  }

  /* Rounded sliders */
  .slider.round {
    border-radius: 34px;
  }

  .slider.round:before {
    border-radius: 50%;
  }

  </style>
</dblt-switch>

Refresh the browser you should be up and running.

I have been very surprised how much can be achieved with so little code, with uibuilder (thanks to Julian Knight) and riot doing all the hard work in the background.

One issue that had to be overcome is that when the browser is refreshed or initially connected (or when another browser is connected) that there is no history so the initial states of all the controls will be unknown. To overcome this, in index.js when the socket connection is made it sends a message with msg.command set. In the flow this is sensed by the switch node and is sent on to the Topic repeater function node. This node is designed to remember the latest payload values for all topics sent to it. When it sees the reload command it re-sends a message for each topic, which goes to the uibuilder node and populates the controls with the latest values. There is one final twist to this in that it is possible for the connection to be made before riot has fully instantiated all the tags, in which case the messages may arrive too early. To cope with this, the reload message is also sent from the on mount handler in the top level tag (the application tag). On mount is called for the top level tag after all child tags have been instantiated so at that time all controls will be populated. I decided to include both calls as, if successful, the first call will populate them quicker and the fact that the messages will be sent twice is of little, if any, consequence.

If anyone who knows more about css than I do would like to provide the css for a fancier looking switch then please do.

Experimental - Add tags dynamically

This covers some experiments that allow tags to be added dynamically from the node-red flow by sending messages to the uibuilder node. This is not adding new types of tags, just adding instances of existing tags to the page at run time.

Edit 26 Oct 17 Added the ability to remove the tag from a placeholder, and also introduced a technique that populates the tag with the current value (if any) when it is added. The placeholder tag and flow below have been updated. The changes to the flow are in the Topic Repeater function node.

The feature is enabled using the placeholder node type, so the first thing to do is to add this to the file dashboard-tags.tag.

<!--
  This is a placeholder tag within which a tag of any type may be instantiated
  at run time by sending the uibuilder node a message.
  Configure the placeholder tag with a topic that the message will have and
  in the message set that topic and the payload to and object containing the
  key "tagtype" set to the type of tag to be instantiated. Any other attributes
  in the payload will be passed on to the instantiated type.
  For example include in the application
  <placeholder topic="ph001"></placeholder>
  then send it a message
  {topic: "ph001", payload: {tagtype: "dblt-value", topic: "topic/one"}}
  If another message with topic ph001 is passed to the uibuilder node then
  the existing dynamic tag will be removed and a new one created as defined by
  the new message.
  To remove the tag from the placeholder pass tagtype as an empty string, or tagtype 
  not present.
-->
<placeholder>
  <script>
    var mounted = false
    var tag = null
    this.on('update', function() {
      var msg = uibuilder.get("msg")
      if (msg.topic == opts.topic) {
        // if this is a reload operation, so as a result of browser refresh or possibly as a result of this or
        // another tag being mounted by code below, then ignore it unless we are not currently mounted
        if (msg.reloading  &&  this.mounted) {
          // this is a reloading message and we are mounted so ignore it
          // did the logic this way as it is much easier to understand
        } else {
          if (this.mounted) {
            this.root.removeChild(this.root.childNodes[0])
            this.mounted = false
          }
          var payload = msg.payload
          // if tagtype is empty string or not present then leave the placeholder empty
          if (payload.tagtype) {
            tag = document.createElement(payload.tagtype)
            for (var k in payload) {
              if (payload.hasOwnProperty(k)) {
                if (payload[k] !== "tagtype") {
                  tag.setAttribute(k, payload[k])
                }
              }
            }
            this.root.appendChild(tag)
            riot.mount(tag, msg.payload.tagtype)
            this.mounted = true
            uibuilder.send({command: 'reload', payload: 'Refreshing ' + opts.topic})
          }
        }
      }
    })
  </script>
</placeholder>

The comments at the front explain (hopefully) reasonable well how to use it. To demonstrate its use add this to application-tags.tag before the closure of the app div.

    <p>Placeholder ph001:</p>
    <placeholder topic="ph001"></placeholder>
    <p>Placeholder ph002:</p>
    <placeholder topic="ph002"></placeholder>

and use this flow

[{"id":"c9355873.32d038","type":"inject","z":"251597c3.8396e8","name":"ph001 clear","topic":"ph001","payload":"{\"tagtype\":\"\"}","payloadType":"json","repeat":"","crontab":"","once":false,"x":375,"y":562,"wires":[["738e30b9.71b4e"]]},{"id":"738e30b9.71b4e","type":"link out","z":"251597c3.8396e8","name":"Simulated MQTT Out","links":["1c8cf52.85b7a0b"],"x":737.5,"y":136,"wires":[]},{"id":"e52b1aa4.1090e8","type":"inject","z":"251597c3.8396e8","name":"topic/one On","topic":"topic/one","payload":"On","payloadType":"str","repeat":"3","crontab":"","once":false,"x":401,"y":309,"wires":[["738e30b9.71b4e"]]},{"id":"b42be742.a8c12","type":"inject","z":"251597c3.8396e8","name":"topic/one Off","topic":"topic/one","payload":"Off","payloadType":"str","repeat":"5","crontab":"","once":false,"x":405,"y":340,"wires":[["738e30b9.71b4e"]]},{"id":"dbee3905.275c7","type":"switch","z":"251597c3.8396e8","name":"","property":"command","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","outputs":2,"x":448.5,"y":163,"wires":[["738e30b9.71b4e"],["4f0391b0.b04868","c3d15ed.7ce93a"]]},{"id":"1bceb41a.73e694","type":"inject","z":"251597c3.8396e8","name":"topic/two 123.456","topic":"topic/two","payload":"123.456","payloadType":"num","repeat":"","crontab":"","once":false,"x":413,"y":395,"wires":[["738e30b9.71b4e"]]},{"id":"faab15e6.f82ec8","type":"inject","z":"251597c3.8396e8","name":"topic/two -73.5","topic":"topic/two","payload":"-73.5","payloadType":"num","repeat":"","crontab":"","once":false,"x":403,"y":427,"wires":[["738e30b9.71b4e"]]},{"id":"e35e46f3.62c5d8","type":"inject","z":"251597c3.8396e8","name":"ph001, dblt-value, topic/one","topic":"ph001","payload":"{\"tagtype\":\"dblt-value\",\"topic\":\"topic/one\"}","payloadType":"json","repeat":"","crontab":"","once":false,"x":426,"y":489,"wires":[["738e30b9.71b4e"]]},{"id":"d2fc81e1.c67a4","type":"inject","z":"251597c3.8396e8","name":"ph001, dblt-switch, topic/one","topic":"ph001","payload":"{\"tagtype\":\"dblt-switch\",\"topic\":\"topic/one\"}","payloadType":"json","repeat":"","crontab":"","once":false,"x":428,"y":521,"wires":[["738e30b9.71b4e"]]},{"id":"a425e584.4a747","type":"inject","z":"251597c3.8396e8","name":"ph002, dblt-value, topic/two","topic":"ph002","payload":"{\"tagtype\":\"dblt-value\",\"topic\":\"topic/two\",\"extras\":\"somestring\"}","payloadType":"json","repeat":"","crontab":"","once":false,"x":710,"y":485,"wires":[["738e30b9.71b4e"]]},{"id":"7be939b5.af86c8","type":"uibuilder","z":"251597c3.8396e8","name":"","topic":"","url":"home","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"debugFE":false,"x":310.5,"y":163,"wires":[["dbee3905.275c7"]]},{"id":"4f0391b0.b04868","type":"function","z":"251597c3.8396e8","name":"Topic repeater","func":"// Given messages containing topic/payload values this saves the latest payload for each topic\n// and passes the message on.\n// If it receives a message with msg.command set to 'reload' it retransmits each saved pair as\n// individual messages, except that if msg.exclude_topics is provided then it should contain\n// an array of topic strings and those topics will not be resent\n// If it receives a message with msg.command set to 'reset' it removes all saved data\nif (msg.command && msg.command === 'reload') {\n    // a reload message so re-send all messages saved\n    var keys = context.keys();\n    for (var i = 0; i < keys.length; i++) {\n        node.send({topic: keys[i], payload: context.get(keys[i]), reloading: true});\n    }\n    msg = null;\n} else if (msg.command && msg.command === 'reset') {\n    // a reset command so remove all saved messages\n    var keys = context.keys();\n    for (var i = 0; i < keys.length; i++) {\n        context.set(keys[i], undefined);\n    }\n    msg= null;\n} else {\n    // a normal message so add/update the payload for this topic, provided there is a topic\n    if (msg.topic) {\n        context.set(msg.topic, msg.payload);\n    }\n}\n// pass on the message unless it has been nulled\nreturn msg;\n","outputs":"1","noerr":0,"x":338.5,"y":265,"wires":[["7be939b5.af86c8"]]},{"id":"1c8cf52.85b7a0b","type":"link in","z":"251597c3.8396e8","name":"Simulated  MQTT In","links":["738e30b9.71b4e"],"x":83.5,"y":264,"wires":[["4f0391b0.b04868"]]},{"id":"a4c7955b.911278","type":"comment","z":"251597c3.8396e8","name":"Simulated MQTT Out","info":"","x":664.5,"y":184,"wires":[]},{"id":"1911ff6b.e20249","type":"comment","z":"251597c3.8396e8","name":"Simulated MQTT In","info":"","x":95,"y":209,"wires":[]}]

This provides four extra inject nodes to the flow given earlier. The left hand three inject a dblt-value tag or a dblt-switch tag into placeholder ph001 and clear ph001, and the fourth injects a dblt-value into placeholder ph002

Colin Law

Clone this wiki locally