commit d586cb4088722a038d17139a9474c4e9890fadfc Author: GermanBluefox Date: Sun Mar 11 01:38:23 2018 +0100 (bluefox) initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4a6c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.idea +tmp +admin/i18n/flat.txt +admin/i18n/*/flat.txt +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..10a99f6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +gulpfile.js +tasks +tmp +test +.travis.yml +appveyor.yml +admin/i18n +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1505825 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' +services: + - mysql + - postgresql +before_install: + - 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export export CXX=g++-4.8; fi' + - npm install -g npm@3 +before_script: + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install mariadb && mysql.server start; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then rm -rf /usr/local/var/postgres; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then initdb /usr/local/var/postgres; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pg_ctl -D /usr/local/var/postgres start; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then sleep 5; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then createuser -s postgres; fi' + - sleep 15 + - npm install winston@2.3.1 + - 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production' +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6112339 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 bluefox + +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..c82737c --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +![Logo](admin/hass.png) +# ioBroker.hass +================= + +[![NPM version](http://img.shields.io/npm/v/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass) +[![Downloads](https://img.shields.io/npm/dm/iobroker.hass.svg)](https://www.npmjs.com/package/iobroker.hass) + +[![NPM](https://nodei.co/npm/iobroker.hass.png?downloads=true)](https://nodei.co/npm/iobroker.hass/) + + +This adapter allows connect Home Assistant to ioBroker. + +## Changelog +#### 0.1.0 +* (bluefox) initial release + +## License +The MIT License (MIT) + +Copyright (c) 2018 bluefox + +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/hass.png b/admin/hass.png new file mode 100644 index 0000000..f7e5789 Binary files /dev/null and b/admin/hass.png differ diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..5ae4d6b --- /dev/null +++ b/admin/index.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + +
+ + + +

FHEM adapter settings

+ + + + + + +
+
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..5ae4d6b --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + +
+ + + +

FHEM adapter settings

+ + + + + + +
+
+ + diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..513a18c --- /dev/null +++ b/admin/words.js @@ -0,0 +1,19 @@ +systemDictionary = { + "FHEM adapter settings":{"de": "FHEM Adapter Einstellungen", "ru": "Настройки драйвера FHEM"}, + "FHEM Telnet IP:": {"de": "FHEM Telnet IP:", "ru": "FHEM Telnet IP:"}, + "FHEM Telnet Port:": {"en": "FHEM Telnet Port:", "de": "FHEM Telnet Port:", "ru": "FHEM Telnet порт:"}, + "Password:": {"en": "Password:", "de": "Kennwort:", "ru": "Пароль:"}, + "Password repeat:": {"en": "Password repeat:", "de": "Kennwort-Woederholung:", "ru": "Подтверждение пароля:"}, + "Passwords missmatch!": {"en": "Passwords missmatch!", "de": "Kennworte sind unterschiedlich!", "ru": "Пароли не совпадают!"}, + "Prompt:": {"en": "Prompt:", "de": "Prompt:", "ru": "Промпт:"}, + "tooltip_password": { + "en": "Optional. Only if FHEM requires password.", + "de": "Optional. Nur falls in FHEM konfiguriert", + "ru": "Не обязательно. Только если настроено в FHEM" + }, + "tooltip_prompt": { + "en": "You can change standard prompt from 'hass>' to something else.", + "de": "Man kann hier ein eigenes Prompt setzen. Default ist 'hass>'", + "ru": "Можно задать собственный промпт. По умолчанию 'hass>'" + } +}; \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..73142d9 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,28 @@ +version: 'test-{build}' +os: Visual Studio 2013 +environment: + matrix: + - nodejs_version: '4' + - nodejs_version: '6' + - nodejs_version: '8' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +services: + - mssql2014 + - mysql + - postgresql +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - npm install -g npm@3 + - npm install + - npm install winston@2.3.1 + - 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - ps: Start-Sleep -s 15 + - npm test +build: 'off' diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..fd48c48 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,401 @@ +'use strict'; + +var gulp = require('gulp'); +var fs = require('fs'); +var pkg = require('./package.json'); +var iopackage = require('./io-package.json'); +var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; +/*var appName = getAppName(); + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 1].split('.')[0].toLowerCase(); +} +*/ +const fileName = 'words.js'; +var languages = { + en: {}, + de: {}, + ru: {}, + pt: {}, + nl: {}, + fr: {}, + it: {}, + es: {}, + pl: {} +}; + +function lang2data(lang, isFlat) { + var str = isFlat ? '' : '{\n'; + var count = 0; + for (var w in lang) { + if (lang.hasOwnProperty(w)) { + count++; + if (isFlat) { + str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n'; + } else { + var key = ' "' + w.replace(/"/g, '\\"') + '": '; + str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; + } + } + } + if (!count) return isFlat ? '' : '{\n}'; + if (isFlat) { + return str; + } else { + return str.substring(0, str.length - 2) + '\n}'; + } +} + +function readWordJs(src) { + try { + var words; + if (fs.existsSync(src + 'js/' + fileName)) { + words = fs.readFileSync(src + 'js/' + fileName).toString(); + } else { + words = fs.readFileSync(src + fileName).toString(); + } + + var lines = words.split(/\r\n|\r|\n/g); + var i = 0; + while (!lines[i].match(/^systemDictionary = {/)) { + i++; + } + lines.splice(0, i); + + // remove last empty lines + i = lines.length - 1; + while (!lines[i]) { + i--; + } + if (i < lines.length - 1) { + lines.splice(i + 1); + } + + lines[0] = lines[0].replace('systemDictionary = ', ''); + lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}'); + words = lines.join('\n'); + var resultFunc = new Function('return ' + words + ';'); + + return resultFunc(); + } catch (e) { + return null; + } +} +function padRight(text, totalLength) { + return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : ''); +} +function writeWordJs(data, src) { + var text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n'; + text += '/*global systemDictionary:true */\n'; + text += '\'use strict\';\n\n'; + + text += 'systemDictionary = {\n'; + for (var word in data) { + if (data.hasOwnProperty(word)) { + text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50); + var line = ''; + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' '; + } + } + if (line) { + line = line.trim(); + line = line.substring(0, line.length - 1); + } + text += line + '},\n'; + } + } + text = text.replace(/},\n$/, '}\n'); + text += '};'; + + if (fs.existsSync(src + 'js/' + fileName)) { + fs.writeFileSync(src + 'js/' + fileName, text); + } else { + fs.writeFileSync(src + '' + fileName, text); + } +} + +const EMPTY = ''; + +function words2languages(src) { + var langs = Object.assign({}, languages); + var data = readWordJs(src); + if (data) { + for (var word in data) { + if (data.hasOwnProperty(word)) { + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (var j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (var l in langs) { + if (!langs.hasOwnProperty(l)) continue; + var keys = Object.keys(langs[l]); + //keys.sort(); + var obj = {}; + for (var k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + if (!fs.existsSync(src + 'i18n/' + l)) { + fs.mkdirSync(src + 'i18n/' + l); + } + + fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj)); + } + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function words2languagesFlat(src) { + var langs = Object.assign({}, languages); + var data = readWordJs(src); + if (data) { + for (var word in data) { + if (data.hasOwnProperty(word)) { + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (var j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + var keys = Object.keys(langs.en); + //keys.sort(); + for (var l in langs) { + if (!langs.hasOwnProperty(l)) continue; + var obj = {}; + for (var k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + langs[l] = obj; + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (var ll in langs) { + if (!langs.hasOwnProperty(ll)) continue; + if (!fs.existsSync(src + 'i18n/' + ll)) { + fs.mkdirSync(src + 'i18n/' + ll); + } + + fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en)); + } + fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n')); + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function languagesFlat2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + 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].replace(/<\/ i>/g, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); + }); + + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} +function languages2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} + +gulp.task('adminWords2languages', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('adminWords2languagesFlat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('adminLanguagesFlat2words', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('adminLanguages2words', function (done) { + languages2words('./admin/'); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + var news = iopackage.common.news; + var newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('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..732146d --- /dev/null +++ b/io-package.json @@ -0,0 +1,84 @@ +{ + "common": { + "name": "hass", + "version": "0.1.0", + "title": "Home Assistant", + "desc": { + "en": "Home Assistant connection for ioBroker", + "de": "Home Assistant Verbindung für ioBroker", + "ru": "Подключение Home Assistant для ioBroker", + "pt": "Conexão do Home Assistant para ioBroker", + "nl": "Home Assistant-verbinding voor ioBroker", + "fr": "Connexion Home Assistant pour ioBroker", + "it": "Connessione Home Assistant per ioBroker", + "es": "Conexión de Home Assistant para ioBroker", + "pl": "Połączenie Home Assistant dla ioBroker" + }, + "news": { + "0.1.0" : { + "en": "initial version", + "de": "erste Version", + "ru": "Первоначальная версия", + "pt": "versão inicial", + "nl": "eerste versie", + "fr": "version initiale", + "it": "versione iniziale", + "es": "versión inicial", + "pl": "początkowa wersja" + } + }, + "localLink": "http://%host%:8123", + "platform": "Javascript/Node.js", + "mode": "daemon", + "icon": "hass.png", + "enabled": true, + "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.hass/master/admin/hass.png", + "keywords": ["HASS", "Home", "assistant"], + "readme": "https://github.com/ioBroker/ioBroker.hass/blob/master/README.md", + "loglevel": "info", + "type": "iot-systems", + "authors": [ + "bluefox " + ] + }, + "native": { + "host": "127.0.0.1", + "port": 8123, + "password": "", + "secure": false + }, + "objects": [ + + ], + "instanceObjects": [ + { + "_id": "info", + "type": "channel", + "common": { + "name": "Information" + }, + "native": {} + }, + { + "_id": "info.connection", + "type": "state", + "common": { + "role": "indicator.connected", + "name": "If connected to HASS server", + "type": "boolean", + "read": true, + "write": false, + "def": false + }, + "native": {} + }, + { + "_id": "entities", + "type": "channel", + "common": { + "name": "HASS entities" + }, + "native": {} + } + ] +} diff --git a/lib/hass.js b/lib/hass.js new file mode 100644 index 0000000..5c52ae6 --- /dev/null +++ b/lib/hass.js @@ -0,0 +1,233 @@ +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var WebSocket = require('ws'); + +function HASS(options, log) { + if (!(this instanceof HASS)) return new HASS(options); + + options = options || {}; + options.host = options.host || '127.0.0.1'; + options.port = parseInt(options.port, 10) || 8123; + + var ERRORS = { + 1: 'ERR_CANNOT_CONNECT', + 2: 'ERR_INVALID_AUTH', + 3: 'ERR_CONNECTION_LOST' + }; + + this.socket = null; + var that = this; + var currentId = 1; + var requests = {}; + var connected; + var connectTimeout = null; + + function subscribeEvents(socket, callback) { + if (socket && typeof socket.send === 'function') { + var id = currentId++; + requests[id] = {type: 'subscribe_events', ts: Date.now(), cb: callback}; + + socket.send(JSON.stringify({ + id: id, + type: 'subscribe_events' + /*event_type: 'state_changed'*/ + })); + } else { + callback && callback('not connected'); + } + } + + function getConfig(socket, callback) { + if (socket && typeof socket.send === 'function') { + var id = currentId++; + requests[id] = {type: 'get_config', cb: callback, ts: Date.now()}; + socket.send(JSON.stringify({ + id: id, + type: 'get_config' + })); + } else { + callback && callback('not connected'); + } + } + + function getStates(socket, callback) { + if (socket && typeof socket.send === 'function') { + var id = currentId++; + requests[id] = {type: 'get_states', cb: callback, ts: Date.now()}; + socket.send(JSON.stringify({ + id: id, + type: 'get_states' + })); + } else { + callback && callback('not connected'); + } + } + + function getServices(socket, callback) { + if (socket && typeof socket.send === 'function') { + var id = currentId++; + requests[id] = {type: 'get_services', cb: callback, ts: Date.now()}; + socket.send(JSON.stringify({ + id: id, + type: 'get_services' + })); + } else { + callback && callback('not connected'); + } + } + + function callService(socket, service, domain, serviceData, callback) { + if (socket && typeof socket.send === 'function') { + var id = currentId++; + requests[id] = {type: 'call_service', cb: callback, ts: Date.now()}; + socket.send(JSON.stringify({ + id: id, + type: 'call_service', + domain: domain || 'light', + service: service, + service_data: serviceData + })); + } else { + callback && callback('not connected'); + } + } + + function sendAuth(socket, pass) { + if (socket && typeof socket.send === 'function') { + socket.send(JSON.stringify({ + type: 'auth', + api_password: pass + })); + } + } + + function initSocket(socket) { + socket.on('message', function (msg) { + //log.debug(msg); + + var response = JSON.parse(msg); + if (response.type === 'auth_required') { + if (!options.password) { + that.emit('error', 'Password required. Connection closed'); + socket.terminate(); + } else { + setTimeout(function () { + sendAuth(socket, options.password); + }, 50); + } + } else + if (response.type === 'auth_ok') { + setImmediate(function () { + subscribeEvents(socket, function (err) { + if (!err) { + connected = true; + that.emit('connected'); + } + }); + }); + } else if (response.id === undefined) { + log.error('Invalid answer: ' + msg); + } else { + if (response.type === 'result' && requests[response.id]) { + log.debug('got answer for ' + requests[response.id].type); + if (typeof requests[response.id].cb === 'function') { + requests[response.id].cb(!response.success, response.result); + delete requests[response.id]; + } + } + } + }); + + socket.on('error', function (err) { + socket = null; + if (err && err.message.indexOf('RSV2 and RSV3 must be clear') !== -1) { + // ignore deflate error + } else { + log.error(err); + } + }); + socket.on('open', function () { + if (!connected) { + + } + }); + socket.on('close', function () { + that.socket = null; + if (connected) { + connected = false; + that.emit('disconnected'); + + } + if (!connectTimeout) { + setTimeout(function () { + connectTimeout = null; + that.connect(); + }, 3000); + } + }); + } + + this.isConnected = function () { + return connected; + }; + + this.getConfig = function (callback) { + if (!connected) { + if (typeof callback === 'function') { + callback('not connected'); + } + } else { + getConfig(this.socket, callback); + } + }; + + this.getStates = function (callback) { + if (!connected) { + if (typeof callback === 'function') { + callback('not connected'); + } + } else { + getStates(this.socket, callback); + } + }; + + this.getServices = function (callback) { + if (!connected) { + if (typeof callback === 'function') { + callback('not connected'); + } + } else { + getServices(this.socket, callback); + } + }; + + this.callService = function (service, domain, serviceData, callback) { + if (!connected) { + if (typeof callback === 'function') { + callback('not connected'); + } + } else { + callService(this.socket, service, domain, serviceData, callback); + } + }; + + this.connect = function () { + if (connectTimeout) { + clearTimeout(connectTimeout); + connectTimeout = null; + } + + this.socket = new WebSocket('ws' + (options.secure ? 's' : '') + '://' + options.host + ':' + options.port + '/api/websocket', { + perMessageDeflate: false + }); + + initSocket(this.socket); + }; + + return this; +} + +// extend the EventEmitter class using our class +util.inherits(HASS, EventEmitter); + +module.exports = HASS; \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..bcbe98a --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,64 @@ +var controllerDir; +var appName; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +// Get js-controller directory to load libs +function getControllerDir(isInstall) { + var fs = require('fs'); + // Find the js-controller location + var controllerDir = __dirname.replace(/\\/g, '/'); + controllerDir = controllerDir.split('/'); + if (controllerDir[controllerDir.length - 3] === 'adapter') { + controllerDir.splice(controllerDir.length - 3, 3); + controllerDir = controllerDir.join('/'); + } else if (controllerDir[controllerDir.length - 3] === 'node_modules') { + controllerDir.splice(controllerDir.length - 3, 3); + controllerDir = controllerDir.join('/'); + if (fs.existsSync(controllerDir + '/node_modules/' + appName + '.js-controller')) { + controllerDir += '/node_modules/' + appName + '.js-controller'; + } else if (fs.existsSync(controllerDir + '/node_modules/' + appName.toLowerCase() + '.js-controller')) { + controllerDir += '/node_modules/' + appName.toLowerCase() + '.js-controller'; + } else if (!fs.existsSync(controllerDir + '/controller.js')) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + } else if (fs.existsSync(__dirname + '/../../node_modules/' + appName.toLowerCase() + '.js-controller')) { + controllerDir.splice(controllerDir.length - 2, 2); + return controllerDir.join('/') + '/node_modules/' + appName.toLowerCase() + '.js-controller'; + } else { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + return controllerDir; +} + +// Read controller configuration file +function getConfig() { + var fs = require('fs'); + if (fs.existsSync(controllerDir + '/conf/' + appName + '.json')) { + return JSON.parse(fs.readFileSync(controllerDir + '/conf/' + appName + '.json')); + } else if (fs.existsSync(controllerDir + '/conf/' + appName.toLowerCase() + '.json')) { + return JSON.parse(fs.readFileSync(controllerDir + '/conf/' + appName.toLowerCase() + '.json')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = require(controllerDir + '/lib/adapter.js'); +exports.appName = appName; diff --git a/main.js b/main.js new file mode 100644 index 0000000..822bf93 --- /dev/null +++ b/main.js @@ -0,0 +1,783 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ + +'use strict'; + +var utils = require(__dirname + '/lib/utils'); +var HASS = require(__dirname + '/lib/hass'); + +var adapter = utils.Adapter('hass'); + +var connected = false; +var hass; +var hassObjects = {}; + +// is called if a subscribed state changes +adapter.on('stateChange', function (id, state) { + // you can use the ack flag to detect if it is status (true) or command (false) + if (state && !state.ack) { + if (!connected) { + adapter.log.warn('Cannot send command to "' + id + '", because not connected'); + return; + } + /*if (id === adapter.namespace + '.' + '.info.resync') { + queue.push({command: 'resync'}); + processQueue(); + } else */ + if (hassObjects[id]) { + if (!hassObjects[id].common.write) { + adapter.log.warn('Object ' + id + ' is not writable!'); + } else { + var serviceData = {}; + var fields = hassObjects[id].native.fields; + + for (var field in fields) { + if (!fields.hasOwnProperty(field)) continue; + + if (fields === 'entity_id') { + serviceData.entity_id = hassObjects[id].native.entity_id + } else { + serviceData.entity_id = state.val; + } + } + + hass.callService(hassObjects[id].native.type, undefined, serviceData, function (err) { + if (err) { + adapter.log.error('Cannot control ' + id + ': ' + err); + } + }); + } + } + } +}); + +// Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ... +adapter.on('message', function (obj) { + if (typeof obj === 'object' && obj.message) { + if (obj.command === 'send') { + // e.g. send email or pushover or whatever + console.log('send command'); + + // Send response in callback if required + if (obj.callback) adapter.sendTo(obj.from, obj.command, 'Message received', obj.callback); + } + } +}); + +// is called when databases are connected and adapter received configuration. +// start here! +adapter.on('ready', main); + +function getUnit(name) { + name = name.toLowerCase(); + if (name.indexOf('temperature') !== -1) { + return '°C'; + } else if (name.indexOf('humidity') !== -1) { + return '%'; + } else if (name.indexOf('pressure') !== -1) { + return 'hPa'; + } else if (name.indexOf('degrees') !== -1) { + return '°'; + } else if (name.indexOf('speed') !== -1) { + return 'kmh'; + } + return undefined; +} + +function syncStates(states, cb) { + if (!states || !states.length) { + cb(); + return; + } + var state = states.shift(); + var id = state.id; + delete state.id; + + adapter.setForeignState(id, state, function (err) { + if (err) adapter.log.error(err); + setImmediate(syncStates, states, cb); + }); +} + +function syncObjects(objects, cb) { + if (!objects || !objects.length) { + cb(); + return; + } + var obj = objects.shift(); + hassObjects[obj._id] = obj; + + var start = Date.now(); + + adapter.getForeignObject(obj._id, function (err, oldObj) { + console.log('getForeignObject: ' + (Date.now() - start)); + + if (err) adapter.log.error(err); + + if (!oldObj) { + adapter.log.debug('Create "' + obj._id + '"'); + hassObjects[obj._id] = obj; + start = Date.now(); + adapter.setForeignObject(obj._id, obj, function (err) { + console.log('setForeignObject: ' + (Date.now() - start)); + if (err) adapter.log.error(err); + + setImmediate(syncObjects, objects, cb); + }); + } else { + hassObjects[obj._id] = oldObj; + if (JSON.stringify(obj.native) !== JSON.stringify(oldObj.native)) { + oldObj.native = obj.native; + + adapter.log.debug('Update "' + obj._id + '"'); + start = Date.now(); + adapter.setForeignObject(obj._id, oldObj, function (err) { + console.log('ssetForeignObject: ' + (Date.now() - start)); + if (err) adapter.log.error(err); + setImmediate(syncObjects, objects, cb); + }); + } else { + setImmediate(syncObjects, objects, cb); + } + } + }); +} + +function syncRoom(room, members, cb) { + adapter.getForeignObject('enum.rooms.' + room, function (err, obj) { + if (!obj) { + obj = { + _id: 'enum.rooms.' + room, + type: 'enum', + common: { + name: room, + members: members + }, + native: {} + }; + adapter.log.debug('Update "' + obj._id + '"'); + adapter.setForeignObject(obj._id, obj, function (err) { + if (err) adapter.log.error(err); + cb(); + }); + } else { + obj.common = obj.common || {}; + obj.common.members = obj.common.members || []; + var changed = false; + for (var m = 0; m < members.length; m++) { + if (obj.common.members.indexOf(members[m]) === -1) { + changed = true; + obj.common.members.push(members[m]); + } + } + if (changed) { + adapter.log.debug('Update "' + obj._id + '"'); + adapter.setForeignObject(obj._id, obj, function (err) { + if (err) adapter.log.error(err); + cb(); + }); + } else { + cb(); + } + } + }); +} + +function syncRooms(rooms, cb) { + for (var r in rooms) { + if (!rooms.hasOwnProperty(r)) continue; + if (rooms[r]) { + syncRoom(r, rooms[r], function () { + setTimeout(syncRooms, 0, rooms, cb); + }); + rooms[r] = null; + return; + } + } + + if (cb) cb(); +} + +//------------------------------------------------------------<<< +function parseObjects(objs, cb) { + var rooms = {}; + var objects = []; + var states = []; + var id; + var obj; + var name; + var ignoreStates = ['getConfig', 'getRegRaw', 'regBulk', 'regSet', 'deviceMsg', 'CommandAccepted']; + + for (var i = 0; i < objs.length; i++) { + try { + name = objs[i].Name.replace(/\./g, '_'); + + if (objs[i].Attributes && objs[i].Attributes.room === 'hidden') continue; + + id = adapter.namespace + '.' + name; + + objects.push({ + _id: id, + type: 'channel', + common: { + name: objs[i].Name + }, + native: objs[i] + }); + + if (objs[i].Attributes && objs[i].Attributes.room) { + var rrr = objs[i].Attributes.room.split(','); + for (var r = 0; r < rrr.length; r++) { + rrr[r] = rrr[r].trim(); + rooms[rrr[r]] = rooms[rrr[r]] || []; + rooms[rrr[r]].push(adapter.namespace + '.' + name); + } + } + + var isOn = false; + var isOff = false; + var setStates = {}; + + if (objs[i].PossibleSets) { + var attrs = objs[i].PossibleSets.split(' '); + for (var a = 0; a < attrs.length; a++) { + if (!attrs[a]) continue; + var parts = attrs[a].split(':'); + + // ignore some useless "sets" + if (ignoreStates.indexOf(parts[0]) !== -1) continue; + + var stateName = parts[0].replace(/\./g, '_'); + id = adapter.namespace + '.' + name + '.' + stateName; + + + if (parts[0] === 'off') isOff = true; + if (parts[0] === 'on') isOn = true; + + obj = { + _id: id, + type: 'state', + common: { + name: objs[i].Name + ' ' + parts[0], + read: false, + write: true + }, + native: { + Name: objs[i].Name, + Attribute: parts[0] + } + }; + if (parts[1]) { + var _states = parts[1].split(','); + // adapter.log.info('LausiD "' + obj._id + ' : ' + _states + '"'); + // obj.common.states = JSON.stringify(_states); + obj.common.states = ''; + + if (parseFloat(_states[0]) == _states[0]) { + obj.common.type = 'number'; + } + } + + obj.common.type = obj.common.type || 'string'; + obj.common.role = 'command'; + // edit 08.03.17 LausiD + // detect pct,Volume,GroupVolume,brightness + if (parts[0] === 'pct' || parts[0] === 'Volume' || parts[0] === 'GroupVolume' || parts[0] === 'brightness') { + // obj.common.write = true; + // obj.common.unit= '%'; + obj.common.type = 'number'; + obj.common.min = '0'; + obj.common.max = '100'; + obj.common.role = 'command.dim.100'; + } + // detect bri,sat + if (parts[0] === 'bri' || parts[0] === 'sat') { + // obj.common.write = true; + // obj.common.unit = '%'; + obj.common.type = 'number'; + obj.common.min = '0'; + obj.common.max = '254'; + obj.common.role = 'command.dim.254'; + } + + + if (parts[0].indexOf('RGB') !== -1) { + obj.common.role = 'light.color.rgb'; + obj.native.rgb = true; + } + if (parts[0].indexOf('HSV') !== -1) { + obj.common.role = 'light.color.hsv'; + obj.native.hsv = true; + } + objects.push(obj); + setStates[stateName] = obj; + //console.log(' ' + obj._id + ': ' + (parts[1] || '')); + } + } + + + /* + if (objs[i].Attributes[attr]) { + // for (var attr in objs[i].Attributes) { + adapter.log.info('LausiD '+ attr); + + + } + } + */ + + /* if (!objs[i].Readings.hasOwnProperty(attr)) continue; + // ignore some useless states + if (ignoreStates.indexOf(attr) !== -1) continue; + + var stateName = attr.replace(/\./g, '_'); + id = adapter.namespace + '.' + name + '.' + stateName; + //adapter.log.info('LausiD '+ id); + var combined = false; + if (setStates[stateName]) { + combined = true; + obj = setStates[stateName]; + obj.common.read = true; + obj.common.unit = getUnit(attr); + } else { + obj = { + _id: id, + type: 'state', + common: { + name: objs[i].Name + ' ' + attr, + read: true, + write: false, + unit: getUnit(attr) + }, + native: { + Name: objs[i].Name, + Attribute: attr + } + }; + } */ + + + if (objs[i].Readings) { + for (var attr in objs[i].Readings) { + if (!objs[i].Readings.hasOwnProperty(attr)) continue; + // ignore some useless states + if (ignoreStates.indexOf(attr) !== -1) continue; + + var stateName = attr.replace(/\./g, '_'); + id = adapter.namespace + '.' + name + '.' + stateName; + var combined = false; + if (setStates[stateName]) { + combined = true; + obj = setStates[stateName]; + obj.common.read = true; + obj.common.unit = getUnit(attr); + } else { + obj = { + _id: id, + type: 'state', + common: { + name: objs[i].Name + ' ' + attr, + read: true, + write: false, + unit: getUnit(attr) + }, + native: { + Name: objs[i].Name, + Attribute: attr + } + }; + } + + if (objs[i].Readings[attr]) { + var val = convertFhemValue(objs[i].Readings[attr].Value); + obj.common.type = obj.common.type || typeof val; + obj.common.role = obj.common.role || 'value'; + + states.push({ + id: obj._id, + val: val, + ts: objs[i].Readings[attr].Time ? new Date(objs[i].Readings[attr].Time).getTime() : new Date().getTime(), + ack: true + }); + + // detect pct + if (attr === 'pct' || attr === 'Volume' || attr === 'GroupVolume' || attr === 'brightness') { + obj.common.unit = '%'; + } + // detect bri,sat + if (attr === 'bri' || attr === 'sat') { + obj.common.unit = '%'; + } + + // detect state + if (attr === 'state') { + obj.common.write = true; + obj.native.onoff = true; + obj.common.role = 'switch'; + } + // detect on/off state + if (isOff && isOn && attr === 'state') { + obj.common.write = true; + obj.native.onoff = true; + obj.common.role = 'switch'; + } + + if (!combined) objects.push(obj); + } + } + delete objs[i].Readings; + } + setStates = null; + + /*id = adapter.namespace + '.' + name + '.lastError'; + obj = { + _id: id, + type: 'state', + common: { + name: objs[i].Name + ' lastError', + read: true, + write: false, + def: '', + type: 'string', + role: 'error' + }, + native: objs[i] + }; + objects.push(obj);*/ + + /*id = adapter.namespace + '.' + objs[i].Name + '.validity'; + obj = { + _id: id, + type: 'state', + common: { + name: objs[i].Name + ' validity', + read: true, + write: false, + def: '', + type: 'string', + role: 'state.quality' + }, + native: objs[i] + }; + objects.push(obj);*/ + + } catch (err) { + adapter.log.error('Cannot process object: ' + JSON.stringify(objs[i])); + adapter.log.error('Cannot process object: ' + err); + } + } + + syncObjects(objects, function () { + syncRooms(rooms, function () { + syncStates(states, cb); + }); + }); +} + +function readValue(id, cb) { + telnetOut.send('get ' + hassObjects[id].native.Name + ' ' + hassObjects[id].native.Attribute, function (err, result) { + if (err) adapter.log.error('readValue: ' + err); + // MeinWetter city => Berlin + if (result) { + result = convertFhemValue(result.substring(hassObjects[id].native.Name.length + hassObjects[id].native.Attribute + 5)); + if (result !== '') { + adapter.setForeignState(id, result, true); + } + } + + if (cb) cb(); + }); +} + +function writeValue(id, val, cb) { + var cmd; + var val_org = val; + if (val === undefined || val === null) val = ''; + // edit LausiD 05.03.17 + // May be RGB + if (hassObjects[id].native.Attribute === 'rgb') val = val.substring(1); + + // if (typeof val === 'string' && val[0] === '#' && val.length > 3) val = val.substring(1); + // if (hassObjects[id].native.rgb) { + // } + + if (hassObjects[id].native.Attribute === 'state') { + if (val === '1' || val === 1 || val === 'on' || val === 'true' || val === true) val = 'on'; + if (val === '0' || val === 0 || val === 'off' || val === 'false' || val === false) val = 'off'; + cmd = 'set ' + hassObjects[id].native.Name + ' ' + val; + // adapter.log.info(adapter.namespace + '.' + hassObjects[id].native.Name + '.' + hassObjects[id].native.Attribute + '.' + val_org + ' ==> ' + cmd); + } + else { + cmd = 'set ' + hassObjects[id].native.Name + ' ' + hassObjects[id].native.Attribute + ' ' + val; + } + adapter.log.info(adapter.namespace + '.' + hassObjects[id].native.Name + '.' + hassObjects[id].native.Attribute + '.' + val_org + ' ==> writeFHEM: ' + cmd); + // edit end LausiD 05.03.17 + + telnetOut.send(cmd, function (err, result) { + if (err) adapter.log.error('writeValue: ' + err); + if (cb) cb(); + }); +} + +var knownAttributes = { + azimuth: {write: false, read: true, unit: '°'}, + elevation: {write: false, read: true, unit: '°'} +}; + + +var ERRORS = { + 1: 'ERR_CANNOT_CONNECT', + 2: 'ERR_INVALID_AUTH', + 3: 'ERR_CONNECTION_LOST' +}; +var mapTypes = { + 'string': 'string', + 'number': 'number', + 'object': 'mixed', + 'boolean': 'boolean' +}; +var skipServices = [ + 'persistent_notification' +]; + +function parseStates(entities, services, callback) { + var objs = []; + var states = []; + var obj; + var channel; + for (var e = 0; e < entities.length; e++) { + var entity = entities[e]; + if (!entity) continue; + + var name = entity.attributes && entity.attributes.friendly_name ? entity.attributes.friendly_name : entity.entity_id; + var desc = entity.attributes && entity.attributes.attribution ? entity.attributes.attribution : undefined; + + channel = { + _id: adapter.namespace + '.entities.' + entity.entity_id, + common: { + name: name + }, + type: 'channel', + native: { + entity_id: entity.entity_id + } + }; + if (desc) channel.common.desc = desc; + objs.push(channel); + + var lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined; + var ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined; + + if (entity.state !== undefined) { + obj = { + _id: adapter.namespace + '.entities.' + entity.entity_id + '.state', + type: 'state', + common: { + name: name + ' STATE', + read: true, + write: false + }, + native: { + entity_id: entity.entity_id + } + }; + objs.push(obj); + states.push({id: obj._id, lc: lc, ts: ts, val: entity.state, ack: true}) + } + + if (entity.attributes) { + for (var attr in entity.attributes) { + if (entity.attributes.hasOwnProperty(attr)) { + if (attr === 'friendly_name') continue; + + var common; + if (knownAttributes[attr]) { + common = Object.assign({}, knownAttributes[attr]); + } else { + common = {}; + } + + obj = { + _id: adapter.namespace + '.entities.' + entity.entity_id + '.' + attr, + type: 'state', + common: common, + native: { + entity_id: entity.entity_id, + attr: attr + } + }; + if (!common.name) { + common.name = name + ' ' + attr.replace(/_/g, ' '); + } + if (common.read === undefined) { + common.read = true; + } + if (common.write === undefined) { + common.write = false; + } + if (common.type === undefined) { + common.type = mapTypes[typeof entity.attributes[attr]]; + } + + objs.push(obj); + states.push({id: obj._id, lc: lc, ts: ts, val: entity.attributes[attr], ack: true}); + } + } + } + + var serviceType = entity.entity_id.split('.')[0]; + + if (services[serviceType] && skipServices.indexOf(serviceType) === -1) { + var service = services[serviceType]; + for (var s in service) { + if (service.hasOwnProperty(s)) { + obj = { + _id: adapter.namespace + '.entities.' + entity.entity_id + '.' + s, + type: 'state', + common: { + desc: service.description, + read: false, + write: true + }, + native: { + field: service.fields, + entity_id: entity.entity_id, + attr: s, + type: serviceType + } + }; + objs.push(obj); + } + } + } + } + + syncObjects(objs, function () { + syncStates(states, callback); + }); +} + +function main() { + adapter.config.host = adapter.config.host || '127.0.0.1'; + adapter.config.port = parseInt(adapter.config.port, 10) || 8123; + + adapter.setState('info.connection', false, true); + + // in this template all states changes inside the adapters namespace are subscribed + adapter.subscribeStates('*'); + + hass = new HASS(adapter.config, adapter.log); + + hass.on('error', function (err) { + adapter.log.error(err); + }); + + hass.on('connected', function () { + if (!connected) { + adapter.log.debug('Connected'); + connected = true; + adapter.setState('info.connection', true, true); + + hass.getConfig(function (err, config) { + if (err) { + adapter.log.error('Cannot read config: ' + err); + return; + } + //adapter.log.debug(JSON.stringify(config)); + setTimeout(function () { + hass.getStates(function (err, states) { + if (err) { + return adapter.log.error('Cannot read states: ' + err); + } + //adapter.log.debug(JSON.stringify(states)); + setTimeout(function () { + hass.getServices(function (err, services) { + if (err) { + adapter.log.error('Cannot read states: ' + err); + } else { + //adapter.log.debug(JSON.stringify(services)); + parseStates(states, services, function () { + + }); + } + }); + }, 100); + }); + }, 100); + }); + } + }); + + hass.on('disconnected', function () { + if (connected) { + adapter.log.debug('Disconnected'); + connected = false; + adapter.setState('info.connection', false, true); + } + }); + + hass.connect(); + + /*const ssocket = new WebSocket('ws://' + adapter.config.host + ':' + adapter.config.port + '/api/websocket'); + ssocket.addEventListener('message', function (msg) { + console.log(JSON.stringify(msg.a)); + }); + ssocket.addEventListener('close', function () { + console.log('Closed'); + }); + ssocket.addEventListener('error', function (err) { + console.error(err); + }); + + hass.createConnection('ws://' + adapter.config.host + ':' + adapter.config.port + '/api/websocket').then(function (conn) { + if (!connected) { + adapter.log.debug('Connected'); + connected = true; + adapter.setState('info.connection', true, true); + } + socket = conn; + hass.subscribeEntities(conn, function (entities) { + console.log('New entities!', entities); + var attrs = Object.keys(entities); + for (var a = 0; a < attrs.length; a++) { + console.log(attrs[a] + ': ' + entities[attrs[a]].state); + } + }); + hass.subscribeConfig(conn, function (config) { + console.log('New config!', config) + }); + conn.addEventListener('ready', function () { + if (!connected) { + adapter.log.debug('Connected'); + connected = true; + adapter.setState('info.connection', true, true); + } + }); + conn.addEventListener('disconnected', function () { + if (connected) { + adapter.log.debug('Disconnected'); + connected = false; + adapter.setState('info.connection', false, true); + } + }); + + conn.getStates(function (states) { + console.log('getStates: ', states) + }); + + }).catch(function (err) { + if (err === 2) { + adapter.log.error('Invalid password!.'); + } else { + adapter.log.error('Connection failed with code', ERRORS[err] || err); + } + socket = null; + + if (connected) { + adapter.log.debug('Disconnected'); + connected = false; + adapter.setState('info.connection', false, true); + } + });*/ +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a1d5e28 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "iobroker.hass", + "version": "0.1.0", + "description": "Home Assistant", + "author": { + "name": "bluefox", + "email": "dogafox@gmail.com" + }, + "contributors": [ + { + "name": "bluefox", + "email": "dogafox@gmail.com" + } + ], + "homepage": "https://github.com/ioBroker/ioBroker.hass", + "license": "MIT", + "keywords": [ + "ioBroker", + "hass", + "REST API", + "IoT" + ], + "repository": { + "type": "git", + "url": "https://github.com/ioBroker/ioBroker.hass" + }, + "dependencies": { + "home-assistant-js-websocket": "^1.1.4", + "websocket": "^1.0.25", + "ws": "^5.0.0" + }, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^4.1.0", + "chai": "^4.1.2" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "bugs": { + "url": "https://github.com/ioBroker/ioBroker.hass/issues" + }, + "readmeFilename": "README.md" +} diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..7433f9d --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,696 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +var fs = require('fs'); +var path = require('path'); +var child_process = require('child_process'); +var rootDir = path.normalize(__dirname + '/../../'); +var pkg = require(rootDir + 'package.json'); +var debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +var adapterStarted = false; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +var appName = getAppName().toLowerCase(); + +var objects; +var states; + +var pid = null; + +function copyFileSync(source, target) { + + var targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + fs.writeFileSync(targetFile, fs.readFileSync(source)); +} + +function copyFolderRecursiveSync(source, target, ignore) { + var files = []; + + var base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + var targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + var curSource = path.join(source, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, targetFolder); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(function() { + checkIsAdapterInstalled(cb, counter + 1); + }, 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(function() { + checkIsControllerInstalled(cb, counter + 1); + }, 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, function () { + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + var _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, function () { + checkIsControllerInstalled(function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + var client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', function() { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(function () { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/iobroker-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + if (cb) cb(); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) callback(objects, states); + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + startAdapter(objects, states, callback); + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/testAdapter.js b/test/testAdapter.js new file mode 100644 index 0000000..17a3028 --- /dev/null +++ b/test/testAdapter.js @@ -0,0 +1,125 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + console.log('Try check #' + counter); + if (counter > 30) { + if (cb) cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error(err); + if (state && state.val) { + if (cb) cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + if (cb) cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error(err); + if (value === null && !state) { + if (cb) cb(); + } else + if (state && (value === undefined || state.val === value)) { + if (cb) cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test ' + adapterShortName + ' adapter', function() { + before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + //config.native.dbtype = 'sqlite'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + +/* + ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY +*/ + it('Test ' + adapterShortName + ' adapter: Check if connected', function (done) { + this.timeout(60000); + setTimeout(function () { + states.getState('hass.0.info.connection', function (err, state) { + if (err) console.error(err); + expect(err).to.be.not.ok; + expect(state).to.be.ok; + expect(state.val).to.be.true; + done(); + }); + }, 5000); + }); + + after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..a85d1e3 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,47 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version).to.exist; + expect(npmPackage.version).to.exist; + + if (!expect(ioPackage.common.version).to.be.equal(npmPackage.version)) { + console.log('ERROR: Version numbers in package.json and io-package.json differ!!'); + } + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + } + + expect(ioPackage.common.authors).to.exist; + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length).to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0]).to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors).to.not.be.equal('my Name '); + } + } + else { + console.log('Testing for set authors field in io-package skipped because template adapter'); + } + done(); + }); +});