node-red-contrib-onvif-nodes
Version:
Node Red nodes for communicating with OnVif compliant IP devices
226 lines (193 loc) • 11 kB
JavaScript
/**
* Copyright 2018 Bart Butenaers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
var settings = RED.settings;
const onvif = require('onvif');
function setOnvifStatus(node, onvifStatus) {
node.onvifStatus = onvifStatus;
// Pass the new status to all the available listeners
node.emit('onvif_status', onvifStatus);
}
function OnVifConfigNode(config) {
RED.nodes.createNode(this, config);
this.xaddress = config.xaddress;
this.port = parseInt(config.port || 80);
this.name = config.name;
this.timeout = config.timeout || 3;
this.checkConnectionInterval = config.checkConnectionInterval || 5;
// Remark: user name and password are stored in this.credentials
var node = this;
// All Onvif nodes can add a listener to track the 'onvif_status' events.
// However by default only 10 listeners are allowed, which results in a warning when more Onvif nodes use this config node:
// MaxListenersExceededWarning: Possible EventEmitter memory leak detected
// To avoid that, we will allow an infinite number of listeners.
// Caution: when you have suspicion that the listeners are leaking, put the next line in comment !!!
node.setMaxListeners(0);
this.getProfiles = function(clientConfig, response) {
var profileNames = [];
var config = {};
// TODO checken of er altijd een this.credentials bestaat, indien username en paswoord niet ingevuld is.
// The client credentials will only contain the data (i.e. user name or password) which has changed.
// The other data is not changed, so we will need use the original data stored on the server.
clientConfig.username = clientConfig.user || this.credentials.user;
clientConfig.password = clientConfig.password || this.credentials.password;
// When the user appends some new text to the existing password, then the original password is passed via the client as __PWRD__
// So replace __PWRD__ again by the original password.
if (clientConfig.password && this.credentials.password) {
clientConfig.password.replace('___PWRD__', this.credentials.password);
}
if (this.credentials.user !== clientConfig.user || this.credentials.password !== clientConfig.password || this.xaddress !== clientConfig.hostname){
var cam = new onvif.Cam(clientConfig, function(err) {
if (!err) {
if (cam.profiles) {
for(var i = 0; i < cam.profiles.length; i++) {
profileNames.push({
label: cam.profiles[i].name,
value: cam.profiles[i].$.token
});
}
}
response.json(profileNames);
}
});
}
else {
if (this.cam.profiles) {
// The current deployed cam is still up-to-date, so let’s use that one (for performance reasons)
for(var i = 0; i < this.cam.profiles.length; i++) {
profileNames.push({
label: this.cam.profiles[i].name,
value: this.cam.profiles[i].$.token
});
}
}
response.json(profileNames);
}
}
this.getProfileTokenByName = function(profileName) {
if (this.cam.profiles) {
// Try to find a profile with the specified name, and return the token
for(var i = 0; i < this.cam.profiles.length; i++) {
if (this.cam.profiles[i].name === profileName) {
return this.cam.profiles[i].$.token;
}
}
}
// No token found with the specified name
return null;
}
// This should be called by all nodes that use this config node
this.initialize = function() {
// This config node can only be initialized oncellchange
if (this.cam) {
return;
}
// Without an xaddress, it is impossible to connect to an Onvif device
if (!this.xaddress) {
// Make sure the Catch-node can catch the error
node.error( "Cannot connect to unconfigured Onvif device", {} );
this.cam = null;
setOnvifStatus(node, "unconfigured");
return;
}
setOnvifStatus(node, "initializing");
var options = {};
options.hostname = this.xaddress;
options.port = this.port;
options.timeout = this.timeout * 1000;
if (this.credentials && this.credentials.user) {
options.username = this.credentials.user;
options.password = this.credentials.password;
}
// Create a new camera instance, which will automatically connect to the device (to load configuration data)
this.cam = new onvif.Cam(options, function(err) {
if (err) {
// Make sure the Catch-node can catch the error
node.error( err, {} );
setOnvifStatus(node, "disconnected");
}
else {
setOnvifStatus(node, "connected");
}
});
// When an checkConnection timer is running, then stop it
if (this.checkConnectionTimer) {
clearInterval(this.checkConnectionTimer);
this.checkConnectionTimer = null;
}
if (this.checkConnectionInterval > 0) {
// Start a new checkConnection timer, that checks at the specified interval whether the Onvif device is disconnected.
// This way we can keep the node status in the flow editor up to date ...
this.checkConnectionTimer = setInterval(function() {
// Check whether the Onvif device is connected, by calling the device system and time
node.cam.getSystemDateAndTime(function(err, date, xml) {
if (err) {
setOnvifStatus(node, "disconnected");
}
else {
if (!node.cam.capabilities && !node.cam.services) {
// Probably the device was unavailable when this node was being started, so the autoConnect was not able
// to load all the data from the camera (like e.g. its capabilities). Afterwards the device became available,
// so it is now time to load the device data into the Cam instance (by calling its 'connect' function).
// Note that this needs to be executed before setOnvifStatus, because that function uses the capabilities.
node.cam.connect(function(err, date, xml) {
if (err) {
node.error("The device is now connected, but the data cannot be loaded");
}
});
}
setOnvifStatus(node, "connected");
}
});
}, this.checkConnectionInterval * 1000);
}
}
node.on('close', function(){
setOnvifStatus(this, "");
this.removeAllListeners("onvif_status");
// When an checkConnection timer is running, then stop it
if (this.checkConnectionTimer) {
clearInterval(this.checkConnectionTimer);
this.checkConnectionTimer = null;
}
});
}
RED.nodes.registerType("onvif-config",OnVifConfigNode,{
credentials: {
user: {type:"text"},
password: {type: "password"}
}
});
// Make all the available profiles accessible for the node's config screen
RED.httpAdmin.get('/onvifdevice/:cmd/:config_node_id', RED.auth.needsPermission('onvifdevice.read'), function(req, res){
var configNode = RED.nodes.getNode(req.params.config_node_id);
switch (req.params.cmd) {
case "profiles":
if (!configNode) {
console.log("Cannot determine profile list from node " + req.params.config_node_id);
return;
}
// Get the profiles of the camera, based on the config data on the client, instead of the config data
// stored inside this config node. Reason is that the config data on the client might be 'dirty', i.e. changed
// by the user but not deployed yet on this config node. But the client still needs to be able to get the profiles
// corresponding to that dirty config node. That way the config screen can be filled with profiles already...
// But when the config data is not dirty, we will just use the profiles already loaded in this config node (which is faster).
// See https://discourse.nodered.org/t/initializing-config-screen-based-on-new-config-node/7327/10?u=bartbutenaers
configNode.getProfiles(req.query, res);
break;
}
});
}