/** * * 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) ); };