Initializing config screen based on new config node

#1

Hi folks,

In my new node-red-contrib-onvif-nodes, I have a config node that contains all the logic to communicate with an Onvif-compliant IP camera. So all my other nodes can get data from the IP camera (via their corresponding config node) to show on their config screen:

  1. The Onvif Media node refers to the MyCamKitchen config node, which contains all the logic to connect to the camera in the kitchen.
  2. Via the config node, I can get all available profiles to show in the dropdown. That data is loaded from the config node like this:
    RED.httpAdmin.get('/onvifdevice/:cmd/:config_node_id', RED.auth.needsPermission('onvifdevice.read'), function(req, res){
       var node = RED.nodes.getNode(req.params.config_node_id);
       for(var i = 0; i < node.deviceConfig.cam.profiles.length; i++) {
           // Fill dropdown value list with profiles
           ...
        }
    }
    

That works fine as you can see in the above screenshot.
However this mechanism fails when the user creates a new config node:

image

  1. The user creates a new config node, and presses 'Done' on the config node screen.
  2. Then Node-RED goes back to the screen of the Media node.
  3. At that moment I go to the server (code snippet above) which fails because RED.nodes.getNode returns an undefined. This is pretty normal since the new config node has not been deployed yet, so I cannot connect to the camera yet...

Has anybody have a better idea to do implement?

Thanks !!!
Bart

#2

I don't think I have many options: The user needs to be able to select an option from a list of profiles.
But the profile list is not available when the config node is not deployed yet. So in that case the user should be able to enter the profile name manually in a text input field.

As a result I need a text input field with an optional selection list:

  • Seems html allows text input to have optional dropdown list(see datalist). However that is not supported by many browsers...

  • So I think autocomplete might be a better solution. The user can enter manually a profile name, and (every time the user enters a character) the content of the field will be send to the server-side flow. If similar token names are available, that list of tokens are being displayed in the browser:

    autocomplete

    And if the config node is not deployed yet, no tokens will be received from the server. So in that case the user can enter the profile name manually if he wants, without getting suggestions from the server ...

Moreover the autocomplete is fairly simple to implement.

  1. The node's config screen contains a text input field:
    <div class="form-row actionParam-row" id="profileToken-div">
        <label for="node-input-profileToken"><i class="fa fa-hashtag"></i> Profile token</label>
        <input type="text" id="node-input-profileToken"></select>
    </div>
    
  2. In the oneditprepare (of the node's config screen), autocomplete functionality is added to that input field (to get profiles as json from the server-side flow):
    oneditprepare: function() { 
        var node = this;
        var cache = {};
             
        $( "#node-input-profileToken" ).autocomplete({
               minLength: 3,
               source: function( request, response ) {
                 var term = request.term;
                 if ( term in cache ) {
                   response( cache[ term ] );
                   return;
                 }
    
                 $.getJSON("onvifdevice/profiles/" + node.id + "/" + $("#node-input-profileToken").val(), request, function( data, status, xhr ) {
                   cache[ term ] = data;
                   response( data );
                 });
               }
             });
    }
    
  3. And in my config node (on the server-side flow) I return all profiles whose name contains the text that the user has entered:
    RED.httpAdmin.get('/onvifdevice/:cmd/:config_node_id/:search_string', RED.auth.needsPermission('onvifdevice.read'), function(req, res){
         var node = RED.nodes.getNode(req.params.config_node_id);
    
         if (req.params.cmd === "profiles") {
             if (!node || !node.deviceConfig || !node.deviceConfig.cam) {
                 console.log("Cannot determine profile list from node " + req.params.config_node_id);
                 return;
             }
            
             var profileNames = [];
             
             for(var i = 0; i < node.deviceConfig.cam.profiles.length; i++) {
                 if (node.deviceConfig.cam.profiles[i].name.includes(req.params.search_string)) {
                     profileNames.push(
                         {
                             label: node.deviceConfig.cam.profiles[i].name,
                             value: node.deviceConfig.cam.profiles[i].$.token
                         });
                 }
             }
             
             // Return a list of all available profiles for the specified Onvif node
             res.json(profileNames);
         }
     });
    

If I get at least one 'like', I will implement it this way :wink:

[EDIT]: you can also download the entire profile list to the browser (in oneditprepare) and do autocomplete based on that client-side cached profile list (see example). That way there would be less communication to the Node-RED flow ...

#3

Hi @BartButenaers, I meant to reply earlier, but this is one of those questions that doesn't have a quick response and I haven't had the time to give a proper reply.

The autocomplete route is certainly one way.

The other is to allow the HTTP Admin end point you create to return the information to accept additional query parameters in place of the config node properties.

That way, if the node is 'known' to the runtime, it can use those values to do whatever work is needed to get the data. Otherwise it can use the query parameters to complete the request.

In your node's edit dialog you need to distinguish between a selection of a 'known' config node and a newly added one. There's a trick for that, but I can't quite remember the details - not done it for quite a while.

But there is another scenario to consider - they select an existing (known-to-the-runtime) config node, but then edit the config node to change its details. The node in the runtime no longer matches what they have in the editor, so the list will not be correct. This is all a bit abstract as I don't know what config properties your Device config node has.

So it may be better to change your http admin endpoint to always require the query parameters to be used to specify what data to return - that way it will always be in sync with what the user has entered in the dialog.

#4

Also the serial port config node is an example of a select box that you also type into (the port selection field)

#5

Hi guys (@dceejay, @knolleary),

That is indeed a good alternative to reduce the communication between the browser and the server (compared to my autocomplete solution). Will keep it in mind !

Until a few days I thought that Nick knew everything, and that he could answer on all my questions immediately. However after reading this post, it seems that he even doesn't know 'all' existing frameworks in detail. I am very shock :joy:
Just kidding, I don't expect you to answer all my questions after a couple of seconds ...

That is indeed so true ... That is a big disadvantage of my autocomplete solution. There goes my short moment of glory :woozy_face:

That would indeed be the best solution. But not sure how to get started with this... Is there perhaps somewhere a node that I could use as an example?

My config node has 3 fields:

image

As soon as this config node is deployed, it automatically connects (based on the above 3 settings) to the camera and loads information (e.g. profiles) from the IP camera:

function OnVifConfigNode(config) {
   var options = {};
   options.hostname = config.ipaddress;
   options.username = config.username;
   options.password = config.password;

   // Create a new camera instance ('onvif' node), which will automatically connect to the device (to load configuration data)
   this.cam = new onvif.Cam(options);
}

But it is not clear to me:

  • where the 'dirty' config node is stored, when I create a new config node or update an existing config node (until that config node is deployed).
  • if I can get hold of that dirty config node data somehow in my code.
  • and suppose I could get hold of those dirty config node settings, I would have to create a 'new onvif.Cam(dirtyOptions)' every time a config screen (of a related node) is shown. That Cam instance will send N requests to the camera to load all kind of information. I assume that will become slow, so autocomplete will be impossible.
#6

I 'think' will need to do it like this:

image

When I navigate to the config node, and change its name to 'MyCamKitchenUpdated', and go back to the config screen of my related node: then the change listener of the config node is triggered and I can get hold of the dirty config node.

I seems that RED.nodes.getNode is called in the flow editor RED.nodes.node ...

#7

I'm almost there ...
A 'search' button (similar to the serial-node) indeed looks nice, at the network traffic is much lower:

profile_lookup

Just need to pass now the information (username / password / ipaddress) of the dirty config node (in the browser) to the backend, via the ajax call. But the dirty config node doesn't seem to contain the credentials:

image

My (last) questions:

  • Is there an easy/secure way to get the credentials in the client-side code?
  • Should I pass the data like this to the server?
    $.getJSON("onvifdevice/profiles/" + node.id + "/" + username + "/" + password + "/" + ipaddress, function(profiles) {
    
    Or should I pass somehow 3 URL parameters, instead of sub-paths? But perhaps they are not automatically parsed into req.params on the server-side?
  • Is is safe (even via ssl) to pass those credentials from browser to server, when using the getJSON function?
#8

I'm having doubt whether it is possible to get the settings of a dirty config node, and send those to my server side flow. Especially since I need to get access to the credentials (username/password) from that config node.

The Node-RED documentation tells this:

Within the runtime, a node can access its credentials using the credentials property

From a security point of view, it make sense that only a node itself can get access to its own properties. And indeed I cannot get access to the username/password:

RED.nodes.node($("#node-input-deviceConfig").val()).credentials

Which returns undefined. In fact this is good news, but not for my purpose ...

So I think I should ask to the dirty config node, whether it can connect itself to the camera (based on the dirty credential information to which the dirty config node itself has access). But then again, the dirty config node should be available on the server side flow ... Does anybody know:

  • Whether the dirty nodes are available on the server side (before a node is deployed), and if so how I could get an instance of such a dirty node?
  • Whether it is possible to send the dirty config node using e.g. $getJSON to the server, and deserialize it there somehow?
  • Others?
#9

Ok have tried all kind of stuff, but I cannot get hold of the dirty credentials. So I assume it is impossible to connect to an IP camera (to get its profiles), based on those dirty credentials ...

So up to plan B...
I want to have at least a warning (that the user should 'Deploy'), which should be displayed as soon as the device config node setting has been changed:

image

  1. I change the settings of the device config node
  2. Then I want this text to become visible

This is the code to display/hide that "deployTip" section:

oneditprepare: function() { 
    $("#node-input-deviceConfig").change(function() {
         $("#deployTip").hide()

         if(this.value) {
             var configNode = RED.nodes.node(this.value);
                    
             if (configNode && configNode.dirty) {
                   $("#deployTip").show();
             }
        }
})

However the config node is always dirty, even when I haven't changed anything:

image

Getting nuts right now :-1::-1:
Does anybody have an idea what I'm doing wrong??

#10

Hi @BartButenaers

For a newly added config node, it will have a credentials object containing whatever has been entered in its edit dialog.

For an existing config node - that has not yet been edited - it will not have a credentials object.

When that node gets edited, the editor retrieves credential meta-data for the node from the runtime - which populates node.credentials. This meta-data does not include the actual value of any credential labelled as a password - just enough meta-data to tell the editor whether a value is known for it.

If the user enters a new value for a password, once the dialog is confirmed, the credentials object will have that value until deploy is pressed.

So... you have a couple different cases to handle:

  1. if there's no credentials object it means the editor doesn't know the values but the runtime might. Your request to the runtime should include the id of the node and let the runtime do the lookup (as it has full access to credentials).

  2. if there is a credentials object it means the user might have provided updated values. Your request to the runtime should in the id of the node and any of the updated values. The runtime code can then use the combination of credentials already known to the node with the updated values from the editor.

This feels like one of those replies I'll be bookmarking to referring back to in the future until the proper docs cover this sort of thing.

#11

Hey @knolleary, very clear explained! Just the kind of info I needed to get started with this.

#12

Hey Nick (@knolleary),
The username seems quite easy, but the password content is not clear to me ...

This is the content of the configNode.credentials on the client-side (i.e. in the config screen):

  • When I neither change the username nor the password, then credentials are undefined.

  • When I only change the username to 'xxxx', then the credentials contain only the new username:
    image
    The password is not included, but the boolean shows a password is available (at the server-side).

  • When I change the password to 'xxxx', then the credentials contain only the new password:
    image

  • When I change both username and password, then the credentials contain both values.

  • When I append 'xxxx' to the original password, then the credentials contain a constant __PWDR___ (representing the password which is only known on the server side) combined with my newly added characters:
    image
    Could still manage this one: when I send it to the server, I could replace the constant value by the real password value.

  • But when I replace a few characters of the original password by 'xx', then a part of the constant value will be replaced by my new characters:
    image
    I have no clue how I could determine which part of the real original password is remained. Indeed if I'm not mistaken, the constant value is always __PWDR___, undependent of the length of my real original password. Which is of course great from a security perspective, but not for me ...

I see that other nodes (like mqtt) are also dealing with the constant value, but I don't see at first sight how to handle partial replacements of the original password...

Do you have any other tips in your big magic box?

#13

That is because nothing handles the partial update of the password. It simple isn't a scenario the code considers - we assume if you are changing the password, then you're setting a whole new one, not blinding editing what's there already.

A better visual representation of the password field is somewhere on my to-do list.

So, my advice, don't worry about those edge cases for now.

1 Like