From d9ce220c936bc00d9f1c104495c3e5f51b80f122 Mon Sep 17 00:00:00 2001 From: zhongjin Date: Thu, 9 Aug 2018 17:23:06 +0800 Subject: [PATCH] Initial commit --- .gitignore | 3 + .npmignore | 10 + .travis.yml | 28 ++ LICENSE | 21 + README.md | 113 +++++ admin/ham.png | Bin 0 -> 6158 bytes admin/index_m.html | 219 +++++++++ admin/words.js | 26 ++ appveyor.yml | 35 ++ 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 | 406 ++++++++++++++++ io-package.json | 49 ++ lib/global-handler.js | 280 +++++++++++ lib/mapper.js | 276 +++++++++++ lib/utils.js | 83 ++++ lib/wrapper-handler.js | 148 ++++++ main.js | 300 ++++++++++++ package.json | 48 ++ test/homebridge/config.json | 133 ++++++ test/lib/setup.js | 728 +++++++++++++++++++++++++++++ test/testAdapterWrapperNoInfo.js | 365 +++++++++++++++ test/testAdapterWrapperWithInfo.js | 364 +++++++++++++++ test/testAdapterXGlobal.js | 233 +++++++++ test/testPackageFiles.js | 93 ++++ 39 files changed, 3985 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/ham.png create mode 100644 admin/index_m.html 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/global-handler.js create mode 100644 lib/mapper.js create mode 100644 lib/utils.js create mode 100644 lib/wrapper-handler.js create mode 100644 main.js create mode 100644 package.json create mode 100644 test/homebridge/config.json create mode 100644 test/lib/setup.js create mode 100644 test/testAdapterWrapperNoInfo.js create mode 100644 test/testAdapterWrapperWithInfo.js create mode 100644 test/testAdapterXGlobal.js create mode 100644 test/testPackageFiles.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aff4100 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/package-lock.json +/node_modules 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..b688464 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +os: + - linux + - osx +language: node_js +node_js: + - '6' + - '8' + - '10' +before_install: + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then CC=gcc-4.9; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then CXX=g++-4.9; fi' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install -g homebridge + - npm install -g homebridge-http-webhooks + - npm install -g homebridge-sun-position + - export NODE_GLOBAL_DIR=$(npm root -g) + - npm install winston@2.3.1 + - 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production' +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 + - libavahi-compat-libdnssd-dev diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9eb25a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Apollon77 + +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..cf20e64 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +![Logo](admin/ham.png) +# yunkong2 Homebridge accessories manager +================= + +Use Homebridge plugins in yunkong2 or run a global installed Homebridge as yunkong2 adapter. +All States from Homebridge will be available in yunkong2 too and can also be controlled there. + +## Description +This adapter provides two different modes: + +### Default (Wrapper) Mode +In the default mode the adapter allows you to use homebridge Plugin Modules directly. +You can explore all available plugins at the NPM website by [searching for the keyword `homebridge-plugin`](https://www.npmjs.com/search?q=homebridge-plugin). + +You simply add the list of modules to the Adapter configuration and provide the configuration +in the JSON-editor (see Plugin descriptions). +After this all Homebridge objects will be created in yunkong2 too and all writable objects can +be changed too. + +A link of successfully tried plugins with examples can be found here: https://forum.yunkong2.net/viewtopic.php?f=20&t=15021 + +### Global-Homebridge-Mode +If you already use Homebridge (Apple OpenSource SmartHome) to control your devices +then you can use this existing Homebridge installation and start this Homebridge +installation as yunkong2 process. In this case the Homebridge server is started by yunkong2. +Additionally all states from Homebridge are available as states in yunkong2 and allows to +control from yunkong2. + +For this to work you need to provide the location of the systems global node-modules folder. For this call **npm root -g**. Additionally you need to privide the path of the homebridge configuration directory (usually .homebridge in the users folder). + +## Following adapters were tested in Default mode + +* homebridge-chamberlain v1.0.1 - plugin for Chamberlain garage door openers with MyQ +* homebridge-doorbird v0.0.4 - Plugin for Doorbird +* homebridge-dyson-link v2.2.2 - Dyson Link devices +* homebridge-edomoticz v2.1.11 - A fully-fledged up-to-date Plugin for Domoticz +* homebridge-Fibaro-HC2 v2.1.5 - Fibaro HomeCenter integration +* homebridge-homee v0.2.4 - A fully-fledged up-to--date Plugin for Homee +* homebridge-ikea-tradfri-gateway v1.0.26 - Tradfri +* homebridge-noolite v0.0.29 - Noolite via USB MTRF-64 or МТRF-64 modules +* homebridge-platform-wemo v1.0.1 - Belkin WeMo Platform plugin +* homebridge-seasons v1.0.1 - A plugin to display the current season of the year. +* homebridge-vera v0.8.2 - VeraLink is an application for Z-Wave accessories from Vera (Node.js 8.11.3) + + +## TODO +* Tests +* More documentation?! + +## Changelog + +### 0.4.4 (2018.08.07) +* (Apollon77) corrected automatic role determination and bugs fixed + +### 0.4.2 (2018.06.25) +* (Apollon77) Fix for global mode + +### 0.4.1 (2018.06.21) +* (Apollon77) option to poll values from the plugins added and other optimizations + +### 0.3.1 (2018.06.20) +* (kirovilya) Fixed a bug in global mode that values were not reported back to iOS devices + +### 0.3.0 (2018.06.20) +* (bluefox) Support of ham plugins was added + +### 0.2.6 (2018.06.19) +* (Apollon77) Updates for Homebridge-Wrapper + +### 0.2.5 (2018.06.18) +* (Apollon77) Catch all console logs from Homegridge and make available as debug log + +### 0.2.4 (2018.06.18) +* (Apollon77) Updates for Homebridge-Wrapper + +### 0.2.3 (2018.06.17) +* (Apollon77) Updates for Homebridge-Wrapper + +### 0.2.2 (2018.06.17) +* (Bluefox) Fixes for JSON editor in Firefox and Chrome + +### 0.2.0/0.2.1 (2018.06.17) +* (Apollon77) Public test version with both modes +* (Bluefox) Admin3 + +### 0.1.0 (2018.06.09) +* (Apollon77) Update for working mode 1 + +### 0.0.1 (2018.03.24) +* (kirovilya) initial commit + +## License +The MIT License (MIT) + +Copyright (c) 2018 Apollon77 + +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/ham.png b/admin/ham.png new file mode 100644 index 0000000000000000000000000000000000000000..4be833dc27ecda671e38aca7abd855ee7eb11f36 GIT binary patch literal 6158 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 Wp;t~BsXcoYZ+gw@YMr4+!hZq2YpLb{ literal 0 HcmV?d00001 diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..d231da5 --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+
+
+
+ + useGlobalHomebridge +
+
+
+
+ + + Global Homebridge Path +
+
+ + + Global Homebridge Config Directory Path +
+
+
+
+ +
+
+
+
+
+ s + + Value Polling Interval +
+
+
+
+
Wrapper-Konfiguration (Non-Global-Homebridge)
+
+
+
+ + + diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..16c8a95 --- /dev/null +++ b/admin/words.js @@ -0,0 +1,26 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n +/*global systemDictionary:true */ +'use strict'; + +systemDictionary = { + "Add module": { "en": "Add module", "de": "Modul hinzufügen", "ru": "Добавить модуль", "pt": "Adicionar módulo", "nl": "Module toevoegen", "fr": "Add module", "it": "Aggiungi modulo", "es": "Agregar módulo", "pl": "Dodaj moduł"}, + "Additional npm modules:": { "en": "Additional npm modules:", "de": "Zusätzliche npm-Module:", "ru": "Дополнительные модули npm:", "pt": "Módulos adicionais npm:", "nl": "Extra npm-modules:", "fr": "Additional npm modules:", "it": "Moduli aggiuntivi di npm:", "es": "Módulos npm adicionales:", "pl": "Dodatkowe moduły npm:"}, + "Configuration file": { "en": "Configuration file", "de": "Konfigurationsdatei", "ru": "Файл конфигурации", "pt": "Arquivo de configuração", "nl": "Configuratiebestand", "fr": "Configuration file", "it": "File di configurazione", "es": "Archivo de configuración", "pl": "Plik konfiguracyjny"}, + "Global Homebridge Config Directory Path": { "en": "Global Homebridge Config Directory Path", "de": "Globaler Homebridge-Konfigurationsverzeichnispfad", "ru": "Путь к каталогу Global Homebridge Config", "pt": "Caminho global do diretório de configuração do Homebridge", "nl": "Wereldwijd Homebridge Config Directory-pad", "fr": "Global Homebridge Config Directory Path", "it": "Percorso globale della directory di configurazione di Homebridge", "es": "Ruta del directorio de Global Homebridge Config", "pl": "Ścieżka katalogu konfiguracji globalnej Homebridge"}, + "Global Homebridge Path": { "en": "Global Homebridge Path", "de": "Globaler Homebridge-Pfad", "ru": "Глобальный путь на мосту", "pt": "Caminho Global Homebridge", "nl": "Wereldwijd homebridge pad", "fr": "Global Homebridge Path", "it": "Percorso globale di Homebridge", "es": "Ruta Global Homebridge", "pl": "Global Homebridge Path"}, + "Main settings": { "en": "Main settings", "de": "Haupteinstellungen", "ru": "Основные настройки", "pt": "Configurações principais", "nl": "Belangrijkste instellingen", "fr": "Réglages principaux", "it": "Impostazioni principali", "es": "Ajustes principales", "pl": "Ustawienia główne"}, + "Module names": { "en": "Module names", "de": "Modulnamen", "ru": "Имена модулей", "pt": "Nomes de módulos", "nl": "Module namen", "fr": "Module names", "it": "Nomi dei moduli", "es": "Nombres de módulos", "pl": "Nazwy modułów"}, + "useGlobalHomebridge": { "en": "Use global Homebridge", "de": "Verwenden Sie globale Homebridge", "ru": "Использовать глобальный Homebridge", "pt": "Use Homebridge global", "nl": "Gebruik wereldwijde Homebridge", "fr": "useGlobalHomebridge", "it": "Usa Homebridge globale", "es": "Utilice Global Homebridge", "pl": "Użyj globalnego rozwiązania Homebridge"}, + "wrapperConfig": { "en": "Wrapper configuration", "de": "Wrapper-Konfiguration", "ru": "Конфигурация Wrapper", "pt": "Configuração do invólucro", "nl": "Wrapper-configuratie", "fr": "wrapperConfig", "it": "Configurazione wrapper", "es": "Configuración del contenedor", "pl": "Konfiguracja opakowania"}, + "Value Polling Interval": { + "en": "Value Polling Interval", + "de": "Wertabfrageintervall", + "ru": "Интервал опроса значений", + "pt": "Intervalo de Polling de Valor", + "nl": "Value Polling Interval", + "fr": "Intervalle d'interrogation de valeur", + "it": "Intervallo di polling del valore", + "es": "Intervalo de interrogación de valor", + "pl": "Okres odpytywania wartości" + } +}; diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..a4c0a3c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,35 @@ +version: 'test-{build}' +environment: + matrix: + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - ps: '$NpmVersion = (npm -v).Substring(0,1)' + - ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }' + - ps: npm --version + - 'appveyor DownloadFile https://github.com/Apollon77/SupportingFiles/raw/master/appveyor/bonjour/bonjourcore2.msi' + - msiexec /i bonjourcore2.msi /qn + - del bonjourcore2.msi + - 'appveyor DownloadFile https://github.com/Apollon77/SupportingFiles/raw/master/appveyor/bonjour/bonjoursdksetup.exe' + - bonjoursdksetup.exe /quiet + - del bonjoursdksetup.exe + - 'set BONJOUR_SDK_HOME=C:\Program Files\Bonjour SDK' + - npm install -g homebridge + - npm install -g homebridge-http-webhooks + - npm install -g homebridge-sun-position + - for /f "delims=" %%A in ('npm root -g') do set "NODE_GLOBAL_DIR=%%A" + - npm install + - npm install winston@2.3.1 + - 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - npm test +build: 'off' diff --git a/docs/de/img/picture.png b/docs/de/img/picture.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 fs = require('fs'); + 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('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']); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..66df2b8 --- /dev/null +++ b/io-package.json @@ -0,0 +1,49 @@ +{ + "common": { + "name": "ham", + "version": "0.4.4", + "title": "Homebridge accessories manager", + "titleLang": { + "en": "Homebridge accessories manager", + "de": "智能家具网关" + }, + "desc": { + "en": "yunkong2 Homebridge Adapter" + }, + "authors": [ + "Kirov Ilya ", + "Apollon77 { + logger.debug('Char change event: ' + data.oldValue + ' --> ' + data.newValue); + handleCharValue(accessory, service, char, data.newValue); + }); + + updateState(dev_idname, sr_idname, ch_idname, ch_name, ch_val, common, ch_id, () => { + char.getValue((err, value) => { + if (err) { + logger.warn('Error while getting current value: ' + err); + return; + } + handleCharValue(accessory, service, char, value); + }) + }); + } + } + + override(MyBridge, function addBridgedAccessory(accessory, deferUpdate) { + // Вызов метода родительского класса + // Calling the method of the parent class + accessory = addBridgedAccessory.inherited.call(this, accessory, deferUpdate); + logger.debug('yunkong2.ham Bridge addBridgedAccessory ' + customStringify(accessory)); //OK + // Новое устройство + // New device + const dev_id = accessory.UUID; + const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName); + const dev_name = accessory.displayName; + const dev_cat = accessory.category; + + updateDev(dev_idname, dev_name, dev_cat, dev_id); + + for (const index in accessory.services) { + if (!accessory.services.hasOwnProperty(index)) continue; + + const service = accessory.services[index]; + const sr_id = service.UUID; + const sr_idname = mapper.mapServiceType(sr_id, service.displayName); + const sr_name = service.displayName; + + if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') { + continue; + } + + logger.silly('Add service class=' + customStringify(service)); + updateChannel(dev_idname, sr_idname, sr_name, sr_id); + + iterateCharArray(service.characteristics, accessory, service, dev_idname, sr_id, sr_idname); + if (service.optionalCharacteristics) { + iterateCharArray(service.optionalCharacteristics, accessory, service, dev_idname, sr_id, sr_idname); + } + + } + return accessory; + }); + + Server.prototype._createBridge = function() { + logger.debug('yunkong2.ham Bridge create'); //OK + // pull out our custom Bridge settings from config.json, if any + const bridgeConfig = this._config.bridge || {}; + + // Create our Bridge which will host all loaded Accessories + return new MyBridge(bridgeConfig.name || 'Homebridge', hap.uuid.generate('HomeBridge')); + }; + + // Updated to compare value differetly. Needed till officially updated + Characteristic.prototype.setValue = function(newValue, callback, context, connectionID) { + + if ( newValue instanceof Error ) { + this.status = newValue + } else { + this.status = null; + } + + newValue = this.validateValue(newValue); //validateValue returns a value that has be cooerced into a valid value. + + var oldValue = this.value; + + if (this.listeners('set').length > 0) { + + // allow a listener to handle the setting of this value, and wait for completion + this.emit('set', newValue, once(function(err) { + this.status = err; + if (err) { + // pass the error along to our callback + if (callback) callback(err); + } + else { + if (newValue === undefined || newValue === null) + newValue = this.getDefaultValue(); + // setting the value was a success; so we can cache it now + this.value = newValue; + if (callback) callback(); + + if (this.eventOnlyCharacteristic === true || oldValue !== newValue) + this.emit('change', { oldValue:oldValue, newValue:newValue, context:context }); + } + + }.bind(this)), context, connectionID); + + } + else { + if (newValue === undefined || newValue === null) + newValue = this.getDefaultValue(); + // no one is listening to the 'set' event, so just assign the value blindly + this.value = newValue; + if (callback) callback(); + + if (this.eventOnlyCharacteristic === true || oldValue !== newValue) + this.emit('change', { oldValue:oldValue, newValue:newValue, context:context }); + } + + return this; // for chaining + } + + function MyBridge(displayName, serialNumber) { + logger.debug('yunkong2.ham Bridge constructor'); + MyBridge.super_.call(this, displayName, serialNumber); + } +} + +function registerExistingAccessory(UUID, name) { + mapper.mapAccessoryUUID(UUID, name); +} + +function start() { + const insecureAccess = false; + logger.info('Using Homebridge Config Path: ' + User.persistPath()); + // Initialize HAP-NodeJS with a custom persist directory + hap.init(User.persistPath()); + + server = new Server(insecureAccess); + + server.run(); +} + +function end() { + if (server) { + server._teardown(); + // Save cached accessories to persist storage. + server._updateCachedAccessories(); + } +} + +function setValueForCharId(id, value) { + if (charMap[id]) { + logger.debug('set value of char for ' + id); + charMap[id].setValue(value); + } +} + +function handleCharValue(accessory, serv, char, newValue){ + logger.debug('handleCharValue = ' + newValue); + logger.silly('characteristic = ' + customStringify(char)); + logger.silly('accessory =' + customStringify(accessory)); + + const sr_id = serv.UUID; + const sr_idname = mapper.mapServiceType(sr_id, serv.displayName); + const ch_id = char.UUID; + const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, char.displayName); + const dev_id = accessory.UUID; + const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName); + const value = newValue; + + setState(dev_idname, sr_idname, ch_idname, value); +} + +// Средство для переопределения функций +// Tools for overriding functions +function override(child, fn) { + child.prototype[fn.name] = fn; + fn.inherited = child.super_.prototype[fn.name]; +} + +exports.init = init; +exports.end = end; +exports.setValueForCharId = setValueForCharId; +exports.start = start; +exports.registerExistingAccessory = registerExistingAccessory; diff --git a/lib/mapper.js b/lib/mapper.js new file mode 100644 index 0000000..2a33da8 --- /dev/null +++ b/lib/mapper.js @@ -0,0 +1,276 @@ +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +/* jslint esversion: 6 */ +'use strict'; + +module.exports = function (config) { + const module = {}; + + let accTypes; + if (config.useGlobalHomebridge) { + accTypes = require(config.homebridgeBasePath + 'node_modules/hap-nodejs/accessories/types'); + } + else { + accTypes = require('homebridge-plugin-wrapper').HapTypes; + } + + const forbiddenCharacters = /[\]\[*,;'"`<>\\\s?]/g; + + const accessoryNameMap = {}; + module.mapAccessoryUUID = function (accessoryUUID, displayName) { + if (accessoryNameMap[accessoryUUID]) { + return accessoryNameMap[accessoryUUID]; + } + if (displayName) { + displayName = displayName.replace(forbiddenCharacters, '-'); + } + else { + displayName = accessoryUUID; + } + + let found = false; + for (const uuid in accessoryNameMap) { + if (accessoryNameMap.hasOwnProperty(uuid) && accessoryNameMap[uuid] === displayName) { + found = true; + break; + } + } + if (found) { + displayName = displayName + '-' + accessoryUUID; + } + + accessoryNameMap[accessoryUUID] = displayName; + return displayName; + }; + + + const serviceTypeMap = {}; + serviceTypeMap[accTypes.LIGHTBULB_STYPE] = 'Lightbulb'; + serviceTypeMap[accTypes.SWITCH_STYPE] = 'Switch'; + serviceTypeMap[accTypes.THERMOSTAT_STYPE] = 'Thermostat'; + serviceTypeMap[accTypes.GARAGE_DOOR_OPENER_STYPE] = 'Garage-Door-Opener'; + serviceTypeMap[accTypes.ACCESSORY_INFORMATION_STYPE] = 'Accessory-Information'; + serviceTypeMap[accTypes.FAN_STYPE] = 'Fan'; + serviceTypeMap[accTypes.OUTLET_STYPE] = 'Outlet'; + serviceTypeMap[accTypes.LOCK_MECHANISM_STYPE] = 'Lock-Mechanism'; + serviceTypeMap[accTypes.LOCK_MANAGEMENT_STYPE] = 'Lock-Management'; + serviceTypeMap[accTypes.ALARM_STYPE] = 'Alarm'; + serviceTypeMap[accTypes.WINDOW_COVERING_STYPE] = 'Window-Covering'; + serviceTypeMap[accTypes.OCCUPANCY_SENSOR_STYPE] = 'Occupancy-Sensor'; + serviceTypeMap[accTypes.CONTACT_SENSOR_STYPE] = 'Contact-Sensor'; + serviceTypeMap[accTypes.MOTION_SENSOR_STYPE] = 'Motion-Sensor'; + serviceTypeMap[accTypes.HUMIDITY_SENSOR_STYPE] = 'Humidity-Sensor'; + serviceTypeMap[accTypes.TEMPERATURE_SENSOR_STYPE] = 'Temperature-Sensor'; + + module.mapServiceType = function (serviceUUID, displayName) { + if (displayName) { + displayName = displayName.replace(forbiddenCharacters, '-'); + return displayName; + } + if (serviceTypeMap[serviceUUID]) { + return serviceTypeMap[serviceUUID]; + } + + const pos = serviceUUID.indexOf('-0000-1000-8000-0026BB765291'); + if (pos !== -1) { + return serviceUUID.substring(0, pos); + } + + return serviceUUID; + }; + + + const characteristicTypeMap = {}; + characteristicTypeMap[accTypes.ALARM_CURRENT_STATE_CTYPE] = {name: 'Current-Alarm-State', roleDetail: '.alarm'}; + characteristicTypeMap[accTypes.ALARM_TARGET_STATE_CTYPE] = {name: 'Target-Alarm-State', roleDetail: ''}; + characteristicTypeMap[accTypes.ADMIN_ONLY_ACCESS_CTYPE] = {name: 'Admin-Only-Access', roleDetail: ''}; + characteristicTypeMap[accTypes.AUDIO_FEEDBACK_CTYPE] = {name: 'Audio-Feedback', roleDetail: ''}; + characteristicTypeMap[accTypes.BRIGHTNESS_CTYPE] = {name: 'Brightness', roleDetail: '.brightness'}; + characteristicTypeMap[accTypes.BATTERY_LEVEL_CTYPE] = {name: 'Battery-Level', roleDetail: '.battery'}; + characteristicTypeMap[accTypes.COOLING_THRESHOLD_CTYPE] = {name: 'Cooling-Threshold', roleDetail: '.temperature'}; + characteristicTypeMap[accTypes.CONTACT_SENSOR_STATE_CTYPE] = {name: 'Contact-Sensor-State', roleDetail: '.window'}; //?? + characteristicTypeMap[accTypes.CURRENT_DOOR_STATE_CTYPE] = {name: 'Current-Door-State', roleDetail: '.door'}; + characteristicTypeMap[accTypes.CURRENT_LOCK_MECHANISM_STATE_CTYPE] = {name: 'Current-Lock-Mechanism-State', roleDetail: '.lock'}; + characteristicTypeMap[accTypes.CURRENT_RELATIVE_HUMIDITY_CTYPE] = {name: 'Current-Relative-Humidity', roleDetail: '.humidity'}; + characteristicTypeMap[accTypes.CURRENT_TEMPERATURE_CTYPE] = {name: 'Current-Temperature', roleDetail: '.temperature'}; + characteristicTypeMap[accTypes.HEATING_THRESHOLD_CTYPE] = {name: 'Heating-Threshold', roleDetail: '.temperature'}; + characteristicTypeMap[accTypes.HUE_CTYPE] = {name: 'Hue', roleDetail: ''}; + characteristicTypeMap[accTypes.IDENTIFY_CTYPE] = {name: 'Identify', roleDetail: ''}; + characteristicTypeMap[accTypes.LOCK_MANAGEMENT_AUTO_SECURE_TIMEOUT_CTYPE] = {name: 'Auto-Secure-Timeout', roleDetail: ''}; + characteristicTypeMap[accTypes.LOCK_MANAGEMENT_CONTROL_POINT_CTYPE] = {name: 'Control-Point', roleDetail: ''}; + characteristicTypeMap[accTypes.LOCK_MECHANISM_LAST_KNOWN_ACTION_CTYPE] = {name: 'Last-Known-Action', roleDetail: ''}; + characteristicTypeMap[accTypes.LOGS_CTYPE] = {name: 'Logs', roleDetail: ''}; + characteristicTypeMap[accTypes.MANUFACTURER_CTYPE] = {name: 'Manufacturer', roleDetail: ''}; + characteristicTypeMap[accTypes.MODEL_CTYPE] = {name: 'Model', roleDetail: ''}; + characteristicTypeMap[accTypes.MOTION_DETECTED_CTYPE] = {name: 'Motion-Detected', roleDetail: '.motion'}; + characteristicTypeMap[accTypes.NAME_CTYPE] = {name: 'Name', roleDetail: ''}; + characteristicTypeMap[accTypes.OBSTRUCTION_DETECTED_CTYPE] = {name: 'Obstruction-Detected', roleDetail: ''}; + characteristicTypeMap[accTypes.OUTLET_IN_USE_CTYPE] = {name: 'Outlet-In-Use', roleDetail: 'indicator.working'}; + characteristicTypeMap[accTypes.OCCUPANCY_DETECTED_CTYPE] = {name: 'Occupancy-Detected', roleDetail: ''}; + characteristicTypeMap[accTypes.POWER_STATE_CTYPE] = {name: 'Power-State', roleDetail: ''}; + characteristicTypeMap[accTypes.PROGRAMMABLE_SWITCH_SWITCH_EVENT_CTYPE] = {name: 'Switch-Event-Programmable-Switch', roleDetail: ''}; + characteristicTypeMap[accTypes.PROGRAMMABLE_SWITCH_OUTPUT_STATE_CTYPE] = {name: 'Output-State-Programmable-Switch', roleDetail: ''}; + characteristicTypeMap[accTypes.ROTATION_DIRECTION_CTYPE] = {name: 'Rotation-Direction', roleDetail: '.direction'}; + characteristicTypeMap[accTypes.ROTATION_SPEED_CTYPE] = {name: 'Rotation-Speed', roleDetail: ''}; + characteristicTypeMap[accTypes.SATURATION_CTYPE] = {name: 'Saturation', roleDetail: '.color.saturation'}; + characteristicTypeMap[accTypes.SERIAL_NUMBER_CTYPE] = {name: 'Serial-Number', roleDetail: ''}; + characteristicTypeMap[accTypes.STATUS_LOW_BATTERY_CTYPE] = {name: 'Low-Battery', roleDetail: '.lowbat'}; + characteristicTypeMap[accTypes.STATUS_FAULT_CTYPE] = {name: 'Fault', roleDetail: ''}; + characteristicTypeMap[accTypes.TARGET_DOORSTATE_CTYPE] = {name: 'Target-Doorstate', roleDetail: '.door'}; + characteristicTypeMap[accTypes.TARGET_LOCK_MECHANISM_STATE_CTYPE] = {name: 'Target-Lock-Mechanism-State', roleDetail: '.lock'}; + characteristicTypeMap[accTypes.TARGET_RELATIVE_HUMIDITY_CTYPE] = {name: 'Target-Relative-Humidity', roleDetail: '.humidity'}; + characteristicTypeMap[accTypes.TARGET_TEMPERATURE_CTYPE] = {name: 'Target-Temperature', roleDetail: '.temperature'}; + characteristicTypeMap[accTypes.TEMPERATURE_UNITS_CTYPE] = {name: 'Temperature-Units', roleDetail: ''}; + characteristicTypeMap[accTypes.VERSION_CTYPE] = {name: 'Version', roleDetail: ''}; + characteristicTypeMap[accTypes.WINDOW_COVERING_TARGET_POSITION_CTYPE] = {name: 'Target-Position-Window-Covering', roleDetail: '.blind'}; + characteristicTypeMap[accTypes.WINDOW_COVERING_CURRENT_POSITION_CTYPE] = {name: 'Current-Position-Window-Covering', roleDetail: '.blind'}; + characteristicTypeMap[accTypes.WINDOW_COVERING_OPERATION_STATE_CTYPE] = {name: 'Operation-State-Window-Covering', roleDetail: '.working'}; + characteristicTypeMap[accTypes.CURRENTHEATINGCOOLING_CTYPE] = {name: 'Current-Heating-Cooling', roleDetail: ''}; + characteristicTypeMap[accTypes.TARGETHEATINGCOOLING_CTYPE] = {name: 'Target-Heating-Cooling', roleDetail: ''}; + + module.mapCharacteristicType = function (serviceUUID, charUUID, displayName) { + if (displayName) { + displayName = displayName.replace(forbiddenCharacters, '-'); + return displayName; + } + if (characteristicTypeMap[charUUID]) return characteristicTypeMap[charUUID].name; + + const pos = charUUID.indexOf("-0000-1000-8000-0026BB765291"); + if (pos !== -1) return charUUID.substring(0, pos); + + return charUUID; + }; + + + const characteristicFormats = { + 'bool': 'boolean', + 'int': 'number', + 'float': 'number', + 'string': 'string', + 'uint8': 'number', + 'uint16': 'number', + 'uint32': 'number', + 'uint64': 'number', + 'data': 'string', + 'tlv8': 'string', + 'array': 'string', //Not in HAP Spec + 'dict': 'string' //Not in HAP Spec + }; + + // Known HomeKit unit types + const characteristicUnits = { + // HomeKit only defines Celsius, for Fahrenheit, it requires iOS app to do the conversion. + 'celsius': '°C', + 'percentage': '%', + 'arcdegrees': '°', + 'lux': 'lx', + 'seconds': 's' + }; + + module.mapCharacteristicProperties = function (char) { + const common = {}; + if (characteristicFormats[char.props.format]) { + common.type = characteristicFormats[char.props.format]; + } + else common.type = 'string'; + + if (characteristicUnits[char.props.unit]) { + common.unit = characteristicUnits[char.props.unit]; + } + else if (char.props.unit) { + common.unit = char.props.unit; + } + + if (char.props.minValue !== null && char.props.minValue !== undefined) { + common.min = char.props.minValue; + } + if (char.props.maxValue !== null && char.props.maxValue !== undefined) { + common.max = char.props.maxValue; + } + if (common.min === undefined || common.max === undefined) { + switch(char.props.format) { + case 'int': + if (common.min === undefined) common.min = -2147483648; + if (common.max === undefined) common.max = 2147483647; + break; + case 'uint8': + if (common.min === undefined) common.min = 0; + if (common.max === undefined) common.max = 255; + break; + case 'uint16': + if (common.min === undefined) common.min = 0; + if (common.max === undefined) common.max = 65535; + break; + case 'uint32': + if (common.min === undefined) common.min = 0; + if (common.max === undefined) common.max = 4294967295; + break; + case 'uint64': + if (common.min === undefined) common.min = 0; + if (common.max === undefined) common.max = 18446744073709551615; + break; + } + } + + if (char.validValues && char.validValues.length > 0 && common.type !== 'boolean') { + common.states = {}; + for (let i = 0; i < char.validValues; i++) { + common.states[char.validValues[i]] = char.validValues[i]; + } + } + + common.read = (char.props.perms.indexOf('pr') !== -1); + common.write = (char.props.perms.indexOf('pw') !== -1); + if (!common.read && !common.write) { + common.read = true; + } + + let roleDetail = null; + if (characteristicTypeMap[char.UUID]) { + roleDetail = characteristicTypeMap[char.UUID].roleDetail; + } + + // Try to set roles + let role = ''; + if (common.type === 'boolean') { + if (common.read && !common.write) { // Boolean, read-only --> Sensor OR Indicator! + role = 'sensor'; + } + else if (common.write && !common.read) { // Boolean, write-only --> Button + role = 'button'; + } + else if (common.read && common.write) { // Boolean, read-write --> Switch + role = 'switch'; + } + } + else if (common.type === 'number') { + if (common.read && !common.write) { // Number, read-only --> Value + role = 'value'; + } + else if (common.write && !common.read) { // Boolean, write-only --> ?? Level? + role = 'level'; + } + else if (common.read && common.write) { // Number, read-write --> Level + role = 'level'; + } + } + else if (common.type === 'string') { + role = 'text'; + } + if (roleDetail && roleDetail !== '' && roleDetail.indexOf('.') === 0 && role !== '') { + role += roleDetail; + } + else if (roleDetail && roleDetail !== '' && roleDetail.indexOf('.') === -1) { + role = roleDetail; + } + if (role !== '') common.role = role; + + if (!common.role) common.role = 'state'; + + return common; + }; + + + return module; +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..b07a7c6 --- /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 = [ + 'yunkong2.js-controller', + 'yunkong2.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) { + 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/lib/wrapper-handler.js b/lib/wrapper-handler.js new file mode 100644 index 0000000..0138a42 --- /dev/null +++ b/lib/wrapper-handler.js @@ -0,0 +1,148 @@ +/* jshint strict: false */ +/* jslint node: true */ +/* jslint esversion: 6 */ +'use strict'; + +const HomebridgeWrapper = require('homebridge-plugin-wrapper').Wrapper; + +let homebridgeWrapper; +const charMap = {}; + +let logger; +let updateState; +let updateDev; +let updateChannel; +let setState; +let mapper; +let ignoreInfoAccessoryServices; + +function customStringify(v, func, intent) { + const cache = new Map(); + return JSON.stringify(v, function (key, value) { + if (typeof value === 'object' && value !== null) { + if (cache.get(value)) { + // Circular reference found, discard key + return; + } + // Store value in our map + cache.set(value, true); + } + return value; + }, intent); +} + +function init(config) { + logger = config.logger; + updateState = config.updateState; + updateDev = config.updateDev; + updateChannel = config.updateChannel; + setState = config.setState; + ignoreInfoAccessoryServices = config.ignoreInfoAccessoryServices; + + mapper = require('./mapper')(config); + homebridgeWrapper = new HomebridgeWrapper(config); + + homebridgeWrapper.on('characteristic-value-change', data => { + logger.debug('Char change event: ' + data.oldValue + ' --> ' + data.newValue); + handleCharValue(data.accessory, data.service, data.characteristic, data.newValue); + }); + + homebridgeWrapper.on('addAccessory', accessory => { + function iterateCharArray(chars, dev_idname, sr_id, sr_idname) { + for (const chindex in chars) { + if (!chars.hasOwnProperty(chindex)) continue; + const char = chars[chindex]; + const ch_id = char.UUID; + const ch_name = char.displayName; + const ch_val = char.value; + const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, ch_name); + const id = dev_idname + '.' + sr_idname + '.' + ch_idname; + + const common = mapper.mapCharacteristicProperties(char); + logger.debug('Mapped Common for ' + id + ': ' + JSON.stringify(common)); + if (common.write) { + charMap[id] = char; // TODO only if write allowed! + logger.silly('Add object to charmap with id ' + id + '/' + customStringify(char)); + } + + updateState(dev_idname, sr_idname, ch_idname, ch_name, ch_val, common, ch_id); + } + } + + logger.debug('yunkong2.ham Bridge addBridgedAccessory ' + customStringify(accessory)); //OK + // Новое устройство + // New device + const dev_id = accessory.UUID; + const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName); + const dev_name = accessory.displayName; + const dev_cat = accessory.category; + + updateDev(dev_idname, dev_name, dev_cat, dev_id); + for (const index in accessory.services) { + if (!accessory.services.hasOwnProperty(index)) continue; + + const service = accessory.services[index]; + const sr_id = service.UUID; + const sr_idname = mapper.mapServiceType(sr_id, service.displayName); + const sr_name = service.displayName; + + if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') { + continue; + } + + logger.silly('Add service class=' + customStringify(service)); + updateChannel(dev_idname, sr_idname, sr_name, sr_id); + + iterateCharArray(service.characteristics, dev_idname, sr_id, sr_idname); + if (service.optionalCharacteristics) { + iterateCharArray(service.optionalCharacteristics, dev_idname, sr_id, sr_idname); + } + } + + }); + +} + +function registerExistingAccessory(UUID, name) { + mapper.mapAccessoryUUID(UUID, name); +} + +function start() { + homebridgeWrapper.init(); +} + +function end() { + homebridgeWrapper.finish(); +} + +function setValueForCharId(id, value) { + if (charMap[id]) { + logger.debug('set value ' + value + ' of char for ' + id); + charMap[id].setValue(value); + } +} + +function handleCharValue(accessory, serv, char, newValue){ + const sr_id = serv.UUID; + const sr_idname = mapper.mapServiceType(sr_id, serv.displayName); + const ch_id = char.UUID; + const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, char.displayName); + const dev_id = accessory.UUID; + const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName); + const value = newValue; + + logger.debug('handleCharValue = ' + newValue); + logger.silly('characteristic = ' + customStringify(char)); + logger.silly('accessory =' + customStringify(accessory)); + + if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') { + return; + } + setState(dev_idname, sr_idname, ch_idname, value); +} + +exports.init = init; +exports.end = end; +exports.setValueForCharId = setValueForCharId; +exports.start = start; +exports.registerExistingAccessory = registerExistingAccessory; diff --git a/main.js b/main.js new file mode 100644 index 0000000..19fb708 --- /dev/null +++ b/main.js @@ -0,0 +1,300 @@ +/* jshint -W097 */ +/* jshint -W030 */ +/* jshint strict: false */ +/* jslint node: true */ +/* jslint esversion: 6 */ +'use strict'; +const nodeFS = require('fs'); +const child_process = require('child_process'); +// you have to require the utils module and call adapter function +const utils = require(__dirname + '/lib/utils'); // Get common adapter utils +const path = require('path'); + +// it is not an object. +function createHam(options) { + const dataDir = path.normalize(path.join(utils.controllerDir, require(path.join(utils.controllerDir, 'lib', 'tools.js')).getDefaultDataDir())); + + // 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(options); + + let homebridgeHandler; + const attempts = {}; + + // is called when adapter shuts down - callback has to be called under any circumstances! + adapter.on('unload', callback => { + try { + adapter.log.info('cleaned everything up...'); + homebridgeHandler.end(); + callback(); + } catch (e) { + callback(); + } + }); + + process.on('SIGINT', () => homebridgeHandler.end()); + + process.on('SIGTERM', () => homebridgeHandler.end()); + + process.on('uncaughtException', err => { + if (adapter && adapter.log) { + adapter.log.warn('Exception: ' + err); + } + homebridgeHandler.end(); + }); + + // is called if a subscribed state changes + adapter.on('stateChange', (id, state) => { + // Warning, state can be null if it was deleted + adapter.log.info('stateChange ' + id + ' ' + JSON.stringify(state)); + + id = id.substr(adapter.namespace.length+1); + adapter.log.debug('lookup id: ' + id); + // 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!'); + homebridgeHandler.setValueForCharId(id, state.val); + } + }); + + function updateDev(dev_id, dev_name, dev_type, dev_uuid) { + adapter.log.info('updateDev ' + dev_id + ': name = ' + dev_name + ' /type= ' + dev_type); + // create dev + adapter.getObject(dev_id, (err, obj) => { + if (!err && obj) { + adapter.extendObject(dev_id, { + type: 'device', + common: {name: dev_name}, + native: { + UUID: dev_uuid, + displayName: dev_name, + category: dev_type + } + }); + } + else { + adapter.setObject(dev_id, { + type: 'device', + common: {name: dev_name}, + native: { + UUID: dev_uuid, + displayName: dev_name, + category: dev_type + } + }, {}); + } + }); + } + + function updateChannel(dev_id, ch_id, name, ch_uuid) { + const id = dev_id + '.' + ch_id; + // create channel for dev + adapter.log.info('updateChannel ' + id + ': name = ' + name); + adapter.getObject(id, (err, obj) => { + if (!err && obj) { + adapter.extendObject(id, { + type: 'channel', + common: {name: name}, + native: { + UUID: ch_uuid, + displayName: name + } + }); + } + else { + adapter.setObject(id, { + type: 'channel', + common: {name: name}, + native: { + UUID: ch_uuid, + displayName: name + } + }, {}); + } + }); + } + + function updateState(dev_id, ch_id, st_id, name, value, common, st_uuid, callback) { + const id = dev_id + '.' + ch_id + '.'+ st_id; + if (!common) common = {}; + if (common.name === undefined) common.name = name; + if (common.role === undefined) common.role = 'state'; + if (common.read === undefined && common.write === undefined) common.read = true; + if (common.type === undefined) common.type = 'string'; + if (common.unit === undefined) common.unit = ''; + + adapter.log.info('updateState ' + id + ': value = ' + value + ' /common= ' + JSON.stringify(common)); + + adapter.getObject(id, (err, obj) => { + if (!err && obj) { + adapter.extendObject(id, { + type: 'state', + common: common, + native: { + UUID: st_uuid, + displayName: name + } + }, callback); + } + else { + adapter.setObject(id, { + type: 'state', + common: common, + native: { + UUID: st_uuid, + displayName: name + } + }, callback); + } + }); + } + + function setState(dev_id, ch_id, st_id, value) { + const id = dev_id + '.' + ch_id + '.' + st_id; + adapter.setState(id, value, true); + } + + // is called when databases are connected and adapter received configuration. + // start here! + adapter.on('ready', main); + + function loadExistingAccessories(callback) { + adapter.getDevices((err, res) => { + if (err) { + adapter.log.error('Can not get all existing devices: ' + err); + return; + } + for (let i = 0; i < res.length; i++) { + if (res[i].native && res[i].native.UUID) { + adapter.log.debug('Remember existing Accessory ' + res[i].native.displayName + ' with UUID ' + res[i].native.UUID); + homebridgeHandler.registerExistingAccessory(res[i].native.UUID, res[i].native.displayName); + } + } + + if (callback) callback(); + }); + } + + //Catch Homebridge Console Logging + if (process.argv.indexOf('--logs') === -1 && process.argv.indexOf('-l') === -1) { + console.log = function (logs) { + if (adapter && adapter.log && adapter.log.debug) { + adapter.log.debug(logs); + } + process.stdout.write(logs + '\n'); + }; + } + + function main() { + const usedLogger = { + info: adapter.log.debug, + warn: adapter.log.warn, + debug: adapter.log.silly, + silly: adapter.log.silly + }; + if (adapter.config.useGlobalHomebridge) { + homebridgeHandler = require('./lib/global-handler'); + homebridgeHandler.init({ + logger: usedLogger, + homebridgeBasePath: adapter.config.globalHomebridgeBasePath, + homebridgeConfigPath: adapter.config.globalHomebridgeConfigPath, + updateDev: updateDev, + updateChannel: updateChannel, + updateState: updateState, + setState: setState, + ignoreInfoAccessoryServices: adapter.config.ignoreInfoAccessoryServices + }); + } + else { + homebridgeHandler = require('./lib/wrapper-handler'); + homebridgeHandler.init({ + logger: usedLogger, + homebridgeConfigPath: dataDir + adapter.namespace.replace('.', '_'), + updateDev: updateDev, + updateChannel: updateChannel, + updateState: updateState, + setState: setState, + wrapperConfig: adapter.config.wrapperConfig, + ignoreInfoAccessoryServices: adapter.config.ignoreInfoAccessoryServices, + characteristicPollingInterval: adapter.config.characteristicPollingInterval * 1000 + }); + } + + // in this template all states changes inside the adapters namespace are subscribed + adapter.subscribeStates('*'); + + installLibraries(() => { + loadExistingAccessories(() => { + homebridgeHandler.start(); + + options.exitAfter && setTimeout(() => adapter && adapter.stop(), 10000); + }); + }); + } + + function installNpm(npmLib, callback) { + const path = __dirname; + if (typeof npmLib === 'function') { + callback = npmLib; + npmLib = undefined; + } + + const cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '"'; + adapter.log.info(cmd + ' (System call)'); + // Install node modules as system call + + // System call used for update of js-controller itself, + // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. + const child = child_process.exec(cmd); + + child.stdout.on('data', buf => adapter.log.info(buf.toString('utf8'))); + child.stderr.on('data', buf => adapter.log.error(buf.toString('utf8'))); + + child.on('exit', (code /* , signal */) => { + if (code) { + adapter.log.error('Cannot install ' + npmLib + ': ' + code); + } + // command succeeded + if (typeof callback === 'function') callback(npmLib); + }); + } + + function installLibraries(callback) { + let allInstalled = true; + if (adapter.config && adapter.config.libraries && !adapter.config.useGlobalHomebridge) { + const libraries = adapter.config.libraries.split(/[,;\s]+/); + + for (let lib = 0; lib < libraries.length; lib++) { + if (libraries[lib] && libraries[lib].trim()) { + libraries[lib] = libraries[lib].trim(); + if (!nodeFS.existsSync(__dirname + '/node_modules/' + libraries[lib] + '/package.json')) { + + if (!attempts[libraries[lib]]) { + attempts[libraries[lib]] = 1; + } else { + attempts[libraries[lib]]++; + } + if (attempts[libraries[lib]] > 3) { + adapter.log.error('Cannot install npm packet: ' + libraries[lib]); + continue; + } + + installNpm(libraries[lib], () => installLibraries(callback)); + allInstalled = false; + break; + } + } + } + } + if (allInstalled) callback(); + } + + return adapter; +} + +if (!module || !module.parent) { + createHam('ham'); +} else { + module.exports = createHam; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bbde85f --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "version": "0.4.4", + "name": "yunkong2.ham", + "author": { + "name": "Apollon77", + "email": "ingo@fischer-ka.de" + }, + "bugs": { + "url": "https://github.com/yunkong2/yunkong2.ham/issues" + }, + "contributors": [ + { + "name": "Apollon77", + "email": "ingo@fischer-ka.de" + }, + { + "name": "Kirov Ilya", + "email": "kirovilya@gmail.com" + } + ], + "dependencies": { + "homebridge-plugin-wrapper": "^0.5.2" + }, + "description": "Homebridge accessories manager", + "devDependencies": { + "chai": "^4.1.2", + "gulp": "^3.9.1", + "mocha": "^5.2.0" + }, + "optionalDependencies": {}, + "homepage": "https://github.com/yunkong2/yunkong2.ham", + "keywords": [ + "yunkong2", + "Homebridge", + "Smart Home", + "home automation" + ], + "license": "MIT", + "main": "main.js", + "readmeFilename": "README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/yunkong2/yunkong2.ham.git" + }, + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + } +} diff --git a/test/homebridge/config.json b/test/homebridge/config.json new file mode 100644 index 0000000..bbeea73 --- /dev/null +++ b/test/homebridge/config.json @@ -0,0 +1,133 @@ +{ + "bridge": { + "name": "Test-Bridge", + "username": "EE:22:3D:E3:CE:30", + "port": 61826, + "pin": "031-45-156" + }, + + "description": "This is an example configuration file with one fake accessory and one fake platform. You can use this as a template for creating your own configuration file containing devices you actually own.", + + "accessories": [ + { + "accessory" : "SunPosition", + "name" : "Sun", + "location" : { + "lat" : 49.035924, + "long" : 8.345736 + } + } + ], + + "platforms": [ + { + "platform": "HttpWebHooks", + "webhook_port": "61828", + "cache_directory": "./.node-persist/storage", + "sensors": [ + { + "id": "sensor1", + "name": "Sensor name 1", + "type": "contact" + }, + { + "id": "sensor2", + "name": "Sensor name 2", + "type": "motion" + }, + { + "id": "sensor3", + "name": "Sensor name 3", + "type": "occupancy" + }, + { + "id": "sensor4", + "name": "Sensor name 4", + "type": "smoke" + }, + { + "id": "sensor5", + "name": "Sensor name 5", + "type": "temperature" + }, + { + "id": "sensor6", + "name": "Sensor name 6", + "type": "humidity" + }, + { + "id": "sensor7", + "name": "Sensor name 7", + "type": "airquality" + }, + { + "id": "sensor8", + "name": "Sensor name 8", + "type": "airquality" + } + ], + "switches": [ + { + "id": "switch1", + "name": "Switch name 1", + "on_url": "http://localhost:9080/switch1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch1?off", + "off_method": "GET" + }, + { + "id": "switch2", + "name": "Switch name 2", + "on_url": "http://localhost:9080/switch2?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch2?off", + "off_method": "GET" + }, + { + "id": "switch3", + "name": "Switch name 3", + "on_url": "http://localhost:9080/switch3?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch3?off", + "off_method": "GET" + } + ], + "pushbuttons": [ + { + "id": "pushbutton1", + "name": "Push button name 1", + "push_url": "http://localhost:9080/pushbutton1?push", + "push_method": "GET" + } + ], + "lights": [ + { + "id": "light1", + "name": "Light name 1", + "on_url": "http://localhost:9080/light1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/light1?off", + "off_method": "GET" + } + ], + "thermostats": [ + { + "id": "thermostat1", + "name": "Thermostat name 1", + "set_target_temperature_url": "http://localhost:9080/thermostat1?targettemperature=%f", + "set_target_heating_cooling_state_url": "http://localhost:9080/thermostat1??targetstate=%b" + } + ], + "outlets": [ + { + "id": "outlet1", + "name": "Outlet name 1", + "on_url": "http://localhost:9080/outlet1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/outlet1?off", + "off_method": "GET" + } + ] + } + ] +} diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..09cc5f3 --- /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/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(); + } + // 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/testAdapterWrapperNoInfo.js b/test/testAdapterWrapperNoInfo.js new file mode 100644 index 0000000..818e855 --- /dev/null +++ b/test/testAdapterWrapperNoInfo.js @@ -0,0 +1,365 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; +const expect = require('chai').expect; +const setup = require(__dirname + '/lib/setup'); +const request = require('request'); +const http = require('http'); +const fs = require('fs'); + +let objects = null; +let states = null; +let onStateChanged = null; +let onObjectChanged = null; +let sendToID = 1; + +const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +let httpServer; +let lastHTTPRequest = null; + +function setupHTTPServer(port, callback) { + httpServer = http.createServer((req, res) => { + lastHTTPRequest = req.url; + console.log('HTTP Received: ' + lastHTTPRequest); + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('OK'); + }).listen(port); + setTimeout(() => callback(), 5000); +} + +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', (err, state) => { + if (err) console.error(err); + if (state && state.val) { + if (cb) cb(); + } else { + setTimeout(() => 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, (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 = (id, state) => (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: Date.now() + } + }); +} + +describe('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo', () => { + before('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(() => { + const config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.useGlobalHomebridge = false; + config.native.globalHomebridgeConfigPath = __dirname + "/homebridge/"; + config.native.libraries = "homebridge-http-webhooks homebridge-sun-position"; + config.native.ignoreInfoAccessoryServices = true; + config.native.characteristicPollingInterval = 30; + config.native.wrapperConfig = { + "accessories": [ + { + "accessory" : "SunPosition", + "name" : "Sun", + "location" : { + "lat" : 49.035924, + "long" : 8.345736 + } + } + ], + + "platforms": [ + { + "platform": "HttpWebHooks", + "webhook_port": "61828", + "cache_directory": "./.node-persist/storage", + "sensors": [ + { + "id": "sensor1", + "name": "Sensor name 1", + "type": "contact" + }, + { + "id": "sensor2", + "name": "Sensor name 2", + "type": "motion" + }, + { + "id": "sensor3", + "name": "Sensor name 3", + "type": "occupancy" + }, + { + "id": "sensor4", + "name": "Sensor name 4", + "type": "smoke" + }, + { + "id": "sensor5", + "name": "Sensor name 5", + "type": "temperature" + }, + { + "id": "sensor6", + "name": "Sensor name 6", + "type": "humidity" + }, + { + "id": "sensor7", + "name": "Sensor name 7", + "type": "airquality" + }, + { + "id": "sensor8", + "name": "Sensor name 8", + "type": "airquality" + } + ], + "switches": [ + { + "id": "switch1", + "name": "Switch name 1", + "on_url": "http://localhost:9080/switch1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch1?off", + "off_method": "GET" + }, + { + "id": "switch2", + "name": "Switch name 2", + "on_url": "http://localhost:9080/switch2?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch2?off", + "off_method": "GET" + }, + { + "id": "switch3", + "name": "Switch name 3", + "on_url": "http://localhost:9080/switch3?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch3?off", + "off_method": "GET" + }, + { + "id": "switch4", + "name": "Switch name*3", + "on_url": "http://localhost:9080/switch3-2?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch3-2?off", + "off_method": "GET" + } + ], + "pushbuttons": [ + { + "id": "pushbutton1", + "name": "Push button name 1", + "push_url": "http://localhost:9080/pushbutton1?push", + "push_method": "GET" + } + ], + "lights": [ + { + "id": "light1", + "name": "Light name 1", + "on_url": "http://localhost:9080/light1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/light1?off", + "off_method": "GET" + } + ], + "thermostats": [ + { + "id": "thermostat1", + "name": "Thermostat name 1", + "set_target_temperature_url": "http://localhost:9080/thermostat1?targettemperature=%f", + "set_target_heating_cooling_state_url": "http://localhost:9080/thermostat1??targetstate=%b" + } + ], + "outlets": [ + { + "id": "outlet1", + "name": "Outlet name 1", + "on_url": "http://localhost:9080/outlet1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/outlet1?off", + "off_method": "GET" + } + ] + } + ] + }; + + setup.setAdapterConfig(config.common, config.native); + + setupHTTPServer(9080, () => { + setup.startController( + true, + (id, obj) => {}, + (id, state) => { + if (onStateChanged) onStateChanged(id, state); + }, + (_objects, _states) => { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + }); + + it('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Check if adapter started', done => { + checkConnectionOfAdapter(res => { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + () => { + states.subscribeMessage('system.adapter.test.0'); + done(); + }); + }); + }).timeout(60000); + + it('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Wait for npm installs', done => { + setTimeout(() => done(), 30000); + }).timeout(60000); + + it('Test ' + adapterShortName + ' Wrapper: Verify Init', done => { + expect(fs.existsSync(__dirname + '/homebridge/config.json')).to.be.true; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + + states.getState(adapterShortName + '.0.Sun.Accessory-Information.Model', (err, state) => { + expect(err).to.be.null; + expect(state).to.be.undefined; + + states.getState(adapterShortName + '.0.Sun.Sun.Altitude', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.exist; + done(); + }); + }); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test Change from inside', done => { + request('http://localhost:61828/?accessoryId=switch1&state=true', (error, response, body) => { + expect(error).to.be.null; + expect(response && response.statusCode).to.be.equal(200); + + setTimeout(function() { + expect(lastHTTPRequest).to.be.null; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.true; + done(); + }); + }, 2000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic', done => { + states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: false, ack:false}, function (err) { + expect(err).to.not.exist; + + setTimeout(function() { + expect(lastHTTPRequest).to.be.equal('/switch1?off'); + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + done(); + }); + }, 2000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic 2', done => { + states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: true, ack:false}, function (err) { + expect(err).to.not.exist; + + setTimeout(function() { + expect(lastHTTPRequest).to.be.equal('/switch1?on'); + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.true; + done(); + }); + }, 2000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test Change from inside 2', done => { + lastHTTPRequest = null; + request('http://localhost:61828/?accessoryId=switch1&state=false', (error, response, body) => { + expect(error).to.be.null; + expect(response && response.statusCode).to.be.equal(200); + + setTimeout(function() { + expect(lastHTTPRequest).to.be.null; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + setTimeout(() => done(), 40000); + }); + }, 2000); + }); + }).timeout(50000); + + after('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + httpServer.close(); + done(); + }); + }); +}); diff --git a/test/testAdapterWrapperWithInfo.js b/test/testAdapterWrapperWithInfo.js new file mode 100644 index 0000000..acee23d --- /dev/null +++ b/test/testAdapterWrapperWithInfo.js @@ -0,0 +1,364 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; +const expect = require('chai').expect; +const setup = require(__dirname + '/lib/setup'); +const request = require('request'); +const http = require('http'); +const fs = require('fs'); + +let objects = null; +let states = null; +let onStateChanged = null; +let onObjectChanged = null; +let sendToID = 1; + +const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +let httpServer; +let lastHTTPRequest = null; + +function setupHTTPServer(port, callback) { + httpServer = http.createServer((req, res) => { + lastHTTPRequest = req.url; + console.log('HTTP Received: ' + lastHTTPRequest); + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('OK'); + }).listen(port); + setTimeout(() => callback(), 5000); +} + +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', (err, state) => { + if (err) console.error(err); + if (state && state.val) { + if (cb) cb(); + } else { + setTimeout(() => 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, (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 = (id, state) => (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: Date.now() + } + }); +} + +describe('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo', () => { + before('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(() => { + const config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.useGlobalHomebridge = false; + config.native.globalHomebridgeConfigPath = __dirname + "/homebridge/"; + config.native.libraries = "homebridge-http-webhooks homebridge-sun-position"; + config.native.ignoreInfoAccessoryServices = false; + config.native.wrapperConfig = { + "accessories": [ + { + "accessory" : "SunPosition", + "name" : "Sun", + "location" : { + "lat" : 49.035924, + "long" : 8.345736 + } + } + ], + + "platforms": [ + { + "platform": "HttpWebHooks", + "webhook_port": "61828", + "cache_directory": "./.node-persist/storage", + "sensors": [ + { + "id": "sensor1", + "name": "Sensor name 1", + "type": "contact" + }, + { + "id": "sensor2", + "name": "Sensor name 2", + "type": "motion" + }, + { + "id": "sensor3", + "name": "Sensor name 3", + "type": "occupancy" + }, + { + "id": "sensor4", + "name": "Sensor name 4", + "type": "smoke" + }, + { + "id": "sensor5", + "name": "Sensor name 5", + "type": "temperature" + }, + { + "id": "sensor6", + "name": "Sensor name 6", + "type": "humidity" + }, + { + "id": "sensor7", + "name": "Sensor name 7", + "type": "airquality" + }, + { + "id": "sensor8", + "name": "Sensor name 8", + "type": "airquality" + } + ], + "switches": [ + { + "id": "switch1", + "name": "Switch name 1", + "on_url": "http://localhost:9080/switch1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch1?off", + "off_method": "GET" + }, + { + "id": "switch2", + "name": "Switch name 2", + "on_url": "http://localhost:9080/switch2?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch2?off", + "off_method": "GET" + }, + { + "id": "switch3", + "name": "Switch name 3", + "on_url": "http://localhost:9080/switch3?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch3?off", + "off_method": "GET" + }, + { + "id": "switch4", + "name": "Switch name*3", + "on_url": "http://localhost:9080/switch3-2?on", + "on_method": "GET", + "off_url": "http://localhost:9080/switch3-2?off", + "off_method": "GET" + } + ], + "pushbuttons": [ + { + "id": "pushbutton1", + "name": "Push button name 1", + "push_url": "http://localhost:9080/pushbutton1?push", + "push_method": "GET" + } + ], + "lights": [ + { + "id": "light1", + "name": "Light name 1", + "on_url": "http://localhost:9080/light1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/light1?off", + "off_method": "GET" + } + ], + "thermostats": [ + { + "id": "thermostat1", + "name": "Thermostat name 1", + "set_target_temperature_url": "http://localhost:9080/thermostat1?targettemperature=%f", + "set_target_heating_cooling_state_url": "http://localhost:9080/thermostat1??targetstate=%b" + } + ], + "outlets": [ + { + "id": "outlet1", + "name": "Outlet name 1", + "on_url": "http://localhost:9080/outlet1?on", + "on_method": "GET", + "off_url": "http://localhost:9080/outlet1?off", + "off_method": "GET" + } + ] + } + ] + }; + + setup.setAdapterConfig(config.common, config.native); + + setupHTTPServer(9080, () => { + setup.startController( + true, + (id, obj) => {}, + (id, state) => { + if (onStateChanged) onStateChanged(id, state); + }, + (_objects, _states) => { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + }); + + it('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Check if adapter started', done => { + checkConnectionOfAdapter(res => { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + () => { + states.subscribeMessage('system.adapter.test.0'); + done(); + }); + }); + }).timeout(60000); + + it('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Wait for npm installs', done => { + setTimeout(() => done(), 30000); + }).timeout(60000); + + it('Test ' + adapterShortName + ' Wrapper: Verify Init', done => { + expect(fs.existsSync(__dirname + '/homebridge/config.json')).to.be.true; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + + states.getState(adapterShortName + '.0.Sun.Accessory-Information.Model', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.equal('Sun Position'); + + states.getState(adapterShortName + '.0.Sun.Sun.Altitude', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.exist; + done(); + }); + }); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test Change from inside', done => { + request('http://localhost:61828/?accessoryId=switch1&state=true', (error, response, body) => { + expect(error).to.be.null; + expect(response && response.statusCode).to.be.equal(200); + + setTimeout(function() { + expect(lastHTTPRequest).to.be.null; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.true; + done(); + }); + }, 2000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic', done => { + states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: false, ack:false}, function (err) { + expect(err).to.not.exist; + + setTimeout(function() { + expect(lastHTTPRequest).to.be.equal('/switch1?off'); + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + done(); + }); + }, 2000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic 2', done => { + states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: true, ack:false}, function (err) { + expect(err).to.not.exist; + + setTimeout(function() { + expect(lastHTTPRequest).to.be.equal('/switch1?on'); + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.true; + done(); + }); + }, 2000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test Change from inside 2', done => { + lastHTTPRequest = null; + request('http://localhost:61828/?accessoryId=switch1&state=false', (error, response, body) => { + expect(error).to.be.null; + expect(response && response.statusCode).to.be.equal(200); + + setTimeout(function() { + expect(lastHTTPRequest).to.be.null; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + done(); + }); + }, 2000); + }); + }).timeout(10000); + + after('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + httpServer.close(); + done(); + }); + }); +}); diff --git a/test/testAdapterXGlobal.js b/test/testAdapterXGlobal.js new file mode 100644 index 0000000..5a5fd9f --- /dev/null +++ b/test/testAdapterXGlobal.js @@ -0,0 +1,233 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; +const expect = require('chai').expect; +const setup = require(__dirname + '/lib/setup'); +const request = require('request'); +const http = require('http'); + +let objects = null; +let states = null; +let onStateChanged = null; +let onObjectChanged = null; +let sendToID = 1; + +const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +let httpServer; +let lastHTTPRequest = null; + +function setupHTTPServer(port, callback) { + httpServer = http.createServer(function (req, res) { + lastHTTPRequest = req.url; + console.log('HTTP Received: ' + lastHTTPRequest); + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('OK'); + }).listen(port); + setTimeout(function() { + callback(); + }, 5000); +} + +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', (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, (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 = (id, state) => (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: Date.now() + } + }); +} + +describe('Test ' + adapterShortName + ' Global adapter', () => { + before('Test ' + adapterShortName + ' Global adapter: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(() => { + const config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.useGlobalHomebridge = true; + config.native.globalHomebridgeBasePath = process.env.NODE_GLOBAL_DIR + "/homebridge/"; + config.native.globalHomebridgeConfigPath = __dirname + "/homebridge/"; + + setup.setAdapterConfig(config.common, config.native); + + setupHTTPServer(9080, () => { + setup.startController(true, + (id, obj) => {}, + (id, state) => onStateChanged && onStateChanged(id, state), + (_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 + ' Global adapter: Check if adapter started', done => { + checkConnectionOfAdapter(res => { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + () => { + states.subscribeMessage('system.adapter.test.0'); + done(); + }); + }); + }).timeout(60000); + + it('Test ' + adapterShortName + ' Wrapper adapter: Wait for init', done => { + setTimeout(() => done(), 20000); + }).timeout(60000); + + it('Test ' + adapterShortName + ' Wrapper: Verify Init', done => { + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + + states.getState(adapterShortName + '.0.Sun.Accessory-Information.Model', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.equal('Sun Position'); + + states.getState(adapterShortName + '.0.Sun.Sun.Altitude', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.exist; + done(); + }); + }); + }); + }); + + it('Test ' + adapterShortName + ' Wrapper: Test Change from inside', done => { + request('http://localhost:61828/?accessoryId=switch1&state=true', (error, response, body) => { + expect(error).to.be.null; + expect(response && response.statusCode).to.be.equal(200); + + setTimeout(function() { + expect(lastHTTPRequest).to.be.null; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.true; + done(); + }); + }, 3000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic', done => { + states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: false, ack: false}, err => { + expect(err).to.not.exist; + + setTimeout(function() { + expect(lastHTTPRequest).to.be.equal('/switch1?off'); + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + done(); + }); + }, 3000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic 2', done => { + states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: true, ack: false}, err => { + expect(err).to.not.exist; + + setTimeout(function() { + expect(lastHTTPRequest).to.be.equal('/switch1?on'); + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.true; + done(); + }); + }, 3000); + }); + }).timeout(10000); + + it('Test ' + adapterShortName + ' Wrapper: Test Change from inside 2', done => { + lastHTTPRequest = null; + request('http://localhost:61828/?accessoryId=switch1&state=false', (error, response, body) => { + expect(error).to.be.null; + expect(response && response.statusCode).to.be.equal(200); + + setTimeout(function() { + expect(lastHTTPRequest).to.be.null; + states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => { + expect(err).to.not.exist; + expect(state.val).to.be.false; + done(); + }); + }, 3000); + }); + }).timeout(10000); + + after('Test ' + adapterShortName + ' Global adapter: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + httpServer.close(); + done(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..e42540b --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,93 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +'use strict'; + +const expect = require('chai').expect; +const fs = require('fs'); + +describe('Test package.json and io-package.json', () => { + it('Test package files', done => { + console.log(); + + const fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + const ioPackage = JSON.parse(fileContentIOPackage); + + const fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + const 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('yunkong2') !== -1 || + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.'); + 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; + } + } + + const licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + const 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(); + }); +});