Button with function in .js file

Hello everyone,

I'm having a headache trying to wrap my head arround using button on a node. In my js file, I got a function created in the node description:

function EcobeePinRequest(config) {
		RED.nodes.createNode(this, config);
		this.name = config.name;
		var node = this;
		var ecobeeController = RED.nodes.getNode(config.controller);
		var itemName = config.itemname;

function pinRequest(config) {
			node.log("entered");
			console.log("entered");
}
	}
	RED.nodes.registerType("ecobee-pinrequest", EcobeePinRequest);

In the html file, I got this:

<script type="text/javascript">
    $.getScript('static/js/bootstrap-multiselect.js');
    RED.nodes.registerType('ecobee-pinrequest', {
        category: 'ecobee',
        color: '#00cc00',
        defaults: {
            name:      {value:""},
		    controller:  {value:"", type:"ecobee-controller", required:true}
        },
        inputs: 0,
        outputs: 0,
        //icon: "node-red-contrib-openhab2.png",
        paletteLabel: "pin-request",
        label: function() {

            return(this.name||this.itemname||"ecobee pin request");
        },
		button: {
			onclick: function() {
				console.log("entered button");
				node.log("entered button");
				pinRequest(RED.nodes.node(node.controller));
			}
		}
    });
</script>

Now, nothing get logged when I press the button. I though it might be because pinRequest isn't in the html file, so I created one in javascript to see in the html file:

<script type="text/javascript">
    	function pinRequest(cntrlConfig) {
            if ( cntrlConfig ) {
            	var config = {
           			name: cntrlConfig.name,
           			clientID: cntrlConfig.clientID,
           			accessToken: cntrlConfig.accessToken,
           			refreshToken: cntrlConfig.refreshToken
            	}
            	console.log("config = " + JSON.stringify(config));
               
				cntrlConfig.pinRequest(cntrlConfig);
            }else{
			console.log("failed");
			}
		}
</script>

still, nothing ever get logged. In fact, even just trying to do a console.log or node.log in the onclick action doesn't do anything. Can anyone help me? Best case, I would like to trigger a function from the js file.

Thank you!

Hello,
Multiple things come to mind.
When you do a console.log() from the html this will happen client side and not server side so you will only see it in the developer console of the browser and not in the logs of the machine running nodered.
If you want to trigger anything server side in your js file from the front end html file you will have to create an endpoint for the communication. You will than have to use jquery or ajax requests from your html to communicate with the that endpoint you defined in you js file.
A good example for endpoints a with a button is the core inject node here:


and here:

Johannes

2 Likes

Hello, thank you for the reply. I was reading from the inject file late last night. I'm just unsure how it work, I stopped working with HTML before ajax and jquery.

My understanding. In the js file, which is backend, there is this line:

RED.httpAdmin.post("/inject/:id", RED.auth.needsPermission("inject.write"), function(req,res) {

This is kind of creating a "web socket listener" that I can access with an html file to trigger a query, by going to /inject/:id where :id, in this instance, is the calling node id. When triggering that, the function(req,res) is triggered. I see this function have a req and a res value. req seems to be an instance of the current request received, where params are any parameter passed (req.params.id = this.is in this instance). Here, it's used to return the answer to this node.

On the html side, I see in the button these lines:

$.ajax({
url: "inject/"+this.id,
type:"POST",
success: function(resp) {
RED.notify(node.("inject.success",{label:label}),{type:"success",id:"inject"});
},
error: function(jqXHR,textStatus,errorThrown) {
if (jqXHR.status == 404) {
RED.notify(node.
("common.notification.error",{message:node.("common.notification.errors.not-deployed")}),"error");
} else if (jqXHR.status == 500) {
RED.notify(node.
("common.notification.error",{message:node.("inject.errors.failed")}),"error");
} else if (jqXHR.status == 0) {
RED.notify(node.
("common.notification.error",{message:node.("common.notification.errors.no-response")}),"error");
} else {
RED.notify(node.
("common.notification.error",{message:node._("common.notification.errors.unexpected",{status:jqXHR.status,message:textStatus})}),"error");
}
}
});

I understand the first 2 params, where it's the method to send and the url to get, defined from the httpadmin earlier. But where is the actual data send? Right after, it's reading the answer from resp.

Thank you

They pass 4 parameters to the $.ajax function:

  • url
  • type
  • success: callback function that will be called in case the server has returned a correct resp(onse).
  • error: callback function that will be called in case the server has returned an error
1 Like

oh yeah, didn't see properly there was 4 parm. I'm "toying" with it right now and seeing more and more how it work. I have another question though.

Right now, it pass url: "inject/"+this.id, where, where this.id is the current node id that is then used in the js file to find it with var node = RED.nodes.getNode(req.params.id).

That's great. Now I want the controller of that node also. So I thouh, why not just do node.controller in the js, but this return undefined. If, in the html, I do "ecoControl = RED.nodes.node(this.controller);", I get the controller.

So I'm wondering why in the js, I can't get the property controller of the node.

Thanks again!

Sorrt, I have heard of that controller. What do you want to do (with it) on the server side?

1 Like

The controller will contain an api key, an access token and a refresh token. It will also host function to request and refresh api and tokens. The controller is a configuration node. My current node are as follow:

ecobee.js

module.exports = function(RED) {
   
   RED.httpAdmin.post("/ecobee/:id", function(req,res) {
        var node = RED.nodes.getNode(req.params.id);
        if (node != null) {
            try {
				node.warn(node.controller);
				node.warn("Got request");
                //node.receive();
                res.sendStatus(200);
            } catch(err) {
				node.warn("test2");
                res.sendStatus(500);
                node.error(RED._("inject.failed",{error:err.toString()}));
            }
        } else {
			node.warn("test3");
            res.sendStatus(404);
        }
    });

	function EcobeeControllerNode(config) {
		RED.nodes.createNode(this, config);

		this.getConfig = function () {
			return config;
		}

		var node = this;
	}
	
    RED.nodes.registerType("ecobee-controller", EcobeeControllerNode);


function EcobeePinRequest(config) {
		RED.nodes.createNode(this, config);
		this.name = config.name;
		var node = this;
		var ecobeeController = RED.nodes.getNode(config.controller);
		var itemName = config.itemname;
		node.log("controller: " + ecobeeController.name);
	}
	RED.nodes.registerType("ecobee-pinrequest", EcobeePinRequest);

}
ecobee.html
<script type="text/x-red" data-template-name="ecobee-controller">
    <div class="form-row">
        <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-config-input-name" placeholder="Name">
    </div>
	<div class="form-row">
        <label for="node-config-input-clientID"><i class="fa fa-tag"></i> Client ID</label>
        <input type="text" id="node-config-input-clientID">
    </div>
	<div class="form-row">
        <label for="node-config-input-accessToken"><i class="fa fa-tag"></i> Access Token</label>
        <input type="text" id="node-config-input-accessToken">
    </div>
	<div class="form-row">
        <label for="node-config-input-refreshToken"><i class="fa fa-tag"></i> Refresh Token</label>
        <input type="text" id="node-config-input-refreshToken">
    </div>
</script>


<script type="text/x-red" data-template-name="ecobee-pinrequest">
    <style type="text/css">
        .btn-group {
            width: 70%;
        }
        .multiselect {
            width: 100%;
        }
        .form-row input.multiselect-search {
            width: 100%;
        }
        .multiselect-clear-filter {
            display: none;
        }
        .dropdown-menu {
            width: 100% !important;
        }
        .multiselect-container input[type="checkbox"] {
            display: none;
        }
        .multiselect-container > li > a > label.radio {
            margin: 5px;
            width: 90%;
            height: 100%;
            cursor: pointer;
            font-weight: 400;
            padding: 3px 20px 3px 20px;
        }
        .multiselect-container label.radio input[type="radio"] {
            display: none;
        }
    </style>
    <link rel="stylesheet" href="static/css/bootstrap-multiselect.css" type="text/css" />
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
    <div class="form-row">
        <label for="node-input-controller"><i class="fa fa-globe"></i> Controller</label>
        <input type="text" id="node-input-controller">
    </div>
</script>


<script type="text/x-red" data-help-name="ecobee-controller">
    <p>Configuration node for communication with an ecobee controller.</p>
	<p></p>
	<b>Configuration</b>
    <ul>
        <li><b>Name :</b> Specify a name for the configuration node</li>
        <li><b>Client ID :</b> The API Key generated on the my developper portal</li>
        <li><b>Access Token :</b> Generated after ecobee Pin used for authentication </li>
        <li><b>Refresh Token :</b> Used for refresh</li>
   	 </ul>
</script>

<script type="text/x-red" data-help-name="ecobee-pinrequest">
    <p>Used to request a pin to enter in the ecobee portal.</p>
	<p>Log in to Ecobee.com and go to your "My Apps" and click "Add Application" before clicking the button on the node</p>
	<p>The ecobeePin must be entered within 10 minutes in ecobee.com</p>
	<p>Once the pin received, enter it in the My Apps window</p>
	<p>The node can be deleted afterward and serve no other purpose, unless you need to reauthorize again</p>
	<b>Configuration</b>
    <ul>
        <li><b>Name :</b> Optionally specify a name</li>
        <li><b>Controller :</b> Select the ecobee controller</li>
   	 </ul>
	<p></p>
</script>

<script type="text/javascript">
	RED.nodes.registerType('ecobee-controller', {
		category: 'config',
		defaults: {
			name: {value:"",required:true},
			clientID: {value:"",required:true},
			accessToken	: {value:"",	required:false /*,	validate:RED.validators.number() */},
			refreshToken: {value:"",	required:false}
        },
        paletteLabel: "ecobee-controller",
		label: function() {
			return this.name;
		}
	});
</script>

<script type="text/javascript">
    $.getScript('static/js/bootstrap-multiselect.js');
    RED.nodes.registerType('ecobee-pinrequest', {
        category: 'ecobee',
        color: '#00cc00',
        defaults: {
            name:      {value:""},
		    controller:  {value:"", type:"ecobee-controller", required:true}
        },
        inputs: 0,
        outputs: 0,
        //icon: "node-red-contrib-openhab2.png",
        paletteLabel: "pin-request",
        label: function() {

            return(this.name||this.itemname||"ecobee pin request");
        },
		button: {
			onclick: function() {
				ecoControl = RED.nodes.node(this.controller);
				var label = this._def.label.call(this);
                if (label.length > 30) {
                    label = label.substring(0,50)+"...";
                }
                label = label.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
                var node = this;

				$.ajax({
                    url: "ecobee/"+this.id,
                    type:"POST",
                    success: function(resp) {
                        RED.notify(node._("inject.success",{label:label}),{type:"success",id:"inject"});
                    },
                    error: function(jqXHR,textStatus,errorThrown) {
                        if (jqXHR.status == 404) {
                            RED.notify(node._("common.notification.error",{message:node._("common.notification.errors.not-deployed")}),"error");
                        } else if (jqXHR.status == 500) {
                            RED.notify(node._("common.notification.error",{message:node._("inject.errors.failed")}),"error");
                        } else if (jqXHR.status == 0) {
                            RED.notify(node._("common.notification.error",{message:node._("common.notification.errors.no-response")}),"error");
                        } else {
                            RED.notify(node._("common.notification.error",{message:node._("common.notification.errors.unexpected",{status:jqXHR.status,message:textStatus})}),"error");
                        }
                    }
                });
			}
		}
    });
</script>

So for now, the only thing it does is at flow runtime, EcobeePinRequest output in the server console the name of the controller. When I press the button, I get a inject success notification and 2 message in the debug, first one is undefined (cause no controller property) and the other "got request". I used node.warn to see them in the nodered debug screen in the browser.

Right now, I'm mixing concept from openhab2 binding and 20-inject. And I think I'm going the wrong way.

My goal is to create nodes for ecobee.Since my understanding of the nodes is getting better, I'm thinking about switching my approach.

What I was planning. For ecobee, we need to request a pin code using and api key. This is what ecobeePinRequest node was created for. I wanted a button that take the controller.clientID property and send the request. The request will get an answer with a pin code that I must then output somewhere.

Afterward, that pin code must be entered on the ecobee api screen within 10 minutes. Because of that, I cannot automate that step.

Afterward, a request must be made to get an accesstoken and a refresh token, and these need to be refresh at a certain period.

After, with the tokens ready, I can create other node to query and set different property on the ecobee using these tokens from the controller.

openhab2 nodes use this approach with a controller having the url, ports and user. That's why I have the config param in the constructer. Now I just need to find a way to use it when coming from ajax.

edit: to provide way more information

I manage to get way farther now. I have 2 different scenario, both with the same problem in the end though.

In my configuration (ecobeeController) node, I have 4 property:

  • clientID
  • ecobeePin
  • accessToken
  • refreshToken

I have another node "pinRequest" that when I press a button, it does the operation to get the pin and access token. This work well. I'm even able to set the ecobeePin and accessToken value in the js. The problem is these value aren't followed in the html side nor the GUI. So if I edit the configuration node, it's empty. Is there a way to write to these value from the js?

THank you

Hi,
Sorry for the delay but have been away all day...

Some remarks about the code:

  • I assume that the tokens need to be private? You have declared the tokens as normal node fields, which means that all nodes in the flow can read them (even e.g. malicious nodes). Moreover when somebody ever exports an example flow - containing your nodes - the exported flow will contain the tokens, which means they will be shared with everybody importing the flow... Better to declare those tokens as credential fields (see here).

  • You create a function to return the config:

    function EcobeeControllerNode(config) {
       RED.nodes.createNode(this, config);
    
       this.getConfig = function () {
          return config;
       }
    

    This will work perfectly I assume, but most of the time I see people only storing your own fields in the server side node:

    function EcobeeControllerNode(config) {
       RED.nodes.createNode(this, config);
       this.clientID = config.clientID;
       this.accessToken = config.accessToken;
       this.refreshToken = config.refreshToken;
    
       var node = this;
    

    I have always thought I had to do it that way, because the config contains much more info (like e.g. the wires ...), and I don't want to circumvent the API's to get that kind of info. But perhaps your way of working is equally good, or better...
    Is that perhaps what you have done in your last post: "In my configuration (ecobeeController) node, I have 4 property" ?

  • So your controller is simply a config node. Perhaps you call it better like that next time, because otherwise people don't know what you are talking about...

  • I have done something similar in my Onvif nodes:

    • First store a reference to your config node in the server side of your EcobeePinRequest node:
      function EcobeePinRequest(config) {
      ...
         this.ecobeeController = RED.nodes.getNode(config.controller);
      
    • Then the controller is available in your http endpoint:
      RED.httpAdmin.post("/ecobee/:id", function(req,res) {
          var node = RED.nodes.getNode(req.params.id);
          if (node != null) {
             var controller = node.controller;
             if (controller != null) {
                ...
      

Not quite sure what you mean with this.
Perhaps this pull request I made is something similar??
If so you can perhaps learn from that example ...

1 Like

Thanks for the reply. I'll try to better explain myself. Sorry for lack of using proper term, I'm new to node-red. I code in Java (well, used to) and the method get and set are there because that's how I learned to used these. The property will end private and get used through these command.

Now, for better explanation. In the HTML file for the "controller", I have 4 fields:

    <div class="form-row">
        <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-config-input-name" placeholder="Name">
    </div>
	<div class="form-row">
        <label for="node-config-input-clientID"><i class="fa fa-tag"></i> Client ID</label>
        <input type="text" id="node-config-input-clientID">
    </div>
	<div class="form-row">
        <label for="node-config-input-accessToken"><i class="fa fa-tag"></i> Access Token</label>
        <input type="text" id="node-config-input-accessToken">
    </div>
	<div class="form-row">
        <label for="node-config-input-refreshToken"><i class="fa fa-tag"></i> Refresh Token</label>
        <input type="text" id="node-config-input-refreshToken">
    </div>

These fields can be seen in the GUI when someone click on the ecobeeController under configuration nodes. One of these field is clientID, which is input by the user and contain the API key from ecobee, generated for his app.

Now, I can access this value from the controller node by doing this.clientID, and this is without doing any "this.clientID = config.clientID". Since config from the ecobeePinRequest module contain a reference to the controller, config.clientID from a pinRequest node. I only did that so I can use set/get method later on for a good object usage (unless rules of access changed since early 2000 when I learned them).

For simplicity sakes, what I want is this.

When the use press the button on the pinrequest node, it start query to get the pincode and accesstoken (I already have this working in the .js file, triggered by the button). Now, once I have these information, I want them to be "pasted" in the ecobeecontroller node GUI, i.e. the html side, so when the user click on the controller in the configuration nodes, he can see the values. This is what I'm missing.

One of the problem (on my understanding) right now is the fact that I used part of the code from openhab2 nodes (which use a controller and ajax query) and inject node (for better understanding of the button).

Right now, the code to send back the ajax (and only that work) is res.sendStatus(200);.

The way I've done my trigger is that way:

> ecobee.html - button code for pinrequest
> button: {
> 			onclick: function() {
> 				//ecoControl = RED.nodes.node(this.controller);
> 				
> 				$.ajax({
>                     url: "ecobee/"+this.id,
>                     type:"POST",
>                     success: function(resp) {
> 						//ecoControl = RED.nodes.node(this.controller);
>                         //RED.notify(node._("inject.success",{label:label}),{type:"success",id:"inject"});
> 						RED.notify("resp: " + resp ,"information");
> 						//ecoControl.ecobeePin = resp; //WORK BUT RESP = OK...
>                     },
>                     error: function(jqXHR,textStatus,errorThrown) {
>                         if (jqXHR.status == 404) {
>                             RED.notify(node._("common.notification.error",{message:node._("common.notification.errors.not-deployed")}),"error");
>                         } else if (jqXHR.status == 500) {
>                             RED.notify(node._("common.notification.error",{message:node._("inject.errors.failed")}),"error");
>                         } else if (jqXHR.status == 0) {
>                             RED.notify(node._("common.notification.error",{message:node._("common.notification.errors.no-response")}),"error");
>                         } else {
>                             RED.notify(node._("common.notification.error",{message:node._("common.notification.errors.unexpected",{status:jqXHR.status,message:textStatus})}),"error");
>                         }
>                     }
>                 });
> 			}
> 		}

To be honest, I don't quite understand this syntax. I've check the syntax in w3school for jquery and they say success/error are depreciated and to use done. I tried and that made an error so I guess we aren't using 3.0+. Aside of that, they use textResponse to get the message but this throw me an error. I've tried resp (which output OK), looked at all of his children and can't see where it is. I tried to add object and that didn't work.

The calling is done to here (that might be another problem) outside any node:

> ecobee.js
> module.exports = function(RED) {
>    
>    RED.httpAdmin.post("/ecobee/:id", function(req,res) {
>         var node = RED.nodes.getNode(req.params.id);
> 		var ecobeeController = node.getController();
>         if (node != null) {
>             try {
> 			ecobeeController.pinRequest(res,node);
> 			 
>             } catch(err) {
> 				node.warn("test2");
>                 res.sendStatus(500);
>                 node.error(RED._("inject.failed",{error:err.toString()}));
>             }
>         } else {
> 			node.warn("test3");
>             res.sendStatus(404);
>         }
>     });

Since everything is async, i had to put the answer at the end of the pinRequest method, else it would try to output without having any info in it.

So my goal, unless there's a way in the js file to do it, is to have the values field in the HTML so I can see them.

I know I can do

ecoControl = RED.nodes.node(this.controller);
ecoControl.clientID="asdf"

and that will change it in the GUI from the html file.

I hope I'm clearer, thanks again, I know it's not easy to understand my confused mind.

I think you are making life hard for yourself. Why do you want to use a button on a node for the request of the information? Why don’t you use a button in the ui of the config node itself? This would at least to me make much more sense from a user perspective.
Have a look at @BartButenaers and mine voice2json config node where there is a button in the config ui that makes a request to an http endpoint in the js and than uses the information that gets send back in the config.
Here is the button:


Here the request triggered on press:

and here is the endpoint doing things and returning the information to be used in the config:

of course you don’t need to use the sendFile Response but you can send anything back this way, for example in the sox node I send back the available audio devices:

and do the request with jquery and not ajax:

I think this approach to use a button in the configs user interface might make more sense to the enduser.
1 Like

Hello, thanks for the reply, I'll check all of that. to be honest, I don't care about the button, it's just the way I found it. At first, I was even doing it at the loading of the controller, checking if and access token and refresh token is present and if not and a clientID is present, send a request. I though after that maybe a trigger would be required when the api expired and that's how I found that button.

I'll check to be sure I understand what you mean by a button in the UI. From what I can see, it seems to be a button in the edit properties gui? I'll look into that, will be even better.

One question, in the "RED.httpAdmin.get(), RED.auth.needPermission()... How do you know if you need that "auth.needPermission" and what to give it?

I'll analyse the rest of the code and see how to get it.

Thank you!

Hopefully this an this might make this a bit clear for you...

1 Like

Thanks a lot, I'll look into it once I get the hang of it.

Right now, I just understood how to send data through the getJSON, thanks to (https://api.jquery.com/jQuery.getJSON/). I agree, I prefer a lot more the button in the UI and that give me other idea. I'll have to check later on how to create a "timer" even so the controller node autorefresh it's token.

THanks again for the big help

1 Like

Everything is now working properly! I'm adding some function in the oneditsave to save the datas to the js. I found out that they don't sync on there own. So right now, because of I don't remember how, I got a different value in the GUI for my accesstoken then what I have in my back end. I'm doing what need to be done so what's in the GUI is always right.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.