From 3d6226b615acb3cde57f66e9d0aa57e68287c08e Mon Sep 17 00:00:00 2001 From: zhongjin Date: Tue, 25 Sep 2018 10:42:52 +0800 Subject: [PATCH] Initial commit --- .gitignore | 6 + .npmignore | 10 + .travis.yml | 23 + LICENSE | 21 + README.md | 95 +++++ 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 | 89 ++++ admin/index_m.html | 128 ++++++ admin/template.png | Bin 0 -> 2318 bytes admin/words.js | 13 + appveyor.yml | 25 ++ 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 | 489 +++++++++++++++++++++ io-package.json | 89 ++++ lib/utils.js | 83 ++++ main.js | 158 +++++++ package.json | 41 ++ test/lib/setup.js | 728 ++++++++++++++++++++++++++++++++ test/testAdapter.js | 140 ++++++ test/testPackageFiles.js | 91 ++++ widgets/template.html | 138 ++++++ widgets/template/css/style.css | 3 + widgets/template/img/test.png | Bin 0 -> 3619 bytes widgets/template/js/template.js | 68 +++ www/README.md | 2 + www/index.html | 4 + 48 files changed, 2540 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/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 create mode 100644 widgets/template.html create mode 100644 widgets/template/css/style.css create mode 100644 widgets/template/img/test.png create mode 100644 widgets/template/js/template.js create mode 100644 www/README.md create mode 100644 www/index.html 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..2456688 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' + - '10' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install winston@2.3.1 + - 'npm install https://github.com/ioBroker/ioBroker.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..d91b0ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 @@Author@@ <@@email@@> + +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..5441ffc --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +![Logo](admin/template.png) +# ioBroker.template +================= + +This adapter is a template for the creation of an ioBroker adapter. You do not need it at least that you plan developing your own adapter. + +It includes both code running within iobroker and as vis widget. If you only plan to create a vis widget then you should use the [iobroker.vis-template](https://github.com/ioBroker/ioBroker.vis-template) instead. + +## Steps +1. download and unpack this packet from github ```https://github.com/ioBroker/ioBroker.template/archive/master.zip``` + or clone git repository ```git clone --depth=1 https://github.com/ioBroker/ioBroker.template.git``` + +2. download required npm packets. Write in ioBroker.template directory: + + ```npm install``` + +3. set name of this template. Call + + ```gulp rename --name mynewname --email email@mail.com --author "Author Name"``` + + *mynewname* must be **lower** case and with no spaces. + + If gulp is not available, install gulp globally: + + ```npm install -g gulp-cli``` + +4. rename directory from *ioBroker.template* (can be *ioBroker.template-master*) to *iobroker.mynewname* + +5. to use this template you should copy it into *.../iobroker/node_modules* directory and then create an instance for it with iobroker.admin + +6. create your adapter: + + * you might want to start with main.js (code running within iobroker) and admin/index.html (the adapter settings page). + + * [Adapter-Development-Documentation](https://github.com/ioBroker/ioBroker/wiki/Adapter-Development-Documentation), + + * [Installation, setup and first steps with an ioBroker Development Environment](https://github.com/ioBroker/ioBroker/wiki/Installation,-setup-and-first-steps-with-an-ioBroker-Development-Environment) + + * [Write and debug vis widgets](https://github.com/ioBroker/ioBroker/wiki/How-to-debug-vis-and-to-write-own-widget-set) + + * files under the www folders are made available under http://<iobrokerIP>:8082/<adapter-name>/ + * for this to work the iobroker.vis adapter has to be installed + * delete this folder if you do not plan to export any files this way + * call ```iobroker upload ``` after you change files in the www folder to get the new files uploaded to vis + * the widget folder contains an example of a vis widget + * you might want to start with *widget/.html* and *widget/js/.js* + * call ```iobroker visdebug ``` to enable debugging and upload widget to "vis". (This works only from V0.7.15 of js-controller) + * If you do not plan to export any widget then delete the whole widget folder and remove the ```"restartAdapters": ["vis"]``` statement from *io-package.json* + * After admin/index.html is changed you must execute ```iobroker upload mynewname``` to see changes in admin console. The same is valid for any files in *admin* and *www* directory + +7. change version: edit package.json and then call ```grunt p``` in your adapter directory. + +8. share it with the community + +## Requirements +* your github repository must have name "ioBroker.". **B** is capital in "ioBroker", but in the package.json the *name* must be low case, because npm does not allow upper case letters. +* *title* in io-package.json (common) is simple short name of adapter in english. *titleLang* is object that consist short names in many languages. *Lang* ist not german Länge, but english LANGuages. +* Do not use in the title the words "ioBroker" or "Adapter". It is clear anyway, that it is adapter for ioBroker. + +## Changelog + +### 0.6.0 (2017.01.02) +* (bluefox) Support of admin3 + +### 0.5.0 +* (vegetto) include vis widget + +### 0.4.0 +* (bluefox) fix errors with grunt + +### 0.2.0 +* (bluefox) initial release + +## License +The MIT License (MIT) + +Copyright (c) 2018 @@Author@@ <@@email@@> + +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/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..deda2a6 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +

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..7d44cdb --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ + + Descriptions of the input field +
+
+ + + + test2 +
+
+ + + + Verification input +
+
+
+
+ + + +
+
+ mode_edit + + +
+
+
+
+

on save adapter restarts with new config immediately

+
+
+
+
+ + + diff --git a/admin/template.png b/admin/template.png new file mode 100644 index 0000000000000000000000000000000000000000..a16caf481d8d433b352db3e8e58feaa441275dcd GIT binary patch literal 2318 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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: /ioBroker template Adapter/g, + replacement: newname + }, + { + 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 + '/gulpfile.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..86cf35d --- /dev/null +++ b/io-package.json @@ -0,0 +1,89 @@ +{ + "common": { + "name": "template", + "version": "0.6.0", + "news": { + "0.6.0": { + "en": "Support of admin3", + "de": "Unterstützung von admin3", + "ru": "Поддержка admin3", + "pt": "Suporte de admin3", + "nl": "Ondersteuning van admin3", + "fr": "Support de admin3", + "it": "Supporto di admin3", + "es": "Soporte de admin3" + }, + "0.5.0": { + "en": "beta version", + "de": "Betaversion", + "ru": "Бета версия", + "pt": "Versão beta", + "fr": "Version bêta", + "nl": "Beta versie" + }, + "0.0.1": { + "en": "initial adapter", + "de": "Initiale Version", + "ru": "Первоначальный адаптер", + "pt": "Versão inicial", + "fr": "Version initiale", + "nl": "Eerste release" + } + }, + "title": "Javascript/Node.js based template", + "titleLang": { + "en": "Javascript/Node.js based template", + "de": "Javascript / Node.js basierte Vorlagenadapter", + "ru": "Адаптер шаблонов на основе Javascript / Node.js", + "pt": "Adaptador de modelo baseado em Javascript / Node.js", + "nl": "Javascript / Node.js gebaseerde sjabloonadapter", + "fr": "Javascript / Node.js basé adaptateur de modèle", + "it": "Adattatore modello basato su Javascript / Node.js", + "es": "Adaptador de plantilla basado en JavaScript / Node.js" + }, + "desc": { + "en": "ioBroker template", + "de": "ioBroker Template", + "ru": "ioBroker Template как образец", + "pt": "Modelo de adaptador para o ioBroker", + "fr": "ioBroker adaptateur modèle", + "nl": "ioBroker Template", + "it": "Adattatore modello ioBroker", + "es": "Adaptador de plantilla ioBroker" + }, + "authors": [ + "@@Author@@ <@@email@@>" + ], + "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": "template.png", + "materialize": true, + "enabled": true, + "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.template/master/admin/template.png", + "keywords": ["template", "vis", "GUI", "graphical", "scada"], + "readme": "https://github.com/ioBroker/ioBroker.template/blob/master/README.md", + "loglevel": "info", + "type": "general", + "license": "MIT", + "messagebox": false, + "restartAdapters": ["vis"] + }, + "native": { + "test1": true, + "test2": 42, + "mySelect": "auto" + }, + "objects": [ + + ] +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..c8a0eb7 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,83 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +let controllerDir; +let appName; + +/** + * returns application name + * + * The name of the application can be different and this function finds it out. + * + * @returns {string} + */ + function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +/** + * looks for js-controller home folder + * + * @param {boolean} isInstall + * @returns {string} + */ +function getControllerDir(isInstall) { + // Find the js-controller location + const possibilities = [ + 'iobroker.js-controller', + 'ioBroker.js-controller', + ]; + /** @type {string} */ + let controllerPath; + for (const pkg of possibilities) { + try { + const possiblePath = require.resolve(pkg); + if (fs.existsSync(possiblePath)) { + controllerPath = possiblePath; + break; + } + } catch (e) { /* not found */ } + } + if (controllerPath == null) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/main.js b/main.js new file mode 100644 index 0000000..a64234f --- /dev/null +++ b/main.js @@ -0,0 +1,158 @@ +/** + * + * template adapter + * + * + * file io-package.json comments: + * + * { + * "common": { + * "name": "template", // name has to be set and has to be equal to adapters folder name and main file name excluding extension + * "version": "0.0.0", // use "Semantic Versioning"! see http://semver.org/ + * "title": "Node.js template Adapter", // Adapter title shown in User Interfaces + * "authors": [ // Array of authord + * "name " + * ] + * "desc": "template adapter", // Adapter description shown in User Interfaces. Can be a language object {de:"...",ru:"..."} or a string + * "platform": "Javascript/Node.js", // possible values "javascript", "javascript/Node.js" - more coming + * "mode": "daemon", // possible values "daemon", "schedule", "subscribe" + * "materialize": true, // support of admin3 + * "schedule": "0 0 * * *" // cron-style schedule. Only needed if mode=schedule + * "loglevel": "info" // Adapters Log Level + * }, + * "native": { // the native object is available via adapter.config in your adapters code - use it for configuration + * "test1": true, + * "test2": 42, + * "mySelect": "auto" + * } + * } + * + */ + +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; + +// you have to require the utils module and call adapter function +const 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 +const adapter = new utils.Adapter('template'); + +/*Variable declaration, since ES6 there are let to declare variables. Let has a more clearer definition where +it is available then var.The variable is available inside a block and it's childs, but not outside. +You can define the same variable name inside a child without produce a conflict with the variable of the parent block.*/ +let variable = 1234; + +// is called when adapter shuts down - callback has to be called under any circumstances! +adapter.on('unload', function (callback) { + try { + adapter.log.info('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.info('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.info('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.info('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 (typeof obj === 'object' && obj.message) { + if (obj.command === 'send') { + // e.g. send email or pushover or whatever + console.log('send command'); + + // Send response in callback if required + if (obj.callback) adapter.sendTo(obj.from, obj.command, 'Message received', obj.callback); + } + } +}); + +// is called when databases are connected and adapter received configuration. +// start here! +adapter.on('ready', function () { + main(); +}); + +function main() { + + // The adapters config (in the instance object everything under the attribute "native") is accessible via + // adapter.config: + adapter.log.info('config test1: ' + adapter.config.test1); + adapter.log.info('config test1: ' + adapter.config.test2); + adapter.log.info('config mySelect: ' + adapter.config.mySelect); + + + /** + * + * For every state in the system there has to be also an object of type state + * + * Here a simple template for a boolean variable named "testVariable" + * + * Because every adapter instance uses its own unique namespace variable names can't collide with other adapters variables + * + */ + + adapter.setObject('testVariable', { + type: 'state', + common: { + name: 'testVariable', + type: 'boolean', + role: 'indicator' + }, + native: {} + }); + + // in this template all states changes inside the adapters namespace are subscribed + adapter.subscribeStates('*'); + + + /** + * setState examples + * + * you will notice that each setState will cause the stateChange event to fire (because of above subscribeStates cmd) + * + */ + + // the variable testVariable is set to true as command (ack=false) + adapter.setState('testVariable', true); + + // same thing, but the value is flagged "ack" + // ack should be always set to true if the value is received from or acknowledged from the target system + adapter.setState('testVariable', {val: true, ack: true}); + + // same thing, but the state is deleted after 30s (getState will return null afterwards) + adapter.setState('testVariable', {val: true, ack: true, expire: 30}); + + + + // examples for the checkPassword/checkGroup functions + adapter.checkPassword('admin', 'iobroker', function (res) { + console.log('check user admin pw ioboker: ' + res); + }); + + adapter.checkGroup('admin', 'admin', function (res) { + console.log('check group user admin group admin: ' + res); + }); + + + +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..79407ad --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "iobroker.template", + "version": "0.6.0", + "description": "ioBroker template Adapter", + "author": { + "name": "@@Author@@", + "email": "@@email@@" + }, + "contributors": [ + { + "name": "@@Author@@", + "email": "@@email@@" + } + ], + "homepage": "https://github.com/ioBroker/ioBroker.template", + "license": "MIT", + "keywords": [ + "ioBroker", + "template", + "Smart Home", + "home automation" + ], + "repository": { + "type": "git", + "url": "https://github.com/ioBroker/ioBroker.template" + }, + "dependencies": {}, + "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://github.com/ioBroker/ioBroker.template/issues" + }, + "readmeFilename": "README.md" +} \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..16857ed --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,728 @@ +/* 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)); + } + } + + try { + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + catch (err) { + console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)"); + } +} + +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); + var curTarget = path.join(targetFolder, 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, curTarget); + } + }); + } +} + +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://github.com/' + 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/iobroker-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(); + } + // read system.config object + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +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); + callback = null; + } + } + } + }, + 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!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + 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..c600a60 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,91 @@ +/* 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) { + console.log(); + + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + 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!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('iobroker') !== -1 || + ioPackage.common.title.indexOf('ioBroker') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.'); + console.log(); + } + + if (ioPackage.common.name.indexOf('vis-') !== 0) { + if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + console.log('WARNING: Admin3 support is missing! Please add it'); + console.log(); + } + if (ioPackage.common.materialize) { + expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + } + } + + var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +}); diff --git a/widgets/template.html b/widgets/template.html new file mode 100644 index 0000000..ff9e4cf --- /dev/null +++ b/widgets/template.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + diff --git a/widgets/template/css/style.css b/widgets/template/css/style.css new file mode 100644 index 0000000..6c01b33 --- /dev/null +++ b/widgets/template/css/style.css @@ -0,0 +1,3 @@ +.template-class { + font-style: italic; +} diff --git a/widgets/template/img/test.png b/widgets/template/img/test.png new file mode 100644 index 0000000000000000000000000000000000000000..883c891b4e7c24f50aeddff31b5c3105d5ffe9a5 GIT binary patch literal 3619 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 qRO{A2ADYl#f7gGHKvmPG3I+H^P&F0v + +*/ +"use strict"; + +// add translations for edit mode +if (vis.editMode) { + $.extend(true, systemDictionary, { + "myColor": {"en": "myColor", "de": "mainColor", "ru": "Мой цвет"}, + "myColor_tooltip": { + "en": "Description of\x0AmyColor", + "de": "Beschreibung von\x0AmyColor", + "ru": "Описание\x0AmyColor" + }, + "htmlText": {"en": "htmlText", "de": "htmlText", "ru": "htmlText"}, + "group_extraMyset": {"en": "extraMyset", "de": "extraMyset", "ru": "extraMyset"}, + "extraAttr": {"en": "extraAttr", "de": "extraAttr", "ru": "extraAttr"} + }); +} + +// add translations for non-edit mode +$.extend(true, systemDictionary, { + "Instance": {"en": "Instance", "de": "Instanz", "ru": "Инстанция"} +}); + +// this code can be placed directly in template.html +vis.binds.template = { + version: "0.5.0", + showVersion: function () { + if (vis.binds.template.version) { + console.log('Version template: ' + vis.binds.template.version); + vis.binds.template.version = null; + } + }, + createWidget: function (widgetID, view, data, style) { + var $div = $('#' + widgetID); + // if nothing found => wait + if (!$div.length) { + return setTimeout(function () { + vis.binds.template.createWidget(widgetID, view, data, style); + }, 100); + } + + var text = ''; + text += 'OID: ' + data.oid + '
'; + text += 'OID value: ' + vis.states[data.oid + '.val'] + '
'; + text += 'Color: ' + data.myColor + '
'; + text += 'extraAttr: ' + data.extraAttr + '
'; + text += 'Browser instance: ' + vis.instance + '
'; + text += 'htmlText:
'; + + $('#' + widgetID).html(text); + + // subscribe on updates of value + if (data.oid) { + vis.states.bind(data.oid + '.val', function (e, newVal, oldVal) { + $div.find('.template-value').html(newVal); + }); + } + } +}; + +vis.binds.template.showVersion(); diff --git a/www/README.md b/www/README.md new file mode 100644 index 0000000..3b8083a --- /dev/null +++ b/www/README.md @@ -0,0 +1,2 @@ +if you put files in this directory they will be uploaded to DB on adapter install/upgrade. +they can then be accessed via ioBroker WEB adapter http://<iobrokerIP>:8082/<adapter-name>/... \ No newline at end of file diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..23d1ff7 --- /dev/null +++ b/www/index.html @@ -0,0 +1,4 @@ +

template adapter

+

+ +

\ No newline at end of file