Skip to content

Simple Button Acknowledgement Example

ringmybell edited this page Nov 29, 2020 · 4 revisions

Here is a simple example showing how messages flow from Node-Red to the UIbuilder Vue webpage and back to Node-Red on a user interaction.

In this example the message from Node-Red is simulating an alarm by changing the state of a variable from 0 to 1. In the webpage it receives the change in state and changes a button from green to red. When the user acknowledges the alarm by clicking the button it changes the variable from 1 back to 0, changes the color from red back to green and sends an update message to Node-Red so it knows a user interaction has occurred.

Paste the following Node-Red flow into your page.

Note I have included some MQTT nodes to show how simple it is to interface MQTT to the UIbuilder, but you will need to make sure you have MQTT broker and server setup on your computer for it to work. Delete these flows if not required, you can still build and deploy but it will give you an error (it can be ignored)

flow

[{"id":"1a7c5d0e.0fdc73","type":"uibuilder","z":"32910924.887cb6","name":"","topic":"","url":"uibuilder","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"showfolder":false,"x":1120,"y":1040,"wires":[["a39ed31d.bca6f"],[]]},{"id":"3403d92d.5c4176","type":"inject","z":"32910924.887cb6","name":"Alarm 1","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":350,"y":920,"wires":[["fec3d20f.90bba"]]},{"id":"fec3d20f.90bba","type":"function","z":"32910924.887cb6","name":"alarm1StatusOn","func":"msg.topic = \"alarm1Status\";\nmsg.payload = 1;\n\nreturn msg;","outputs":1,"noerr":0,"x":720,"y":920,"wires":[["1a7c5d0e.0fdc73","eb57a4c3.9770b8"]]},{"id":"a39ed31d.bca6f","type":"debug","z":"32910924.887cb6","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1310,"y":980,"wires":[]},{"id":"364bd0d0.f77b4","type":"comment","z":"32910924.887cb6","name":"Press to trigger alarm","info":"","x":360,"y":880,"wires":[]},{"id":"fe887baa.3db618","type":"inject","z":"32910924.887cb6","name":"Alarm 2","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":350,"y":980,"wires":[["1a7b62b6.219fad"]]},{"id":"1a7b62b6.219fad","type":"function","z":"32910924.887cb6","name":"alarm2StatusOn","func":"msg.topic = \"alarm2Status\";\nmsg.payload = 1;\n\nreturn msg;","outputs":1,"noerr":0,"x":720,"y":980,"wires":[["1a7c5d0e.0fdc73","eb57a4c3.9770b8"]]},{"id":"eb57a4c3.9770b8","type":"debug","z":"32910924.887cb6","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1310,"y":920,"wires":[]},{"id":"55baa5ee.7f793c","type":"comment","z":"32910924.887cb6","name":"Note the data format - msg.topic and msg.payload","info":"","x":730,"y":880,"wires":[]},{"id":"16d11a39.ea9dd6","type":"mqtt in","z":"32910924.887cb6","name":"","topic":"alarm1Status","qos":"2","datatype":"auto","broker":"","x":910,"y":1120,"wires":[["1a7c5d0e.0fdc73"]]},{"id":"a6317bd9.41d248","type":"comment","z":"32910924.887cb6","name":"You can feed MQTT node in directly - very cool","info":"","x":820,"y":1080,"wires":[]},{"id":"6a61d395.7b7b8c","type":"mqtt in","z":"32910924.887cb6","name":"","topic":"alarm2Status","qos":"2","datatype":"auto","broker":"","x":910,"y":1180,"wires":[["1a7c5d0e.0fdc73"]]},{"id":"909a0c6.28a6df","type":"mqtt out","z":"32910924.887cb6","name":"","topic":"alarm1Status","qos":"2","retain":"true","broker":"","x":710,"y":1120,"wires":[]},{"id":"11121be6.386bc4","type":"inject","z":"32910924.887cb6","name":"Alarm 1","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":350,"y":1120,"wires":[["bed91829.4e2bf8"]]},{"id":"50cb6c28.fe4ef4","type":"inject","z":"32910924.887cb6","name":"Alarm 2","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":350,"y":1180,"wires":[["a1aa55a8.870af8"]]},{"id":"2da46cbb.ce99e4","type":"mqtt out","z":"32910924.887cb6","name":"","topic":"alarm2Status","qos":"2","retain":"true","broker":"","x":710,"y":1180,"wires":[]},{"id":"bed91829.4e2bf8","type":"function","z":"32910924.887cb6","name":"alarm1StatusOn","func":"msg.payload = 1;\n\nreturn msg;","outputs":1,"noerr":0,"x":520,"y":1120,"wires":[["909a0c6.28a6df"]]},{"id":"a1aa55a8.870af8","type":"function","z":"32910924.887cb6","name":"alarm2StatusOn","func":"msg.payload = 1;\n\nreturn msg;","outputs":1,"noerr":0,"x":520,"y":1180,"wires":[["2da46cbb.ce99e4"]]},{"id":"92ddd40.ae8183","type":"comment","z":"32910924.887cb6","name":"You will need to download mqtt broker and set it up","info":"","x":830,"y":1220,"wires":[]},{"id":"2ff9727c.2f200e","type":"comment","z":"32910924.887cb6","name":"For debug viewer","info":"","x":1320,"y":880,"wires":[]}]

Add an instance of uibuilder to your flows and replace the index.html, index.css and index.js files with the code below.

index.html

<!doctype html>

<!-- *********** NOTE 1 - The following block almost a direct copy of from Julian's examples ************ -->
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">

    <title>Alarm Buttons with Acknowledgment</title>
    <meta name="description" content="Alarm Buttons with Acknowledgment">
    <link rel="icon" href="./images/node-blue.ico">

    <!-- 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="Alarm Buttons with Acknowledgment">

    <!-- 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="Alarm Buttons with Acknowledgment">

    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" />
    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" />
    <link type="text/css" rel="stylesheet" href="./index.css" media="all">

<!-- ************************** NOTE 1 - End of copied code block *******************************  -->  
    
</head>
<body>
    <div id="app" v-cloak>  <!--v-cloak hides the webpage until all vue content has been loaded -->
        <b-container id="app_container">
            <p class="textWhiteBoldLeft20px">Alarm Acknowledgement Example</p>   
            <table>     
                <tr><td class="textWhiteCentered16px">Alarm State. Click to Cancel Alarm</td></tr>  
                <tr><td><b-button v-bind:variant="alarm1Style" v-on:click="acknowledgeAlarm1()">
                            <b-icon v-bind:icon="alarm1Icon"></b-icon>&nbsp; Alarm 1    <!-- note that &nbsp;is just forcing a space between icon and text -->
                        </b-button></td></tr>  
                <tr><td><b-button :variant="alarm2Style" @click="acknowledgeAlarm2()">
                            <b-icon :icon="alarm2Icon"></b-icon>&nbsp; Alarm 2
                        </b-button></td></tr>  
            </table>
                     
                <!-- ******************** NOTE 2 - comments on above two lines *******************************  
                table, tr, td - Table commands, this is just so I can get a light grey box to sit over darker bagground,
                            just becase I think it looks nice, there are probably other ways to do this
                b-button -  This is the bootstrap version of the standard html button, it includes some nice features 
                            such as hover and focus pre setup. This is great if you want a nice looking button with
                            minimal CSS coding. 
                b-icon -    So we can use the bootstrap icons. NOTE - you need to include the script bootstrap-vue-icons.js
                            if you want to use the icons, they are not included by default 
                v-bind: -   This tells vue that it needs to bind this parameter (the button class) to a component (can 
                            be data, computed, watch or method hooks). This is saying to vue to keep an eye on this 
                            class because one of the compnents (data, computed, watch or method) is going to change its value. 
                            NOTE - you dont have to write "v-bind:", VUE allows a short cut and you can just use ":" which
                            can be confusing if you are looking at code and dont understand the shortcuts. 
                v-on: -     This tells VUE that on some action (in this case click) it needs to go to one of the 
                            components and do something. 
                            NOTE - you dont have to write "v-on:", VUE allows a short cut and you can just use "@" which
                            can be confusing if you are looking at code and dont understand the shortcuts.                                             
                -->  
        </b-container>
    </div>

    <!-- These MUST be in the right order. Note no leading / -->
    <!-- REQUIRED: Socket.IO is loaded only once for all instances without this, you don't get a websocket connection -->
    <script src="../uibuilder/vendor/socket.io/socket.io.js"></script>

    <!-- --- Vendor Libraries - Load in the right order --- -->
    <script src="../uibuilder/vendor/vue/dist/vue.js"></script> <!-- dev version with component compiler -->
    <!-- <script src="../uibuilder/vendor/vue/dist/vue.min.js"></script>   prod version with component compiler -->
    <!-- <script src="../uibuilder/vendor/vue/dist/vue.runtime.min.js"></script>   prod version without component compiler -->
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue-icons.js"></script> <!-- ********* TIP!! - If you want to include the Vue Bootstrap Icons then you need to Include this!!!!  -->
    
    <!-- REQUIRED: Sets up Socket listeners and the msg object -->
    <!-- <script src="./uibuilderfe.js"></script>   //dev version -->
    <script src="./uibuilderfe.min.js"></script> <!--    //prod version -->
    
    <!-- You need this file, it tells VUE how to handle input and output messages -->
    <script src="./index.js"></script>

</body>

</html>

index.js

/* jshint browser: true, esversion: 5, asi: true */
/*globals Vue, uibuilder */
// @ts-nocheck
/*
  Built on the great foundational work of others, free to use, but at your own risk
*/
'use strict'

/** @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods */

// eslint-disable-next-line no-unused-vars
var app1 = new Vue({
    el: '#app',
    
    
    // ******************************************************
    //                     DATA
    // If you want vue to control or react to data then it
    // MUST be declared here in the data section
    // ******************************************************     
    data: {
        alarm1Data   : 0,   //0 = no alarm green, 1 = alarm red. So we will set the initial alarm state to off so button will show green
        alarm2Data   : 0,       
    }, // --- End of data --- //
    
    
    // ******************************************************
    //                     COMPUTED
    // Best way to think about this. Computed are functions
    // that are trigged when some data changes. It requires
    // no input from the user. The data MUST be declared in
    // the data section or vue will ignore it. For example 
    // here we are setting the color of the button only based 
    // on the value of the vaiable alarmXData
    // ******************************************************     
    computed: {
        alarm1Style: function() {	                //this function will contol the button color using bootsrap inbuilt buttons
            if (this.alarm1Data === 0) {	        //if alarm is off then..., 
                return "success"                    //....colour green - in bootstrap success is green style	
            }
            else if (this.alarm1Data == 1) {        //if alarm is on then..., 		
                return "danger"                     //....colour red - in bootstrap warning is red style
            }    
        }, 
        
        alarm1Icon: function() {	                
            if (this.alarm1Data === 0) {	       
                return "check-circle"                    
            }
            else if (this.alarm1Data == 1) {        	
                return "exclamation-circle-fill"                  
            }    
        }, 

        alarm2Style: function() {	                
            if (this.alarm2Data === 0) {	        
                return "success"                    
            }
            else if (this.alarm2Data == 1) {        		
                return "danger"                     
            }    
        }, 
        
        alarm2Icon: function() {	               
            if (this.alarm2Data === 0) {	         
                return "check-circle"                   	
            }
            else if (this.alarm2Data == 1) {       	
                return "exclamation-circle-fill"                   
            }    
        },         
    }, // --- End of computed --- //
    

    // ******************************************************
    //                     METHODS
    // Best way to think about this. Methods are functions
    // that the user (or something else) starts, for example, 
    // we are saying here on a button click change the data variable 
    // alarmXData back to a non alarm condition and send some data 
    // back to Node-Red so the system knows the user has had
    // an interaction
    // ******************************************************     
    methods: {
        acknowledgeAlarm1: function(event) {        //this function is called when the button is clicked. If button is pressed and....
            console.log("Alarm 1 cancelled")           
            if (this.alarm1Data == 1) {             //...if alarm is currently triggered then.....
                this.alarm1Data = 0;                //...cancel the alarm and....
                uibuilder.send( {                   //...send message back to Node-Red saying this is what you did
                    'topic': "alarm1Acknowledge",
                    'payload': this.alarm1Data
                } )
            }
        },
                	
        acknowledgeAlarm2: function(event) {
            console.log("Alarm 2 cancelled")               
            if (this.alarm2Data == 1) {             
                this.alarm2Data = 0;
                uibuilder.send( {
                    'topic': "alarm2Acknowledge",
                    'payload': this.alarm2Data
                } )
            }
        },        	

    }, // --- End of methods --- //


    // ******************************************************
    //                     MOUNTED
    // This section will keep an eye out for incoming messages 
    // from Node-Red, you use the onChange method, then you
    // can filter by the message topics to decide what actions
    // you want to take
    // ****************************************************** 
    mounted: function(){

        uibuilder.start()
        var vueApp = this

        // Example of retrieving data from uibuilder
        vueApp.feVersion = uibuilder.get('version')

        uibuilder.onChange('msg', function(msg){
            if (msg.topic == "alarm1Status") {
                vueApp.alarm1Data  = parseInt(msg.payload)    
                console.log("Alarm 1 triggered")    //if you open web developer console, you can see the message coming in from node-Red when you click the send button
                //console.log(msg.topic)        
                //console.log(msg.payload) 
            }
            else if (msg.topic == "alarm2Status") {
                vueApp.alarm2Data  = parseInt(msg.payload)  
                console.log("Alarm 2 triggered") 
                //console.log(msg.topic)        
                //console.log(msg.payload) 
            }
        })

    } // --- End of mounted hook --- //

}) // --- End of app1 --- //

// EOF

index.css

/* Cloak elements on initial load to hide the possible display of {{ ... }} 
 * Add to the app tag or to specific tags
 * To display "loading...", change to the following:
 *    [v-cloak] > * { display:none }
 *    [v-cloak]::before { content: "loading…" }    
 */

[v-cloak] { display: none; }

body {
  background-color: rgb(51,51,51);
}

table, td, th {
  background-color:rgb(91,91,91);
  border: 5px solid rgb(91,91,91);
}

/* ----------- Setup text ----------- */
.textWhiteBoldLeft20px {
	color: white;
	font-size: 20px;
	font-weight: bold;
	text-align: left;
	font-family: Calibri, 'Gill Sans', 'Gill Sans MT', 'Trebuchet MS', sans-serif;	
}

.textWhiteCentered16px {
	color: white;
	font-size: 16px;
	text-align: center;
	font-family: Calibri, 'Gill Sans', 'Gill Sans MT', 'Trebuchet MS', sans-serif;	
}	
Clone this wiki locally