From f35f8d0f432c72c6d689661ce56fb3a1230e09ff Mon Sep 17 00:00:00 2001 From: zhongjin Date: Tue, 7 Aug 2018 20:30:44 +0800 Subject: [PATCH] Initial commit --- .gitignore | 6 + .npmignore | 10 + .travis.yml | 19 + LICENSE | 21 + README.md | 4 + admin/i18n/de/translations.json | 9 + admin/i18n/en/translations.json | 9 + admin/i18n/es/translations.json | 9 + admin/i18n/fr/translations.json | 9 + admin/i18n/it/translations.json | 9 + admin/i18n/nl/translations.json | 9 + admin/i18n/pt/translations.json | 9 + admin/i18n/ru/translations.json | 9 + admin/index.html | 84 ++++ admin/index_m.html | 353 ++++++++++++++++ admin/onvif.png | Bin 0 -> 22458 bytes admin/onvif_logo.png | Bin 0 -> 27381 bytes admin/template.png | Bin 0 -> 2318 bytes admin/words.js | 13 + appveyor.yml | 21 + docs/de/img/picture.png | Bin 0 -> 2318 bytes docs/de/template.md | 3 + docs/en/img/picture.png | Bin 0 -> 2318 bytes docs/en/template.md | 3 + docs/es/img/picture.png | Bin 0 -> 2318 bytes docs/es/template.md | 3 + docs/fr/img/picture.png | Bin 0 -> 2318 bytes docs/fr/template.md | 3 + docs/it/img/picture.png | Bin 0 -> 2318 bytes docs/it/template.md | 3 + docs/nl/img/picture.png | Bin 0 -> 2318 bytes docs/nl/template.md | 3 + docs/pt/img/picture.png | Bin 0 -> 2318 bytes docs/pt/template.md | 3 + docs/ru/img/picture.png | Bin 0 -> 2318 bytes docs/ru/template.md | 3 + gulpfile.js | 485 ++++++++++++++++++++++ io-package.json | 78 ++++ lib/utils.js | 64 +++ main.js | 542 +++++++++++++++++++++++++ package.json | 44 ++ test/lib/setup.js | 696 ++++++++++++++++++++++++++++++++ test/testAdapter.js | 140 +++++++ test/testPackageFiles.js | 47 +++ 44 files changed, 2723 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 admin/i18n/de/translations.json create mode 100644 admin/i18n/en/translations.json create mode 100644 admin/i18n/es/translations.json create mode 100644 admin/i18n/fr/translations.json create mode 100644 admin/i18n/it/translations.json create mode 100644 admin/i18n/nl/translations.json create mode 100644 admin/i18n/pt/translations.json create mode 100644 admin/i18n/ru/translations.json create mode 100644 admin/index.html create mode 100644 admin/index_m.html create mode 100644 admin/onvif.png create mode 100644 admin/onvif_logo.png create mode 100644 admin/template.png create mode 100644 admin/words.js create mode 100644 appveyor.yml create mode 100644 docs/de/img/picture.png create mode 100644 docs/de/template.md create mode 100644 docs/en/img/picture.png create mode 100644 docs/en/template.md create mode 100644 docs/es/img/picture.png create mode 100644 docs/es/template.md create mode 100644 docs/fr/img/picture.png create mode 100644 docs/fr/template.md create mode 100644 docs/it/img/picture.png create mode 100644 docs/it/template.md create mode 100644 docs/nl/img/picture.png create mode 100644 docs/nl/template.md create mode 100644 docs/pt/img/picture.png create mode 100644 docs/pt/template.md create mode 100644 docs/ru/img/picture.png create mode 100644 docs/ru/template.md create mode 100644 gulpfile.js create mode 100644 io-package.json create mode 100644 lib/utils.js create mode 100644 main.js create mode 100644 package.json create mode 100644 test/lib/setup.js create mode 100644 test/testAdapter.js create mode 100644 test/testPackageFiles.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7228c33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.git +.idea +node_modules +nbproject +admin/i18n/flat.txt +admin/i18n/*/flat.txt \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e966b4f --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +gulpfile.js +admin/i18n +tasks +node_modules +.idea +.git +/node_modules +test +.travis.yml +appveyor.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..575f250 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' +before_script: + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3088ed7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Kirov Ilya + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..11f395a --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +![Logo](admin/onvif_logo.png) +# yunkong2.onvif +================= + diff --git a/admin/i18n/de/translations.json b/admin/i18n/de/translations.json new file mode 100644 index 0000000..8d4781e --- /dev/null +++ b/admin/i18n/de/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Auto", + "Manual": "Manual", + "My select": "Mein Auswahl", + "on save adapter restarts with new config immediately": "Beim Speichern von Einstellungen der Adapter wird sofort neu gestartet.", + "template adapter settings": "Beispiel", + "test1": "Test 1", + "test2": "Test 2" +} \ No newline at end of file diff --git a/admin/i18n/en/translations.json b/admin/i18n/en/translations.json new file mode 100644 index 0000000..bbd1135 --- /dev/null +++ b/admin/i18n/en/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Auto", + "Manual": "Manual", + "My select": "My select", + "on save adapter restarts with new config immediately": "on save adapter restarts with new config immediately", + "template adapter settings": "template adapter settings", + "test1": "Test 1", + "test2": "Test 2" +} \ No newline at end of file diff --git a/admin/i18n/es/translations.json b/admin/i18n/es/translations.json new file mode 100644 index 0000000..d7760bb --- /dev/null +++ b/admin/i18n/es/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Auto", + "Manual": "Manual", + "My select": "Mi seleccion", + "on save adapter restarts with new config immediately": "en el adaptador de guardar se reinicia con nueva configuración de inmediato", + "template adapter settings": "configuración del adaptador de plantilla", + "test1": "Prueba 1", + "test2": "Prueba 2" +} \ No newline at end of file diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr/translations.json new file mode 100644 index 0000000..9aec7fb --- /dev/null +++ b/admin/i18n/fr/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Auto", + "Manual": "Manuel", + "My select": "Mon choix", + "on save adapter restarts with new config immediately": "sur l'adaptateur de sauvegarde redémarre avec la nouvelle config immédiatement", + "template adapter settings": "paramètres de l'adaptateur de modèle", + "test1": "Test 1", + "test2": "Test 2" +} \ No newline at end of file diff --git a/admin/i18n/it/translations.json b/admin/i18n/it/translations.json new file mode 100644 index 0000000..4f3a7f2 --- /dev/null +++ b/admin/i18n/it/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Auto", + "Manual": "Manuale", + "My select": "La mia selezione", + "on save adapter restarts with new config immediately": "su save adapter si riavvia immediatamente con la nuova configurazione", + "template adapter settings": "impostazioni dell'adattatore del modello", + "test1": "Test 1", + "test2": "Test 2" +} \ No newline at end of file diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl/translations.json new file mode 100644 index 0000000..8d16744 --- /dev/null +++ b/admin/i18n/nl/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Auto", + "Manual": "Met de hand", + "My select": "Mijn select", + "on save adapter restarts with new config immediately": "on save-adapter wordt onmiddellijk opnieuw opgestart met nieuwe config", + "template adapter settings": "sjabloon-adapterinstellingen", + "test1": "Test 1", + "test2": "Test 2" +} \ No newline at end of file diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt/translations.json new file mode 100644 index 0000000..25110ec --- /dev/null +++ b/admin/i18n/pt/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Auto", + "Manual": "Manual", + "My select": "Meu selecionado", + "on save adapter restarts with new config immediately": "no adaptador de salvar reinicia com nova configuração imediatamente", + "template adapter settings": "configurações do adaptador de modelo", + "test1": "Teste 1", + "test2": "Teste 2" +} \ No newline at end of file diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru/translations.json new file mode 100644 index 0000000..5117fe1 --- /dev/null +++ b/admin/i18n/ru/translations.json @@ -0,0 +1,9 @@ +{ + "Auto": "Автоматически", + "Manual": "Вручную", + "My select": "Выбор", + "on save adapter restarts with new config immediately": "При сохранении настроек адаптера он сразу же перезапускается", + "template adapter settings": "Пример", + "test1": "Тест 1", + "test2": "Тест 2" +} \ No newline at end of file diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..6b79514 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +

template adapter settings

+

on save adapter restarts with new config immediately

+ +
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..ebc15a7 --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+ +
+
+ + diff --git a/admin/onvif.png b/admin/onvif.png new file mode 100644 index 0000000000000000000000000000000000000000..edb8ac24a5bcbeaeb8ee5b7361dcdb0521f35038 GIT binary patch literal 22458 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 literal 0 HcmV?d00001 diff --git a/admin/onvif_logo.png b/admin/onvif_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..72228cc076eb0e6cf64ecdc1fd6cdd0405f07c42 GIT binary patch literal 27381 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 b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); + + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); + langs[lang] = {}; + keys.forEach(function (word, i) { + langs[lang][word] = values[i]; + }); + + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} +function languages2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} + +gulp.task('adminWords2languages', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('adminWords2languagesFlat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('adminLanguagesFlat2words', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('adminLanguages2words', function (done) { + languages2words('./admin/'); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + var news = iopackage.common.news; + var newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('rename', function () { + var newname; + var author = '@@Author@@'; + var email = '@@email@@'; + for (var a = 0; a < process.argv.length; a++) { + if (process.argv[a] === '--name') { + newname = process.argv[a + 1] + } else if (process.argv[a] === '--email') { + email = process.argv[a + 1] + } else if (process.argv[a] === '--author') { + author = process.argv[a + 1] + } + } + + + console.log('Try to rename to "' + newname + '"'); + if (!newname) { + console.log('Please write the new template name, like: "gulp rename --name mywidgetset" --author "Author Name"'); + process.exit(); + } + if (newname.indexOf(' ') !== -1) { + console.log('Name may not have space in it.'); + process.exit(); + } + if (newname.toLowerCase() !== newname) { + console.log('Name must be lower case.'); + process.exit(); + } + if (fs.existsSync(__dirname + '/admin/template.png')) { + fs.renameSync(__dirname + '/admin/template.png', __dirname + '/admin/' + newname + '.png'); + } + if (fs.existsSync(__dirname + '/widgets/template.html')) { + fs.renameSync(__dirname + '/widgets/template.html', __dirname + '/widgets/' + newname + '.html'); + } + if (fs.existsSync(__dirname + '/widgets/template/js/template.js')) { + fs.renameSync(__dirname + '/widgets/template/js/template.js', __dirname + '/widgets/template/js/' + newname + '.js'); + } + if (fs.existsSync(__dirname + '/widgets/template')) { + fs.renameSync(__dirname + '/widgets/template', __dirname + '/widgets/' + newname); + } + var patterns = [ + { + match: /template/g, + replacement: newname + }, + { + match: /Template/g, + replacement: newname ? (newname[0].toUpperCase() + newname.substring(1)) : 'Template' + }, + { + match: /@@Author@@/g, + replacement: author + }, + { + match: /@@email@@/g, + replacement: email + } + ]; + var files = [ + __dirname + '/io-package.json', + __dirname + '/LICENSE', + __dirname + '/package.json', + __dirname + '/README.md', + __dirname + '/main.js', + __dirname + '/Gruntfile.js', + __dirname + '/widgets/' + newname +'.html', + __dirname + '/www/index.html', + __dirname + '/admin/index.html', + __dirname + '/admin/index_m.html', + __dirname + '/widgets/' + newname + '/js/' + newname +'.js', + __dirname + '/widgets/' + newname + '/css/style.css' + ]; + files.forEach(function (f) { + try { + if (fs.existsSync(f)) { + var data = fs.readFileSync(f).toString('utf-8'); + for (var r = 0; r < patterns.length; r++) { + data = data.replace(patterns[r].match, patterns[r].replacement); + } + fs.writeFileSync(f, data); + } + } catch (e) { + + } + }); +}); + +gulp.task('updateReadme', function (done) { + var readme = fs.readFileSync('README.md').toString(); + var pos = readme.indexOf('## Changelog\n'); + if (pos !== -1) { + var readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + var readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) === -1) { + var timestamp = new Date(); + var date = timestamp.getFullYear() + '-' + + ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ('0' + (timestamp.getDate()).toString(10)).slice(-2); + + var news = ''; + if (iopackage.common.news && iopackage.common.news[pkg.version]) { + news += '* ' + iopackage.common.news[pkg.version].en; + } + + fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + done(); +}); + +gulp.task('default', ['updatePackages', 'updateReadme']); diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..255319f --- /dev/null +++ b/io-package.json @@ -0,0 +1,78 @@ +{ + "common": { + "name": "onvif", + "version": "0.0.1", + "news": { + "0.0.1": { + "en": "initial adapter", + "de": "Initiale Version", + "ru": "Первоначальный адаптер", + "pt": "Versão inicial", + "fr": "Version initiale", + "nl": "Eerste release" + } + }, + "title": "Onvif adapter", + "titleLang": { + "en": "Onvif adapter", + "de": "Onvif Vorlagenadapter", + "ru": "Адаптер Onvif", + "pt": "Adaptador Onvif", + "nl": "Onvif sjabloonadapter", + "fr": "Onvif adaptateur", + "it": "Adattatore Onvif", + "es": "Adaptador Onvif" + }, + "desc": { + "en": "Onvif Adapter", + "de": "Onvif Adapter", + "ru": "Onvif драйвер", + "pt": "Onvif adaptador", + "fr": "Onvif adaptateur", + "nl": "Onvif Adapter", + "it": "Adattatore Onvif", + "es": "Adaptador Onvif" + }, + "authors": [ + "Kirov Ilya " + ], + "docs": { + "en": "docs/en/admin.md", + "ru": "docs/ru/admin.md", + "de": "docs/de/admin.md", + "es": "docs/es/admin.md", + "it": "docs/it/admin.md", + "fr": "docs/fr/admin.md", + "nl": "docs/nl/admin.md", + "pt": "docs/pt/admin.md" + }, + "platform": "Javascript/Node.js", + "mode": "daemon", + "icon": "onvif.png", + "materialize": true, + "enabled": true, + "extIcon": "https://git.spacen.net/kirovilya/yunkong2.onvif/raw/master/admin/onvif.png", + "keywords": ["onvif", "camera"], + "readme": "https://git.spacen.net/kirovilya/yunkong2.onvif/blob/master/README.md", + "loglevel": "info", + "type": "general", + "messagebox": true + }, + "native": { + + }, + "instanceObjects": [ + { + "_id": "discoveryRunning", + "type": "state", + "common": { + "name": "Scannig mode", + "type": "boolean", + "read": true, + "write": false, + "def": false + }, + "native": {} + } + ] +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..bcbe98a --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,64 @@ +var controllerDir; +var appName; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +// Get js-controller directory to load libs +function getControllerDir(isInstall) { + var fs = require('fs'); + // Find the js-controller location + var controllerDir = __dirname.replace(/\\/g, '/'); + controllerDir = controllerDir.split('/'); + if (controllerDir[controllerDir.length - 3] === 'adapter') { + controllerDir.splice(controllerDir.length - 3, 3); + controllerDir = controllerDir.join('/'); + } else if (controllerDir[controllerDir.length - 3] === 'node_modules') { + controllerDir.splice(controllerDir.length - 3, 3); + controllerDir = controllerDir.join('/'); + if (fs.existsSync(controllerDir + '/node_modules/' + appName + '.js-controller')) { + controllerDir += '/node_modules/' + appName + '.js-controller'; + } else if (fs.existsSync(controllerDir + '/node_modules/' + appName.toLowerCase() + '.js-controller')) { + controllerDir += '/node_modules/' + appName.toLowerCase() + '.js-controller'; + } else if (!fs.existsSync(controllerDir + '/controller.js')) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + } else if (fs.existsSync(__dirname + '/../../node_modules/' + appName.toLowerCase() + '.js-controller')) { + controllerDir.splice(controllerDir.length - 2, 2); + return controllerDir.join('/') + '/node_modules/' + appName.toLowerCase() + '.js-controller'; + } else { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + return controllerDir; +} + +// Read controller configuration file +function getConfig() { + var fs = require('fs'); + if (fs.existsSync(controllerDir + '/conf/' + appName + '.json')) { + return JSON.parse(fs.readFileSync(controllerDir + '/conf/' + appName + '.json')); + } else if (fs.existsSync(controllerDir + '/conf/' + appName.toLowerCase() + '.json')) { + return JSON.parse(fs.readFileSync(controllerDir + '/conf/' + appName.toLowerCase() + '.json')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = require(controllerDir + '/lib/adapter.js'); +exports.appName = appName; diff --git a/main.js b/main.js new file mode 100644 index 0000000..726b911 --- /dev/null +++ b/main.js @@ -0,0 +1,542 @@ +/** + * + * 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) ); +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f102965 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "yunkong2.onvif", + "version": "0.0.1", + "description": "yunkong2 onvif dapter", + "author": { + "name": "Kirov Ilya", + "email": "kirovilya@gmail.com" + }, + "contributors": [ + { + "name": "Kirov Ilya", + "email": "kirovilya@gmail.com" + } + ], + "homepage": "https://git.spacen.net/kirovilya/yunkong2.onvif", + "license": "MIT", + "keywords": [ + "yunkong2", + "onvif", + "camera" + ], + "repository": { + "type": "git", + "url": "git+https://git.spacen.net/kirovilya/yunkong2.onvif.git" + }, + "dependencies": { + "onvif": "^0.6.0", + "nimble": "^0.0.2", + "onvif-snapshot": "^1.0.2" + }, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^4.1.0", + "chai": "^4.1.2" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "bugs": { + "url": "https://git.spacen.net/kirovilya/yunkong2.onvif/issues" + }, + "readmeFilename": "README.md" +} diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..a5d9de0 --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,696 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +var fs = require('fs'); +var path = require('path'); +var child_process = require('child_process'); +var rootDir = path.normalize(__dirname + '/../../'); +var pkg = require(rootDir + 'package.json'); +var debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +var adapterStarted = false; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +var appName = getAppName().toLowerCase(); + +var objects; +var states; + +var pid = null; + +function copyFileSync(source, target) { + + var targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + fs.writeFileSync(targetFile, fs.readFileSync(source)); +} + +function copyFolderRecursiveSync(source, target, ignore) { + var files = []; + + var base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + var targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + var curSource = path.join(source, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, targetFolder); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(function() { + checkIsAdapterInstalled(cb, counter + 1); + }, 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(function() { + checkIsControllerInstalled(cb, counter + 1); + }, 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, function () { + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + var _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, function () { + checkIsControllerInstalled(function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + var client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', function() { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(function () { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://git.spacen.net/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/yunkong2-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + if (cb) cb(); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) callback(objects, states); + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + startAdapter(objects, states, callback); + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/testAdapter.js b/test/testAdapter.js new file mode 100644 index 0000000..ae9c289 --- /dev/null +++ b/test/testAdapter.js @@ -0,0 +1,140 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + console.log('Try check #' + counter); + if (counter > 30) { + if (cb) cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error(err); + if (state && state.val) { + if (cb) cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + if (cb) cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error(err); + if (value === null && !state) { + if (cb) cb(); + } else + if (state && (value === undefined || state.val === value)) { + if (cb) cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test ' + adapterShortName + ' adapter', function() { + before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + //config.native.dbtype = 'sqlite'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + +/* + ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY +*/ + it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + done(); + }); + }); + }); +/**/ + +/* + PUT YOUR OWN TESTS HERE USING + it('Testname', function ( done) { + ... + }); + + You can also use "sendTo" method to send messages to the started adapter +*/ + + after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..a85d1e3 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,47 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version).to.exist; + expect(npmPackage.version).to.exist; + + if (!expect(ioPackage.common.version).to.be.equal(npmPackage.version)) { + console.log('ERROR: Version numbers in package.json and io-package.json differ!!'); + } + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + } + + expect(ioPackage.common.authors).to.exist; + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length).to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0]).to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors).to.not.be.equal('my Name '); + } + } + else { + console.log('Testing for set authors field in io-package skipped because template adapter'); + } + done(); + }); +});