commit 86286962947cf106b9a21f7da4b641f66281de47 Author: zhongjin Date: Sun Sep 23 14:32:21 2018 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0f2b01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/.idea diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..a2a50dd --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +Gruntfile.js +tasks +node_modules +.idea +.gitignore +.git +.DS_Store +test +.travis.yml +appveyor.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5a5145d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' + - '10' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..12d25d0 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,130 @@ +// To use this file in WebStorm, right click on the file name in the Project Panel (normally left) and select "Open Grunt Console" + +/** @namespace __dirname */ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +"use strict"; + +module.exports = function (grunt) { + + var srcDir = __dirname + '/'; + var dstDir = srcDir + '.build/'; + var pkg = grunt.file.readJSON('package.json'); + var iopackage = grunt.file.readJSON('io-package.json'); + var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; + + // Project configuration. + grunt.initConfig({ + pkg: pkg, + replace: { + core: { + options: { + patterns: [ + { + match: /var version = *'[\.0-9]*';/g, + replacement: "var version = '" + version + "';" + }, + { + match: /"version"\: *"[\.0-9]*",/g, + replacement: '"version": "' + version + '",' + } + ] + }, + files: [ + { + expand: true, + flatten: true, + src: [ + srcDir + 'controller.js', + srcDir + 'package.json', + srcDir + 'io-package.json' + ], + dest: srcDir + } + ] + } + }, + // Javascript code styler + jscs: require(__dirname + '/tasks/jscs.js'), + // Lint + jshint: require(__dirname + '/tasks/jshint.js'), + http: { + get_hjscs: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/tasks/jscs.js' + }, + dest: 'tasks/jscs.js' + }, + get_jshint: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/tasks/jshint.js' + }, + dest: 'tasks/jshint.js' + }, + get_gruntfile: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.build/master/adapters/Gruntfile.js' + }, + dest: 'Gruntfile.js' + }, + get_utilsfile: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.build/master/adapters/utils.js' + }, + dest: 'lib/utils.js' + }, + get_jscsRules: { + options: { + url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/tasks/jscsRules.js' + }, + dest: 'tasks/jscsRules.js' + } + } + }); + + grunt.registerTask('updateReadme', function () { + var readme = grunt.file.read('README.md'); + 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.whatsNew) { + for (var i = 0; i < iopackage.common.whatsNew.length; i++) { + if (typeof iopackage.common.whatsNew[i] == 'string') { + news += '* ' + iopackage.common.whatsNew[i] + '\n'; + } else { + news += '* ' + iopackage.common.whatsNew[i].en + '\n'; + } + } + } + + grunt.file.write('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + }); + + grunt.loadNpmTasks('grunt-replace'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-jscs'); + grunt.loadNpmTasks('grunt-http'); + + grunt.registerTask('default', [ + 'http', + 'replace', + 'updateReadme', + 'jshint', + 'jscs' + ]); + grunt.registerTask('p', [ + 'replace', + 'updateReadme' + ]); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d6c4ea --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +![Logo](admin/geofency.png) +# yunkong2.geofency +==================== + +[![NPM version](http://img.shields.io/npm/v/yunkong2.geofency.svg)](https://www.npmjs.com/package/yunkong2.geofency) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.geofency.svg)](https://www.npmjs.com/package/yunkong2.geofency) + +[![NPM](https://nodei.co/npm/yunkong2.geofency.png?downloads=true)](https://nodei.co/npm/yunkong2.geofency/) + + +This Adapter is able to receive [geofency](http://www.geofency.com/) events when entering or leaving a defined area with your mobile device. +All values of the geofency-webhook of the request are stored under the name of the location in yunkong2. + +## configuration on mobile device: +* for any location -> properties -> webhook settings: + * URL for entry & exit: <your yunkong2 Domain>:<configured port>/<any locationname> + * Post Format: JSON-encoded: enabled + * authentication: set user / password from yunkong2.geofency config + +## in yunkong2 Forum (German) +http://forum.yunkong2.net/viewtopic.php?f=20&t=2076 + +## security note: +It is not recommended to expose this adapter to the public internet. +Some kind of WAF/proxy/entry Server should be put before yunkong2. (e.g. nginx is nice and easy to configure). + +## Changelog +### 0.3.2 (2018-03-07) +* (Apollon77) Fix Authentication + +### 0.3.0 (2017-10-04) +* (Apollon77) BREAKING!!! Make sure 'entry' is really a boolean as defined in object + +### 0.2.0 (2017-06-09) +* (Apollon77) Add missing authentication check +* (Apollon77) Add option to send in data as Message when received over other ways +* (Apollon77) Add option not to start a webserver for cases where data are received using messages + +### 0.1.5 (2016-09-19) +* (soef) support of certificates + +### 0.1.4 (2016-03-29) +* (dschaedl) replaced geofency Icon (on request of bluefox) + +### 0.1.3 (2016-03-29) +* (soef) fixed atHome and atHomeCount state creation + +### 0.1.2 (2016-02-13) +* (soef) Dots in location name will be replaced by an underscore + +### 0.1.1 (2016-02-01) +* (Pmant) Fix config page + +### 0.1.0 (2016-01-26) +* (soef) Fix error with "at home" settings + +### 0.0.4 (2016-01-24) +* (soef) Added some new states + +### 0.0.3 (2016-01-21) +* (soef) Some modifications +* (bluefox) change type + +### 0.0.2 +* (dschaedl) moved to yunkong2/yunkong2.geofency + +### 0.0.1 +* (dschaedl) initial release + +## License + +The MIT License (MIT) + +Copyright (c) 2015 dschaedl + +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/geofency.png b/admin/geofency.png new file mode 100644 index 0000000..aaf838e Binary files /dev/null and b/admin/geofency.png differ diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..76460c6 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + +
+ + + + + + +

Geofency Adapter Settings

+ + + + + + + + +
atHome
activate_server
port
ssl
user
password
+ + +

port

+

geofency is listening for events on this port

+

user / password

+

set username and password for authentication of your geofenca device. Use the same values in your mobile app webhook settings

+

geofency mobile app

+

+ download geofency for your device
+ * see Geofency website
+ * for any new location -> properties -> webhook settings:
+ -> URL for entry & exit: <your yunkong2 Domain>:<port from above>/<any location name >
+ -> Post Format: JSON-encoded: enabled
+ -> authentication: set user / password from above +

+
+ + diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..9587de5 --- /dev/null +++ b/admin/words.js @@ -0,0 +1,8 @@ +systemDictionary = { + "port": {"de": "Port", "en": "Port", "ru": "Порт"}, + "activate_server": {"de": "Server aktivieren", "en": "Activate Server", "ru": "Activate Server"}, + "user": {"de": "Benutzer", "en": "User", "ru": "Пользователь"}, + "password": {"de": "Passwort", "en": "Password", "ru": "Пароль" }, + "atHome": {"de": "Ortsname für Zuhause", "en": "Name of home", "ru": "Имя места для дома"}, + "ssl": {"de": "SSL (https://) verwenden", "en": "Use SLL (https://)", "ru": "Использовать SSL(https://)"} +}; diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..d278435 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,25 @@ +version: 'test-{build}' +environment: + matrix: + - nodejs_version: '4' + - 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 + - npm install + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - npm test +build: 'off' diff --git a/geofency.js b/geofency.js new file mode 100644 index 0000000..de696ba --- /dev/null +++ b/geofency.js @@ -0,0 +1,305 @@ +/** + * + * geofency adapter + * This Adapter is based on the geofency adapter of ccu.io + * + */ + +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +"use strict"; + +var utils = require(__dirname + '/lib/utils'); // Get common adapter utils + +var webServer = null; +var activate_server = false; + +var adapter = utils.Adapter({ + name: 'geofency', + + unload: function (callback) { + try { + adapter.log.info("terminating http" + (webServer.settings.secure ? "s" : "") + " server on port " + webServer.settings.port); + callback(); + } catch (e) { + callback(); + } + }, + ready: function () { + adapter.log.info("Adapter got 'Ready' Signal - initiating Main function..."); + main(); + }, + message: function (msg) { + processMessage(msg); + } +}); + +function main() { + checkCreateNewObjects(); + if (adapter.config.activate_server !== undefined) activate_server = adapter.config.activate_server; + else activate_server = true; + if (activate_server) { + if (adapter.config.ssl) { + // subscribe on changes of permissions + adapter.subscribeForeignObjects('system.group.*'); + adapter.subscribeForeignObjects('system.user.*'); + + if (!adapter.config.certPublic) { + adapter.config.certPublic = 'defaultPublic'; + } + if (!adapter.config.certPrivate) { + adapter.config.certPrivate = 'defaultPrivate'; + } + + // Load certificates + adapter.getForeignObject('system.certificates', function (err, obj) { + if (err || !obj || !obj.native.certificates || !adapter.config.certPublic || !adapter.config.certPrivate || !obj.native.certificates[adapter.config.certPublic] || !obj.native.certificates[adapter.config.certPrivate] + ) { + adapter.log.error('Cannot enable secure web server, because no certificates found: ' + adapter.config.certPublic + ', ' + adapter.config.certPrivate); + } else { + adapter.config.certificates = { + key: obj.native.certificates[adapter.config.certPrivate], + cert: obj.native.certificates[adapter.config.certPublic] + }; + + } + webServer = initWebServer(adapter.config); + }); + } else { + webServer = initWebServer(adapter.config); + } + } +} + +function initWebServer(settings) { + + var server = { + server: null, + settings: settings + }; + + if (settings.port) { + if (settings.ssl) { + if (!adapter.config.certificates) { + return null; + } + } + + if (settings.ssl) { + server.server = require('https').createServer(adapter.config.certificates, requestProcessor); + } else { + server.server = require('http').createServer(requestProcessor); + } + + server.server.__server = server; + } else { + adapter.log.error('port missing'); + process.exit(1); + } + + if (server.server) { + adapter.getPort(settings.port, function (port) { + if (port != settings.port && !adapter.config.findNextPort) { + adapter.log.error('port ' + settings.port + ' already in use'); + process.exit(1); + } + server.server.listen(port); + adapter.log.info('http' + (settings.ssl ? 's' : '') + ' server listening on port ' + port); + }); + } + + if (server.server) { + return server; + } else { + return null; + } +} + +function requestProcessor(req, res) { + var check_user = adapter.config.user; + var check_pass = adapter.config.pass; + + // If they pass in a basic auth credential it'll be in a header called "Authorization" (note NodeJS lowercases the names of headers in its request object) + var auth = req.headers.authorization; // auth is in base64(username:password) so we need to decode the base64 + adapter.log.debug("Authorization Header is: ", auth); + + var username = ''; + var password = ''; + var request_valid = true; + if (auth && check_user.length > 0 && check_pass.length > 0) { + var tmp = auth.split(' '); // Split on a space, the original auth looks like "Basic Y2hhcmxlczoxMjM0NQ==" and we need the 2nd part + var buf = new Buffer(tmp[1], 'base64'); // create a buffer and tell it the data coming in is base64 + var plain_auth = buf.toString(); // read it back out as a string + + adapter.log.debug("Decoded Authorization ", plain_auth); + // At this point plain_auth = "username:password" + var creds = plain_auth.split(':'); // split on a ':' + username = creds[0]; + password = creds[1]; + if ((username != check_user) || (password != check_pass)) { + adapter.log.warn("User credentials invalid"); + request_valid = false; + } + } + /*else { + adapter.log.warn("Authorization Header missing but user/pass defined"); + request_valid = false; + }*/ + if (!request_valid) { + res.statusCode = 403; + res.end(); + return; + } + if (req.method === 'POST') { + var body = ''; + + adapter.log.debug("request path:" + req.path); + + req.on('data', function (chunk) { + body += chunk; + }); + + req.on('end', function () { + var user = req.url.slice(1); + var jbody = JSON.parse(body); + + handleWebhookRequest(user, jbody); + + res.writeHead(200); + res.write("OK"); + res.end(); + }); + } + else { + res.writeHead(500); + res.write("Request error"); + res.end(); + } +} + +function handleWebhookRequest(user, jbody) { + adapter.log.info("adapter geofency received webhook from device " + user + " with values: name: " + jbody.name + ", entry: " + jbody.entry); + var id = user + '.' + jbody.name.replace(/\s|\./g, '_'); + + // create Objects if not yet available + adapter.getObject(id, function (err, obj) { + if (err || !obj) return createObjects(id, jbody); + setStates(id, jbody); + setAtHome(user, jbody); + }); +} + +var lastStateNames = ["lastLeave", "lastEnter"], + stateAtHomeCount = "atHomeCount", + stateAtHome = "atHome"; + + +function setStates(id, jbody) { + adapter.setState(id + '.entry', {val: ((jbody.entry == "1") ? true : false), ack: true}); + + var ts = adapter.formatDate(new Date(jbody.date), "YYYY-MM-DD hh:mm:ss"); + adapter.setState(id + '.date', {val: ts, ack: true}); + adapter.setState(id + '.' + lastStateNames[(jbody.entry == "1") ? 1 : 0], {val: ts, ack: true}); +} + + +function createObjects(id, b) { + // create all Objects + var children = []; + + var obj = { + type: 'state', + //parent: id, + common: {name: 'entry', read: true, write: true, type: 'boolean'}, + native: {id: id} + }; + adapter.setObjectNotExists(id + ".entry", obj); + children.push(obj); + obj = { + type: 'state', + //parent: id, + common: {name: 'date', read: true, write: true, type: 'string'}, + native: {id: id} + }; + adapter.setObjectNotExists(id + ".date", obj); + children.push(obj); + + for (var i = 0; i < 2; i++) { + obj.common.name = lastStateNames[i]; + adapter.setObjectNotExists(id + "." + lastStateNames[i], obj); + children.push(obj); + } + + adapter.setObjectNotExists(id, { + type: 'device', + //children: children, + common: {id: id, name: b.name}, + native: {name: b.name, lat: b.lat, long: b.long, radius: b.radius, device: b.device, beaconUUID: b.beaconUUID, major: b.major, minor: b.minor} + }, function (err, obj) { + if (!err && obj) setStates(id, b); + }); +} + + +function setAtHome(userName, body) { + if (body.name.toLowerCase() !== adapter.config.atHome.toLowerCase()) return; + var atHomeCount, atHome; + adapter.getState(stateAtHomeCount, function (err, obj) { + if (err) return; + atHomeCount = obj ? obj.val : 0; + adapter.getState(stateAtHome, function (err, obj) { + if (err) return; + atHome = obj ? (obj.val ? JSON.parse(obj.val) : []) : []; + var idx = atHome.indexOf(userName); + if (body.entry === '1') { + if (idx < 0) { + atHome.push(userName); + adapter.setState(stateAtHome, JSON.stringify(atHome), true); + } + } else { + if (idx >= 0) { + atHome.splice(idx, 1); + adapter.setState(stateAtHome, JSON.stringify(atHome), true); + } + } + if (atHomeCount !== atHome.length) adapter.setState(stateAtHomeCount, atHome.length, true); + }); + }); +} + + +function createAndSetObject(id, obj) { + adapter.setObjectNotExists(id, obj, function (err) { + adapter.setState(id, 0, true); + }); + +} + +function checkCreateNewObjects() { + + function doIt() { + var fs = require('fs'), + io = fs.readFileSync(__dirname + "/io-package.json"), + objs = JSON.parse(io); + + for (var i = 0; i < objs.instanceObjects.length; i++) { + createAndSetObject(objs.instanceObjects[i]._id, objs.instanceObjects[i]); + } + } + + var timer = setTimeout(doIt, 2000); + adapter.getState(stateAtHome, function (err, obj) { + clearTimeout(timer); + if (!obj) { + doIt(); + } + }); +} + +function processMessage(message) { + if (!message || !message.message.user || !message.message.data) return; + + adapter.log.info('Message received = ' + JSON.stringify(message)); + + handleWebhookRequest(message.message.user, message.message.data); +} diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..449ac82 --- /dev/null +++ b/io-package.json @@ -0,0 +1,92 @@ +{ + "common": { + "name": "geofency", + "version": "0.3.2", + "news": { + "0.3.2": { + "en": "Fix Authentication", + "de": "Fehler bei Zugangsprüfung behoben", + "ru": "Fix Authentication" + }, + "0.3.0": { + "en": "Make sure 'entry' is really a boolean as defined in object", + "de": "Sicherstellen das 'entry' wirklich ein Boolean ist wie im Objekt definiert.", + "ru": "Make sure 'entry' is really a boolean as defined in object" + }, + "0.2.0": { + "en": "Add Message handling to process webhook data received from other sources then own webserver", + "de": "Behandling von Messages hinzugefügt um Webhook-Daten aus anderen Quellen als dem eigenen Webserver zu verarbeiten", + "ru": "Add Message handling to process webhook data received from other sources then own webserver" + }, + "0.1.6": { + "en": "Catch parse errors", + "de": "Bearbeite Parsefehler", + "ru": "Обработка ошибок парсинга" + } + }, + "title": "Geofency Adapter", + "desc": "listening for geofency events. Based on the location based mobile App (Geofency)", + "platform": "Javascript/Node.js", + "mode": "daemon", + "icon": "geofency.png", + "extIcon": "https://git.spacen.net/yunkong2/yunkong2.geofency/raw/master/admin/geofency.png", + "readme": "https://git.spacen.net/yunkong2/yunkong2.geofency", + "license": "MIT", + "npmLibs": [], + "type": "geoposition", + "keywords": [ + "yunkong2", + "server", + "geofency", + "mobile app" + ], + "loglevel": "info", + "enabled": true, + "localLink": "http://%ip%:%port%", + "messagebox": true, + "subscribe": "messagebox", + "authors": [ + "Daniel Schaedler ", + "Apollon77 " + ] + }, + "native": { + "activate_server": true, + "port": 7999, + "ssl": false, + "user": "geo", + "pass": "geo", + "atHome": "Home" + }, + "objects": [], + "instanceObjects": [ + { + "_id": "atHome", + "type": "state", + "common": { + "name": "atHome", + "type": "string", + "role": "state", + "read": true, + "write": false, + "def": 0, + "desc": "Present persons" + }, + "native": {} + }, + { + "_id": "atHomeCount", + "type": "state", + "common": { + "name": "atHomeCount", + "type": "number", + "role": "state", + "read": true, + "write": false, + "def": [], + "desc": "Number of present persons" + }, + "native": {} + } + ] +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..6af37ff --- /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 == null) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/package.json b/package.json new file mode 100644 index 0000000..e5dc04c --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "yunkong2.geofency", + "version": "0.3.2", + "description": "geofency adapter for yunkong2", + "author": "Daniel Schaedler ", + "contributors": [ + "Apollon77 " + ], + "homepage": "", + "license": "MIT", + "keywords": [ + "yunkong2", + "geofency", + "home automation" + ], + "repository": { + "type": "git", + "url": "https://git.spacen.net/yunkong2/yunkong2.geofency" + }, + "dependencies": { + "request": "^2.67.0" + }, + "devDependencies": { + "grunt": "^1.0.1", + "grunt-replace": "^1.0.1", + "grunt-contrib-jshint": "^1.1.0", + "grunt-jscs": "^3.0.1", + "grunt-http": "^2.2.0", + "mocha": "^4.1.0", + "chai": "^4.1.2" + }, + "bugs": { + "url": "https://git.spacen.net/yunkong2/yunkong2.geofency/issues" + }, + "main": "geofency.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + } +} diff --git a/tasks/jscs.js b/tasks/jscs.js new file mode 100644 index 0000000..12357a4 --- /dev/null +++ b/tasks/jscs.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + all: { + src: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ], + options: require('./jscsRules.js') + } +}; \ No newline at end of file diff --git a/tasks/jscsRules.js b/tasks/jscsRules.js new file mode 100644 index 0000000..89ed888 --- /dev/null +++ b/tasks/jscsRules.js @@ -0,0 +1,36 @@ +module.exports = { + force: true, + "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], /*"if",*/ + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "disallowSpacesInFunctionDeclaration": {"beforeOpeningRoundBrace": true}, + "disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true}, + "requireSpacesInFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInAnonymousFunctionExpression": {"beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true}, + "requireSpacesInNamedFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInFunctionDeclaration": {"beforeOpeningCurlyBrace": true}, + "disallowMultipleVarDecl": true, + "requireBlocksOnNewline": true, + "disallowEmptyBlocks": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpacesInsideParentheses": true, + "requireCommaBeforeLineBreak": true, + //"requireAlignedObjectValues": "all", + "requireOperatorBeforeLineBreak": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "disallowLeftStickedOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "requireRightStickedOperators": ["!"], +// "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + //"disallowSpaceAfterBinaryOperators": [","], + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "requireSpaceAfterBinaryOperators": ["?", ">", ",", ">=", "<=", "<", "+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + //"validateIndentation": 4, + //"validateQuoteMarks": { "mark": "\"", "escape": true }, + "disallowMixedSpacesAndTabs": true, + "disallowKeywordsOnNewLine": ["else", "catch"] + +}; diff --git a/tasks/jshint.js b/tasks/jshint.js new file mode 100644 index 0000000..c54e5c3 --- /dev/null +++ b/tasks/jshint.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + options: { + force: true + }, + all: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ] +}; \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..e2a1680 --- /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://git.spacen.net/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/yunkong2-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/testAdapter.js b/test/testAdapter.js new file mode 100644 index 0000000..ae9c289 --- /dev/null +++ b/test/testAdapter.js @@ -0,0 +1,140 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + console.log('Try check #' + counter); + if (counter > 30) { + if (cb) cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error(err); + if (state && state.val) { + if (cb) cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + if (cb) cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error(err); + if (value === null && !state) { + if (cb) cb(); + } else + if (state && (value === undefined || state.val === value)) { + if (cb) cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test ' + adapterShortName + ' adapter', function() { + before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + //config.native.dbtype = 'sqlite'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + +/* + ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY +*/ + it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + done(); + }); + }); + }); +/**/ + +/* + PUT YOUR OWN TESTS HERE USING + it('Testname', function ( done) { + ... + }); + + You can also use "sendTo" method to send messages to the started adapter +*/ + + after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..d0759c0 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,91 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + console.log(); + + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('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; + } + } + + var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +});