diff --git a/definitions/base.js b/definitions/base.js index 2648b7ab..bb0a4515 100644 --- a/definitions/base.js +++ b/definitions/base.js @@ -6625,75 +6625,79 @@ module.exports = function(s,config,lang){ }, } }, - "ONVIF Scanner": { - "section": "ONVIF Scanner", - "blocks": { - "Search Settings": { - "name": lang["Scan Settings"], - "color": "navy", - "blockquote": lang.ONVIFnote, - "section-pre-class": "col-md-4", - "info": [ - { - "name": "ip", - "field": lang['IP Address'], - "description": lang.fieldTextIp, - "example": "10.1.100.1-10.1.100.254", - }, - { - "name": "port", - "field": lang['Port'], - "description": lang.separateByCommasOrRange, - "example": "80,7575,8000,8080,8081", - }, - { - "name": "user", - "field": lang['Camera Username'], - "description": lang.fieldTextOnvifScanCameraUsername, - "placeholder": "Can be left blank.", - }, - { - "name": "pass", - "field": lang['Camera Password'], - "description": lang.fieldTextOnvifScanCameraPassword, - "fieldType": "password", - }, - { - "fieldType": "btn-group", - "btns": [ - { - "fieldType": "btn", - "forForm": true, - "class": `btn-success start-scan`, - "btnContent": `${lang['Search']}`, - }, - { - "fieldType": "btn", - "class": `btn-danger stop-scan d-none`, - "btnContent": `${lang['Stop']}`, - }, - { - "fieldType": "btn", - "class": `btn-default add-all`, - "btnContent": `${lang['Add All']}`, - }, - ], - }, + "ONVIF Scanner": { + "section": "ONVIF Scanner", + "blocks": { + "Search Settings": { + "name": lang["Scan Settings"], + "color": "navy", + "blockquote": lang.ONVIFnote, + "section-pre-class": "col-md-4", + "info": [ + { + "name": "ip", + "field": lang['IP Address'], + "description": lang[lang["fieldTextIp"]], + "example": "10.1.100.1-10.1.100.254", + }, + { + "name": "port", + "field": lang['Port'], + "description": lang.separateByCommasOrRange, + "example": "80,7575,8000,8080,8081", + }, + { + "name": "user", + "field": lang['Camera Username'], + "placeholder": "Can be left blank.", + }, + { + "name": "pass", + "field": lang['Camera Password'], + "fieldType": "password", + }, + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "forForm": true, + "class": `btn-block btn-success`, + "btnContent": `${lang['Search']}
`, + }, + { + "fieldType": "btn", + "class": `btn-default add-all`, + "btnContent": `${lang['Add All']}`, + }, + ], + }, + ] + }, + "Found Devices": { + "name": lang['Found Devices'], + "color": "blue", + "section-pre-class": "col-md-8", + "info": [ + { + "fieldType": "div", + "class": "onvif_result row", + } ] }, - "Found Devices": { - "name": lang['Found Devices'], - "color": "blue", - "section-pre-class": "col-md-8", + "Other Devices": { + "name": lang['Other Devices'], + "color": "danger", + "section-pre-class": "col-md-12", "info": [ { "fieldType": "div", - "class": "onvif_result row", + "class": "onvif_result_error row", } ] - } - } - }, + }, + } + }, "Camera Probe": { "section": "Camera Probe", "blocks": { diff --git a/libs/scanners.js b/libs/scanners.js index e78197b8..af570cdf 100644 --- a/libs/scanners.js +++ b/libs/scanners.js @@ -1,34 +1,16 @@ module.exports = function(s,config,lang,app,io){ - const { - scanStatus, - runOnvifScanner, - stopOnvifScanner, - } = require('./scanners/utils.js')(s,config,lang) const { ffprobe, } = require('./ffmpeg/utils.js')(s,config,lang) + const { + runOnvifScanner, + } = require('./scanners/utils.js')(s,config,lang) const onWebSocketConnection = async (cn) => { const tx = function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} cn.on('f',(d) => { switch(d.f){ - case'onvif_scan_reconnect': - tx({f: 'onvif_scan_current', devices: Object.values(scanStatus.allSuccessful), isScanning: scanStatus.isActive}) - break; - case'onvif_stop': - stopOnvifScanner() - tx({f: 'onvif_scan_stopped'}) - break; case'onvif': - const groupKey = `${cn.ke}` - runOnvifScanner(d, (data) => { - const response = { f: 'onvif', ...data } - s.tx(response, 'GRP_' + cn.ke) - }, (data) => { - const response = { f: 'onvif', ff: 'failed_capture', ...data } - s.tx(response, 'GRP_' + cn.ke) - }).then((responseList) => { - s.tx({ f: 'onvif_scan_complete' }, 'GRP_' + cn.ke) - }) + runOnvifScanner(d,tx) break; } }) @@ -58,60 +40,4 @@ module.exports = function(s,config,lang,app,io){ }) },res,req); }) - /** - * API : ONVIF Scanner RUN - */ - app.get(config.webPaths.apiPrefix+':auth/onvifScanner/:ke/scan',function (req,res){ - s.auth(req.params,function(user){ - const { - isRestricted, - isRestrictedApiKey, - apiKeyPermissions, - } = s.checkPermission(user); - if( - isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed - ){ - s.closeJsonResponse(res,{ - ok: false, - msg: lang['Not Authorized'] - }); - return - } - const groupKey = req.params.ke; - stopOnvifScanner() - s.closeJsonResponse(res, { ok: true }); - },res,req); - }) - /** - * API : ONVIF Scanner STOP - */ - app.get(config.webPaths.apiPrefix+':auth/onvifScanner/:ke/scan/stop',function (req,res){ - s.auth(req.params,function(user){ - const { - isRestricted, - isRestrictedApiKey, - apiKeyPermissions, - } = s.checkPermission(user); - if( - isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed - ){ - s.closeJsonResponse(res,{ - ok: false, - msg: lang['Not Authorized'] - }); - return - } - const groupKey = req.params.ke; - runOnvifScanner(d, (data) => { - const response = { f: 'onvif', ...data } - s.tx(response, 'GRP_' + groupKey) - }, (data) => { - const response = { f: 'onvif', ff: 'failed_capture', ...data } - s.tx(response, 'GRP_' + groupKey) - }).then((responseList) => { - s.tx({ f: 'onvif_scan_complete' }, 'GRP_' + groupKey) - s.closeJsonResponse(res, responseList) - }) - },res,req); - }) } diff --git a/libs/scanners/utils.js b/libs/scanners/utils.js index e390465f..7169911b 100644 --- a/libs/scanners/utils.js +++ b/libs/scanners/utils.js @@ -1,381 +1,202 @@ var os = require('os'); -const async = require('async'); const onvif = require("shinobi-onvif"); const { addCredentialsToUrl, stringContains, getBuffer, } = require('../common.js') -const scanStatus = { - current: [], - allSuccessful: {}, - cancelPromises: null, - abortController: null -}; - module.exports = (s,config,lang) => { const ipRange = (start_ip, end_ip) => { - const startLong = toLong(start_ip); - const endLong = toLong(end_ip); - if (startLong > endLong) { - const tmp = startLong; - startLong = endLong; - endLong = tmp; - } - const rangeArray = []; - for (let i = startLong; i <= endLong; i++) { - rangeArray.push(fromLong(i)); - } - return rangeArray; - }; - - const portRange = (lowEnd, highEnd) => { - const list = []; - for (let i = lowEnd; i <= highEnd; i++) { + var startLong = toLong(start_ip); + var endLong = toLong(end_ip); + if (startLong > endLong) { + var tmp = startLong; + startLong = endLong + endLong = tmp; + } + var rangeArray = []; + var i; + for (i = startLong; i <= endLong; i++) { + rangeArray.push(fromLong(i)); + } + return rangeArray; + } + const portRange = (lowEnd,highEnd) => { + var list = []; + for (var i = lowEnd; i <= highEnd; i++) { list.push(i); } return list; - }; - + } + //toLong taken from NPM package 'ip' const toLong = (ip) => { - let ipl = 0; - ip.split('.').forEach(function(octet) { - ipl <<= 8; - ipl += parseInt(octet); - }); - return (ipl >>> 0); - }; - + var ipl = 0; + ip.split('.').forEach(function(octet) { + ipl <<= 8; + ipl += parseInt(octet); + }); + return(ipl >>> 0); + } + //fromLong taken from NPM package 'ip' const fromLong = (ipl) => { - return ((ipl >>> 24) + '.' + - (ipl >> 16 & 255) + '.' + - (ipl >> 8 & 255) + '.' + - (ipl & 255)); - }; - - const getNetworkAddresses = () => { - const interfaces = os.networkInterfaces(); - const addresses = []; - for (const k in interfaces) { - for (const k2 in interfaces[k]) { - const address = interfaces[k][k2]; - if (address.family === 'IPv4' && !address.internal) { - addresses.push(address.address); - } - } - } - return addresses; - }; - - const getAddressRange = (addresses) => { - const addressRange = []; - addresses.forEach((address) => { - if (address.indexOf('0.0.0') > -1) return; - const addressPrefix = address.split('.').slice(0, 3).join('.'); - addressRange.push(`${addressPrefix}.1-${addressPrefix}.254`); - }); - return addressRange.join(','); - }; - - const getPorts = (ports) => { - if (ports === '') { - return '80,8080,8000,7575,8081,9080,8090,8999,8899'.split(','); - } - if (ports.indexOf('-') > -1) { - const [start, end] = ports.split('-'); - return portRange(start, end); - } - return ports.split(','); - }; - - const getIpList = (ip) => { - const ipList = []; - ip.split(',').forEach((range) => { - const [start, end] = range.indexOf('-') > -1 ? range.split('-') : [range, range]; - ipList.push(...ipRange(start, end)); - }); - return ipList; - }; - - const createHitList = (ipList, ports, onvifUsername = '', onvifPassword = '') => { - const hitList = []; - const usernameVariants = onvifUsername.split(','); - const passwordVariants = onvifPassword.split(','); - for (const username of usernameVariants) { - for (const password of passwordVariants) { - hitList.push(...ipList.flatMap((ipEntry) => - ports.map((portEntry) => ({ - xaddr: `http://${ipEntry}:${portEntry}/onvif/device_service`, - user: username, - pass: password, - ip: ipEntry, - port: portEntry, - })) - )); - } - } - return hitList; - }; - - const takeSnapshot = async (cameraResponse, device) => { - try { - const snapUri = addCredentialsToUrl({ - username: cameraResponse.user, - password: cameraResponse.pass, - url: (await device.services.media.getSnapshotUri({ ProfileToken: device.current_profile.token })).data.GetSnapshotUriResponse.MediaUri.Uri, - }); - const imgBuffer = await getBuffer(snapUri); - cameraResponse.snapShot = imgBuffer.toString('base64'); - } catch (err) { - console.error('Failed to get snapshot via ONVIF:', err); - } - return cameraResponse; + return ((ipl >>> 24) + '.' + + (ipl >> 16 & 255) + '.' + + (ipl >> 8 & 255) + '.' + + (ipl & 255) ); } - - const fetchCameraDetails = async (camera, onvifUsername, onvifPassword, foundCameraCallback, failedCameraCallback) => { - // const previousSuccess = scanStatus.allSuccessful[camera.ip]; - // if (previousSuccess) { - // // console.log('FOUND PREVIOUS', camera.ip); - // foundCameraCallback(previousSuccess); - // return; - // } - try { - const device = new onvif.OnvifDevice(camera); - const info = await device.init(); - const stream = await device.services.media.getStreamUri({ - ProfileToken: device.current_profile.token, - Protocol: 'RTSP' - }); - - const cameraResponse = { - ip: camera.ip, - port: camera.port, - user: camera.user, - pass: camera.pass, - info: info, - uri: stream.data.GetStreamUriResponse.MediaUri.Uri - }; - - try { - const camPtzConfigs = (await device.services.ptz.getConfigurations()).data.GetConfigurationsResponse; - if (camPtzConfigs.PTZConfiguration && (camPtzConfigs.PTZConfiguration.PanTiltLimits || camPtzConfigs.PTZConfiguration.ZoomLimits)) { - cameraResponse.isPTZ = true; - } - } catch (err) { - console.error(er) - // s.debugLog(err); - } - await takeSnapshot(cameraResponse, device) - scanStatus.allSuccessful[camera.ip] = cameraResponse; - foundCameraCallback(cameraResponse); - - return cameraResponse; - } catch (err) { - return handleCameraError(camera, err, failedCameraCallback); - } - }; - - const handleCameraError = (camera, err, failedCameraCallback) => { - // const previousSuccess = scanStatus.allSuccessful[camera.ip]; - // if (previousSuccess) { - // // console.log('FOUND PREVIOUS AFTER ERROR', camera.ip); - // return previousSuccess; - // } - const searchError = (find) => stringContains(find, err.message, true); - const commonIgnoredErrors = ['ECONNREFUSED', 'socket hang up']; - let foundDevice = false; - let errorMessage = ''; - - switch (true) { - case searchError('ECONNREFUSED'): - errorMessage = `ECONNREFUSED`; - return {refused: true} - break; - case searchError('TIMEDOUT'): - foundDevice = true; - errorMessage = lang.ONVIFErr401; - break; - case searchError('401'): - foundDevice = true; - errorMessage = lang.ONVIFErr401; - break; - case searchError('400'): - foundDevice = true; - errorMessage = lang.ONVIFErr400; - break; - case searchError('405'): - foundDevice = true; - errorMessage = lang.ONVIFErr405; - break; - case searchError('404'): - foundDevice = true; - errorMessage = lang.ONVIFErr404; - break; - default: - break; - } - - if (foundDevice) { - const cameraResponse = { - ip: camera.ip, - port: camera.port, - error: errorMessage, - failedConnection: true, - }; - failedCameraCallback(cameraResponse); - return cameraResponse; - } - - return null; - }; - - function isValidOnvifResult(result) { - return result.info || result.uri; - } - - function detectAndReplaceReolinkRTSP(camera, url){ - const possibilities = [`/h264Preview_01_main`, `/h265Preview_01_main`] - for(possible of possibilities){ - // console.log(url, possible, url.indexOf(possible) > -1) - if(url.indexOf(possible) > -1){ - return `rtmp://${camera.user}:${camera.pass}@${camera.ip}:1935/bcs/channel0_main.bcs?token=sdasdasd&channel=0&stream=0&user=${camera.user}&password=${camera.pass}` - } - } - return url - } - - const runOnvifScanner = async (options, foundCameraCallback, failedCameraCallback) => { - if (scanStatus.isActive) return scanStatus.current; - - scanStatus.isActive = true; - scanStatus.abortController = new AbortController(); - const { signal } = scanStatus.abortController; - const cancelPromises = []; - scanStatus.cancelPromises = cancelPromises; - let ip = options.ip.replace(/ /g, ''); - let ports = options.port.replace(/ /g, ''); - const onvifUsername = options.user || 'admin'; - const onvifPassword = options.pass || ''; - - if (ip === '') { - const addresses = getNetworkAddresses(); - ip = getAddressRange(addresses); - } - - ports = getPorts(ports); - - const ipList = getIpList(ip); - const hitList = createHitList(ipList, ports, onvifUsername, onvifPassword); - - const ipQueues = {}; - const responseList = []; - const allPingSuccess = {}; - - const fetchWithTimeout = async (camera, onvifUsername, onvifPassword, foundCameraCallback, failedCameraCallback, signal) => { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout')), 2500); // Adjust the timeout as needed - fetchCameraDetails(camera, onvifUsername, onvifPassword, foundCameraCallback, failedCameraCallback, signal) - .then(result => { - clearTimeout(timeout); - resolve(result); - }) - .catch(error => { - clearTimeout(timeout); - reject(error); - }); - }); - }; - - try { - for (const camera of hitList) { - if (!ipQueues[camera.ip]) { - ipQueues[camera.ip] = async.queue(async (task) => { - if (signal.aborted) { - throw new Error('Aborted'); - } - // if(!scanStatus.allSuccessful[camera.ip]){ - const cameraIp = task.camera.ip; - const hasPingSuccess = allPingSuccess[cameraIp]; - if (hasPingSuccess !== false) { - const fetchPromise = fetchWithTimeout(task.camera, task.onvifUsername, task.onvifPassword, task.foundCameraCallback, task.failedCameraCallback, signal); - cancelPromises.push(fetchPromise); - const result = await fetchPromise; - if (result.refused) allPingSuccess[cameraIp] = !result.refused; - if (result.uri){ - try{ - result.uri = detectAndReplaceReolinkRTSP(task.camera, addCredentialsToUrl({ url: result.uri, username: task.camera.user, password: task.camera.pass })); - }catch(err){ - console.error(err) - } - } - responseList.push({...result}); - } - // } - }, 1); - } - ipQueues[camera.ip].push({ - camera, - onvifUsername: camera.user, - onvifPassword: camera.pass, - foundCameraCallback, - failedCameraCallback - }); - } - await Promise.all(Object.values(ipQueues).map(queue => new Promise((resolve) => queue.drain(resolve)))); - } catch (err) { - if (err.message === 'Aborted') { - console.log('Scan aborted'); - } else { - console.error('big error', err); - } - } - - scanStatus.isActive = false; - scanStatus.abortController = null; - scanStatus.cancelPromises = null; - s.debugLog('Done Scan'); - return responseList; - }; - - const stopOnvifScanner = () => { - if (scanStatus.isActive && scanStatus.abortController) { - scanStatus.abortController.abort(); - scanStatus.cancelPromises.forEach(promise => promise.catch(() => {})); - scanStatus.isActive = false; - s.debugLog('Scan stopped'); - } - }; - - function expandIPRange(rangeStr) { - const ipRangeToArray = (start, end) => { - const startParts = start.split('.').map(Number); - const endParts = end.split('.').map(Number); - const ips = []; - for (let a = startParts[0]; a <= endParts[0]; a++) { - for (let b = startParts[1]; b <= endParts[1]; b++) { - for (let c = startParts[2]; c <= endParts[2]; c++) { - for (let d = startParts[3]; d <= endParts[3]; d++) { - ips.push([a, b, c, d].join('.')); - } + const runOnvifScanner = (options,foundCameraCallback) => { + var ip = options.ip.replace(/ /g,'') + var ports = options.port.replace(/ /g,'') + if(options.ip === ''){ + var interfaces = os.networkInterfaces() + var addresses = [] + for (var k in interfaces) { + for (var k2 in interfaces[k]) { + var address = interfaces[k][k2] + if (address.family === 'IPv4' && !address.internal) { + addresses.push(address.address) } } } - return ips; - }; + const addressRange = [] + addresses.forEach(function(address){ + if(address.indexOf('0.0.0')>-1){return false} + var addressPrefix = address.split('.') + delete(addressPrefix[3]); + addressPrefix = addressPrefix.join('.') + addressRange.push(`${addressPrefix}1-${addressPrefix}254`) + }) + ip = addressRange.join(',') + } + if(ports === ''){ + ports = '80,8080,8000,7575,8081,9080,8090,8999,8899' + } + if(ports.indexOf('-') > -1){ + ports = ports.split('-') + var portRangeStart = ports[0] + var portRangeEnd = ports[1] + ports = portRange(portRangeStart,portRangeEnd); + }else{ + ports = ports.split(',') + } + var ipList = options.ipList + var onvifUsername = options.user || '' + var onvifPassword = options.pass || '' + ip.split(',').forEach(function(addressRange){ + var ipRangeStart = addressRange[0] + var ipRangeEnd = addressRange[1] + if(addressRange.indexOf('-')>-1){ + addressRange = addressRange.split('-'); + ipRangeStart = addressRange[0] + ipRangeEnd = addressRange[1] + }else{ + ipRangeStart = addressRange + ipRangeEnd = addressRange + } + if(!ipList){ + ipList = ipRange(ipRangeStart,ipRangeEnd); + }else{ + ipList = ipList.concat(ipRange(ipRangeStart,ipRangeEnd)) + } + }) + var hitList = [] + ipList.forEach((ipEntry,n) => { + ports.forEach((portEntry,nn) => { + hitList.push({ + xaddr : 'http://' + ipEntry + ':' + portEntry + '/onvif/device_service', + user : onvifUsername, + pass : onvifPassword, + ip: ipEntry, + port: portEntry, + }) + }) + }) + var responseList = [] + hitList.forEach(async (camera) => { + try{ + var device = new onvif.OnvifDevice(camera) + var info = await device.init() + var date = await device.services.device.getSystemDateAndTime() + var stream = await device.services.media.getStreamUri({ + ProfileToken : device.current_profile.token, + Protocol : 'RTSP' + }) - return rangeStr.split(',') - .flatMap(range => { - const [start, end] = range.split('-'); - return ipRangeToArray(start.trim(), end.trim()); - }); + var cameraResponse = { + ip: camera.ip, + port: camera.port, + info: info, + date: date, + uri: stream.data.GetStreamUriResponse.MediaUri.Uri + } + try{ + const camPtzConfigs = (await device.services.ptz.getConfigurations()).data.GetConfigurationsResponse + if( + camPtzConfigs.PTZConfiguration && + ( + camPtzConfigs.PTZConfiguration.PanTiltLimits || + camPtzConfigs.PTZConfiguration.ZoomLimits + ) + ){ + cameraResponse.isPTZ = true + } + }catch(err){ + s.debugLog(err) + } + responseList.push(cameraResponse) + var imageSnap + try{ + const snapUri = addCredentialsToUrl({ + username: onvifUsername, + password: onvifPassword, + url: (await device.services.media.getSnapshotUri({ + ProfileToken : device.current_profile.token, + })).data.GetSnapshotUriResponse.MediaUri.Uri, + }); + imageSnap = (await getBuffer(snapUri)).toString('base64'); + }catch(err){ + s.debugLog(err) + } + if(foundCameraCallback)foundCameraCallback(Object.assign(cameraResponse,{f: 'onvif', snapShot: imageSnap})) + }catch(err){ + const searchError = (find) => { + return stringContains(find,err.message,true) + } + var foundDevice = false + var errorMessage = '' + switch(true){ + //ONVIF camera found but denied access + case searchError('400'): //Bad Request - Sender not Authorized + foundDevice = true + errorMessage = lang.ONVIFErr400 + break; + case searchError('405'): //Method Not Allowed + foundDevice = true + errorMessage = lang.ONVIFErr405 + break; + //Webserver exists but undetermined if IP Camera + case searchError('404'): //Not Found + foundDevice = true + errorMessage = lang.ONVIFErr404 + break; + } + if(foundDevice && foundCameraCallback){ + foundCameraCallback({ + f: 'onvif', + ff: 'failed_capture', + ip: camera.ip, + port: camera.port, + error: errorMessage + }) + } + if(config.debugLogVerbose)s.debugLog(err); + } + }) + return responseList } - return { - expandIPRange, - ipRange, - portRange, - scanStatus, - runOnvifScanner, - stopOnvifScanner, - }; + ipRange: ipRange, + portRange: portRange, + runOnvifScanner: runOnvifScanner, + } } diff --git a/web/assets/js/bs5.onvifScanner.js b/web/assets/js/bs5.onvifScanner.js index 170ed056..f39221d1 100644 --- a/web/assets/js/bs5.onvifScanner.js +++ b/web/assets/js/bs5.onvifScanner.js @@ -4,8 +4,6 @@ $(document).ready(function(e){ var loadedResultsByIp = {} var monitorEditorWindow = $('#tab-monitorSettings') var onvifScannerWindow = $('#tab-onvifScanner') - var onvifScannerStartButton = onvifScannerWindow.find('.start-scan') - var onvifScannerStopButton = onvifScannerWindow.find('.stop-scan') var onvifScannerResultPane = onvifScannerWindow.find('.onvif_result') var onvifScannerErrorResultPane = onvifScannerWindow.find('.onvif_result_error') var scanForm = onvifScannerWindow.find('form'); @@ -32,97 +30,86 @@ $(document).ready(function(e){ var html = buildSubMenuItems(allFound) sideMenuList.html(html) } - var showStopButton = function(appearance){ + var setAsLoading = function(appearance){ if(appearance){ - onvifScannerStartButton.addClass('d-none') - onvifScannerStopButton.removeClass('d-none') + onvifScannerWindow.find('._loading').show() + onvifScannerWindow.find('[type="submit"]').prop('disabled',true) }else{ - onvifScannerStartButton.removeClass('d-none') - onvifScannerStopButton.addClass('d-none') + onvifScannerWindow.find('._loading').hide() + onvifScannerWindow.find('[type="submit"]').prop('disabled',false) } } - - function drawDeviceTableRow(device, gotAccess){ - var ip = device.ip; - var el = onvifScannerResultPane.find(`[scan-item="${ip}"]`) - var hasError = !!device.error; - var uriText = !hasError ? device.uri ? device.uri.split('?')[0] : '' : device.error; - var statusColor = hasError ? 'red' : 'green'; - var snapShot = device.snapShot; - // console.log(ip, device.error, hasError) - if(gotAccess)loadMonitorConfigFromResult(device) - if(el.length === 0){ - var html = `${lang.addAllCamerasText.replace('9001', numberOfCameras)}