You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

542 lines
19 KiB

/**
*
* Onvif adapter
*
*/
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
// you have to require the utils module and call adapter function
var utils = require(__dirname + '/lib/utils'); // Get common adapter utils
// you have to call the adapter function and pass a options object
// name has to be set and has to be equal to adapters folder name and main file name excluding extension
// adapter will be restarted automatically every time as the configuration changed, e.g system.adapter.template.0
var adapter = new utils.Adapter('onvif');
var Cam = require('onvif').Cam;
var flow = require('nimble');
require('onvif-snapshot');
var url = require('url');
var inherits = require('util').inherits;
var isDiscovery = false;
var cameras = {};
function override(child, fn) {
child.prototype[fn.name] = fn;
fn.inherited = child.super_.prototype[fn.name];
}
// overload Cam to preserve original hostname
function MyCam(options, callback) {
MyCam.super_.call(this, options, callback);
}
inherits(MyCam, Cam);
override(MyCam, function getSnapshotUri(options, callback) {
getSnapshotUri.inherited.call(this, options, function(err, res){
if(!err) {
const parsedAddress = url.parse(res.uri);
// If host for service and default host dirrers, also if preserve address property set
// we substitute host, hostname and port from settings
if (this.hostname !== parsedAddress.hostname) {
adapter.log.debug('need replace '+res.uri);
res.uri = res.uri.replace(parsedAddress.hostname, this.hostname);
adapter.log.debug('after replace '+res.uri);
}
}
if (callback) callback(err, res);
});
});
// is called when adapter shuts down - callback has to be called under any circumstances!
adapter.on('unload', function (callback) {
if (isDiscovery) {
adapter && adapter.setState && adapter.setState('discoveryRunning', false, true);
isDiscovery = false;
}
try {
adapter.log.debug('cleaned everything up...');
callback();
} catch (e) {
callback();
}
});
// is called if a subscribed object changes
adapter.on('objectChange', function (id, obj) {
// Warning, obj can be null if it was deleted
adapter.log.debug('objectChange ' + id + ' ' + JSON.stringify(obj));
});
// is called if a subscribed state changes
adapter.on('stateChange', function (id, state) {
// Warning, state can be null if it was deleted
adapter.log.debug('stateChange ' + id + ' ' + JSON.stringify(state));
// you can use the ack flag to detect if it is status (true) or command (false)
if (state && !state.ack) {
adapter.log.debug('ack is not set!');
}
});
// Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ...
adapter.on('message', function (obj) {
if (!obj || !obj.command) return;
switch (obj.command) {
case 'discovery':
adapter.log.debug('Received "discovery" event');
discovery(obj.message, function (error, newInstances, devices) {
isDiscovery = false;
adapter.log.debug('Discovery finished');
adapter.setState('discoveryRunning', false, true);
adapter.sendTo(obj.from, obj.command, {
error: error,
devices: devices,
newInstances: newInstances
}, obj.callback);
});
break;
case 'getDevices':
adapter.log.debug('Received "getDevices" event');
getDevices(obj.from, obj.command, obj.message, obj.callback);
break;
case 'deleteDevice':
adapter.log.debug('Received "deleteDevice" event');
deleteDevice(obj.from, obj.command, obj.message, obj.callback);
break;
case 'getSnapshot':
adapter.log.debug('Received "getSnapshot" event');
getSnapshot(obj.from, obj.command, obj.message, obj.callback);
break;
default:
adapter.log.debug('Unknown message: ' + JSON.stringify(obj));
break;
}
});
// is called when databases are connected and adapter received configuration.
// start here!
adapter.on('ready', function () {
main();
});
function main() {
isDiscovery = false;
adapter.setState('discoveryRunning', false, true);
// in this template all states changes inside the adapters namespace are subscribed
adapter.subscribeStates('*');
// connect to cameras
startCameras();
}
function getSnapshot(from, command, message, callback){
var camId = message.id,
cam = cameras[camId];
adapter.log.debug('getSnapshot: ' + JSON.stringify(message));
if (cam) {
// get snapshot
cam.getSnapshot((err, data) => {
if(err) throw err;
//adapter.log.debug(JSON.stringify(data));
adapter.sendTo(from, command, data, callback);
});
}
}
function camEvents(camMessage) {
adapter.log.debug('camEvents: ' + JSON.stringify(camMessage));
}
function startCameras(){
cameras = {};
adapter.log.debug('startCameras');
adapter.getDevices((err, result) => {
adapter.log.debug('startCameras: ' + JSON.stringify(result));
for (var item in result) {
let dev = result[item],
devData = dev.common.data,
cam;
//updateState(dev._id, 'connected', false, {type: 'boolean'});
cam = new MyCam({
hostname: devData.ip,
port: devData.port,
username: devData.user,
password: devData.pass,
timeout : 5000,
preserveAddress: true
}, function(err) {
if (!err) {
adapter.log.debug('capabilities: ' + JSON.stringify(cam.capabilities));
adapter.log.debug('uri: ' + JSON.stringify(cam.uri));
//updateState(dev._id, 'connected', true, {type: 'boolean'});
cameras[dev._id] = cam;
cam.on('event', camEvents);
} else {
adapter.log.info('startCameras err=' + err +' dev='+ JSON.stringify(devData));
}
});
}
});
}
function updateState(dev_id, name, value, common) {
var id = dev_id + '.' + name;
adapter.getObject(dev_id, function(err, obj) {
if (obj) {
let new_common = {
name: name,
role: (common != undefined && common.role == undefined) ? 'value' : common.role,
read: true,
write: (common != undefined && common.write == undefined) ? false : true
};
if (common != undefined) {
if (common.type != undefined) {
new_common.type = common.type;
}
if (common.unit != undefined) {
new_common.unit = common.unit;
}
if (common.states != undefined) {
new_common.states = common.states;
}
}
adapter.extendObject(id, {type: 'state', common: new_common});
adapter.setState(id, value, true);
} else {
adapter.log.info('no device '+dev_id);
}
});
}
function deleteDevice(from, command, msg, callback) {
var id = msg.id,
dev_id = id.replace(adapter.namespace+'.', '');
adapter.log.info('delete device '+dev_id);
adapter.deleteDevice(dev_id, function(){
adapter.sendTo(from, command, {}, callback);
});
}
function getDevices(from, command, message, callback){
var rooms;
adapter.getEnums('enum.rooms', function (err, list) {
if (!err){
rooms = list['enum.rooms'];
}
adapter.getDevices((err, result) => {
if (result) {
var devices = [], cnt = 0, len = result.length;
for (var item in result) {
if (result[item]._id) {
var id = result[item]._id.substr(adapter.namespace.length + 1);
let devInfo = result[item];
devInfo.rooms = [];
for (var room in rooms) {
if (!rooms[room] || !rooms[room].common || !rooms[room].common.members)
continue;
if (rooms[room].common.members.indexOf(devInfo._id) !== -1) {
devInfo.rooms.push(rooms[room].common.name);
}
}
cnt++;
devices.push(devInfo);
if (cnt==len) {
adapter.log.debug('getDevices result: ' + JSON.stringify(devices));
adapter.sendTo(from, command, devices, callback);
}
// adapter.getState(result[item]._id+'.paired', function(err, state){
// cnt++;
// if (state) {
// devInfo.paired = state.val;
// }
// devices.push(devInfo);
// if (cnt==len) {
// adapter.log.info('getDevices result: ' + JSON.stringify(devices));
// adapter.sendTo(from, command, devices, callback);
// }
// });
}
}
if (len == 0) {
adapter.log.debug('getDevices result: ' + JSON.stringify(devices));
adapter.sendTo(from, command, devices, callback);
}
}
});
});
}
function discovery(options, callback) {
if (isDiscovery) {
return callback && callback('Yet running');
}
isDiscovery = true;
adapter.setState('discoveryRunning', true, true);
var start_range = options.start_range, //'192.168.1.1'
end_range = options.end_range || options.start_range, //'192.168.1.254'
port_list = options.ports || '80, 7575, 8000, 8080, 8081',
port_list = port_list.split(',').map(item => item.trim()),
user = options.user || 'admin', // 'admin'
pass = options.pass || 'admin'; // 'admin'
var ip_list = generate_range(start_range, end_range);
if (ip_list.length === 1 && ip_list[0] === '0.0.0.0') {
ip_list = [options.start_range];
}
var devices = [], counter = 0, scanLen = ip_list.length * port_list.length;
// try each IP address and each Port
ip_list.forEach(function(ip_entry) {
port_list.forEach(function(port_entry) {
adapter.log.debug(ip_entry + ' ' + port_entry);
new MyCam({
hostname: ip_entry,
username: user,
password: pass,
port: port_entry,
timeout : 5000,
preserveAddress: true
}, function CamFunc(err) {
counter++;
if (err) {
if (counter == scanLen) processScannedDevices(devices, callback);
return;
}
var cam_obj = this;
var got_date;
var got_info;
var got_live_stream_tcp;
var got_live_stream_udp;
var got_live_stream_multicast;
var got_recordings;
var got_replay_stream;
// Use Nimble to execute each ONVIF function in turn
// This is used so we can wait on all ONVIF replies before
// writing to the console
flow.series([
function(callback) {
cam_obj.getSystemDateAndTime(function(err, date, xml) {
if (!err) got_date = date;
callback();
});
},
function(callback) {
cam_obj.getDeviceInformation(function(err, info, xml) {
if (!err) got_info = info;
callback();
});
},
function(callback) {
try {
cam_obj.getStreamUri({
protocol: 'RTSP',
stream: 'RTP-Unicast'
}, function(err, stream, xml) {
if (!err) got_live_stream_tcp = stream;
callback();
});
} catch(err) {callback();}
},
function(callback) {
try {
cam_obj.getStreamUri({
protocol: 'UDP',
stream: 'RTP-Unicast'
}, function(err, stream, xml) {
if (!err) got_live_stream_udp = stream;
callback();
});
} catch(err) {callback();}
},
function(callback) {
try {
cam_obj.getStreamUri({
protocol: 'UDP',
stream: 'RTP-Multicast'
}, function(err, stream, xml) {
if (!err) got_live_stream_multicast = stream;
callback();
});
} catch(err) {callback();}
},
function(callback) {
cam_obj.getRecordings(function(err, recordings, xml) {
if (!err) got_recordings = recordings;
callback();
});
},
function(callback) {
// Get Recording URI for the first recording on the NVR
if (got_recordings) {
//adapter.log.debug('got_recordings='+JSON.stringify(got_recordings));
if (Array.isArray(got_recordings)) {
got_recordings = got_recordings[0];
}
cam_obj.getReplayUri({
protocol: 'RTSP',
recordingToken: got_recordings.recordingToken
}, function(err, stream, xml) {
if (!err) got_replay_stream = stream;
callback();
});
} else {
callback();
}
},
function(localcallback) {
adapter.log.debug('------------------------------');
adapter.log.debug('Host: ' + ip_entry + ' Port: ' + port_entry);
adapter.log.debug('Date: = ' + got_date);
adapter.log.debug('Info: = ' + JSON.stringify(got_info));
if (got_live_stream_tcp) {
adapter.log.debug('First Live TCP Stream: = ' + got_live_stream_tcp.uri);
}
if (got_live_stream_udp) {
adapter.log.debug('First Live UDP Stream: = ' + got_live_stream_udp.uri);
}
if (got_live_stream_multicast) {
adapter.log.debug('First Live Multicast Stream: = ' + got_live_stream_multicast.uri);
}
if (got_replay_stream) {
adapter.log.debug('First Replay Stream: = ' + got_replay_stream.uri);
}
adapter.log.debug('capabilities: ' + JSON.stringify(cam_obj.capabilities));
adapter.log.debug('------------------------------');
devices.push({
id: getId(ip_entry+':'+port_entry),
name: ip_entry+':'+port_entry,
ip: ip_entry,
port: port_entry,
user: user,
pass: pass,
ip: ip_entry,
port: port_entry,
cam_date: got_date,
info: got_info,
live_stream_tcp: got_live_stream_tcp,
live_stream_udp: got_live_stream_udp,
live_stream_multicast: got_live_stream_multicast,
replay_stream: got_replay_stream
});
localcallback();
if (counter == scanLen) processScannedDevices(devices, callback);
}
]); // end flow
});
}); // foreach
}); // foreach
}
function processScannedDevices(devices, callback) {
// check if device is new
var newInstances = [], currDevs = [];
adapter.getDevices((err, result) => {
if(result) {
for (var item in result) {
if (result[item]._id) {
currDevs.push(result[item]._id);
}
}
}
for (var devInd in devices) {
var dev = devices[devInd];
if (currDevs.indexOf(dev.id) == -1) {
newInstances.push(dev);
// create new camera
updateDev(dev.id, dev.name, dev);
}
}
startCameras();
if (callback) callback(newInstances);
});
}
function updateDev(dev_id, dev_name, devData) {
// create dev
adapter.setObjectNotExists(dev_id, {
type: 'device',
common: {name: dev_name, data: devData}
}, {}, function (obj) {
adapter.getObject(dev_id, function(err, obj) {
if (!err && obj) {
// if update
adapter.extendObject(dev_id, {
type: 'device',
common: {data: devData}
});
startCameras();
}
});
});
}
function getId(addr) {
return addr.replace(/\./g, '_').replace(':', '_');
}
function generate_range(start_ip, end_ip) {
var start_long = toLong(start_ip);
var end_long = toLong(end_ip);
if (start_long > end_long) {
var tmp=start_long;
start_long=end_long
end_long=tmp;
}
var range_array = [];
var i;
for (i=start_long; i<=end_long;i++) {
range_array.push(fromLong(i));
}
return range_array;
}
//toLong taken from NPM package 'ip'
function toLong(ip) {
var ipl = 0;
ip.split('.').forEach(function(octet) {
ipl <<= 8;
ipl += parseInt(octet);
});
return(ipl >>> 0);
};
//fromLong taken from NPM package 'ip'
function fromLong(ipl) {
return ((ipl >>> 24) + '.' +
(ipl >> 16 & 255) + '.' +
(ipl >> 8 & 255) + '.' +
(ipl & 255) );
};