Initial commit

This commit is contained in:
2018-09-21 23:46:12 +08:00
commit d1436af18d
20 changed files with 2166 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/node_modules
/.idea
/tmp

10
.npmignore Normal file
View File

@@ -0,0 +1,10 @@
Gruntfile.js
tasks
node_modules
.idea
.git
/node_modules
test
tmp
.travis.yml
appveyor.yml

23
.travis.yml Normal file
View File

@@ -0,0 +1,23 @@
os:
- linux
- osx
language: node_js
node_js:
- '4'
- '6'
- '8'
- '10'
before_script:
- export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1)
- 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi'
- npm -v
- npm install winston@2.3.1
- 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production'
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

222
Gruntfile.js Normal file
View File

@@ -0,0 +1,222 @@
// 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 pkg = grunt.file.readJSON('package.json');
var adaptName = pkg.name.substring('iobroker.'.length);
var iopackage = grunt.file.readJSON('io-package.json');
var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
var newname = grunt.option('name');
var author = grunt.option('author') || '@@Author@@';
var email = grunt.option('email') || '@@email@@';
var fs = require('fs');
// check arguments
if (process.argv[2] == 'rename') {
console.log('Try to rename to "' + newname + '"');
if (!newname) {
console.log('Please write the new template name, like: "grunt rename --name=mywidgetset" --author="Author Name"');
process.exit();
}
if (newname.indexOf(' ') != -1) {
console.log('Name may not have space in it.');
process.exit();
}
if (newname.toLowerCase() != newname) {
console.log('Name must be lower case.');
process.exit();
}
if (fs.existsSync(__dirname + '/admin/template-rest.png')) {
fs.renameSync(__dirname + '/admin/template-rest.png', __dirname + '/admin/' + newname + '.png');
}
if (fs.existsSync(__dirname + '/widgets/template-rest.html')) {
fs.renameSync(__dirname + '/widgets/template-rest.html', __dirname + '/widgets/' + newname + '.html');
}
if (fs.existsSync(__dirname + '/widgets/template/js/template-rest.js')) {
fs.renameSync(__dirname + '/widgets/template/js/template-rest.js', __dirname + '/widgets/template/js/' + newname + '.js');
}
if (fs.existsSync(__dirname + '/widgets/template-rest')) {
fs.renameSync(__dirname + '/widgets/template-rest', __dirname + '/widgets/' + newname);
}
}
// Project configuration.
grunt.initConfig({
pkg: pkg,
replace: {
version: {
options: {
patterns: [
{
match: /version: *"[\.0-9]*"/,
replacement: 'version: "' + version + '"'
},
{
match: /"version": *"[\.0-9]*",/g,
replacement: '"version": "' + version + '",'
},
{
match: /version: *"[\.0-9]*",/g,
replacement: 'version: "' + version + '",'
}
]
},
files: [
{
expand: true,
flatten: true,
src: [
srcDir + 'package.json',
srcDir + 'io-package.json'
],
dest: srcDir
},
{
expand: true,
flatten: true,
src: [
srcDir + 'widgets/' + adaptName + '.html'
],
dest: srcDir + 'widgets'
},
{
expand: true,
flatten: true,
src: [
srcDir + 'widgets/' + adaptName + '/js/' + adaptName + '.js'
],
dest: srcDir + 'widgets/' + adaptName + '/js/'
}
]
},
name: {
options: {
patterns: [
{
match: /template\-rest/g,
replacement: newname
},
{
match: /Template\-rest/g,
replacement: newname ? (newname[0].toUpperCase() + newname.substring(1)) : 'Template-rest'
},
{
match: /@@Author@@/g,
replacement: author
},
{
match: /@@email@@/g,
replacement: email
}
]
},
files: [
{
expand: true,
flatten: true,
src: [
srcDir + 'io-package.json',
srcDir + 'LICENSE',
srcDir + 'package.json',
srcDir + 'README.md',
srcDir + 'io-package.json',
srcDir + 'main.js',
srcDir + 'Gruntfile.js'
],
dest: srcDir
},
{
expand: true,
flatten: true,
src: [
srcDir + 'widgets/' + newname +'.html'
],
dest: srcDir + 'widgets'
},
{
expand: true,
flatten: true,
src: [
srcDir + 'admin/index.html'
],
dest: srcDir + 'admin'
},
{
expand: true,
flatten: true,
src: [
srcDir + 'widgets/' + newname + '/js/' + newname +'.js'
],
dest: srcDir + 'widgets/' + newname + '/js'
},
{
expand: true,
flatten: true,
src: [
srcDir + 'widgets/' + newname + '/css/*.css'
],
dest: srcDir + 'widgets/' + newname + '/css'
}
]
}
},
// 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/ioBroker/ioBroker.js-controller/master/tasks/jscs.js'
},
dest: 'tasks/jscs.js'
},
get_jshint: {
options: {
url: 'https://raw.githubusercontent.com/ioBroker/ioBroker.js-controller/master/tasks/jshint.js'
},
dest: 'tasks/jshint.js'
},
get_jscsRules: {
options: {
url: 'https://raw.githubusercontent.com/ioBroker/ioBroker.js-controller/master/tasks/jscsRules.js'
},
dest: 'tasks/jscsRules.js'
}
}
});
grunt.loadNpmTasks('grunt-replace');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-jscs');
grunt.loadNpmTasks('grunt-http');
grunt.registerTask('default', [
'http',
'replace:version',
'jshint',
'jscs'
]);
grunt.registerTask('prepublish', [
'http',
'replace:version'
]);
grunt.registerTask('p', [
'http',
'replace:version'
]);
grunt.registerTask('rename', [
'replace:name'
]);
};

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 '@@Author@@' <'@@email@@'>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

90
README.md Normal file
View File

@@ -0,0 +1,90 @@
![Logo](admin/template-rest.png)
# ioBroker.template-rest
=================
This adapter is a template for the creation of an ioBroker adapter. You do not need it, at least that you plan developing your own adapter.
This is extension of [ioBroker.template](https://github.com/ioBroker/ioBroker.template) adapter with REST service.
By default the WEB server will be started to serve HTTP GET/POST requests. It can be started as secure(https) or unsecure(http) and with authentication or without.
Additionally is shown how to poll some other URL for JSON data and parse them.
##Steps
1. download and unpack this packet from github ```https://github.com/ioBroker/ioBroker.template/archive/master.zip```
or clone git repository ```git clone https://github.com/ioBroker/ioBroker.template.git```
2. download required npm packets. Write in ioBroker.template directory:
```npm install```
3. set name of this template. Call
```grunt rename --name=mynewname --email=email@mail.com --author="Author Name"```
*mynewname* must be **lower** case and with no spaces.
If grunt is not available, install grunt globally:
```npm install -g grunt-cli```
4. rename directory from *ioBroker.template* (can be *ioBroker.template-master*) to *iobroker.mynewname*
5. to use this template you should copy it into *.../iobroker/node_modules* directory and then create an instance for it with iobroker.admin
6. create your adapter:
* you might want to start with main.js (code running within iobroker) and admin/index.html (the adapter settings page).
* [Adapter-Development-Documentation](https://github.com/ioBroker/ioBroker/wiki/Adapter-Development-Documentation),
* [Installation, setup and first steps with an ioBroker Development Environment](https://github.com/ioBroker/ioBroker/wiki/Installation,-setup-and-first-steps-with-an-ioBroker-Development-Environment)
* [Write and debug vis widgets](https://github.com/ioBroker/ioBroker/wiki/How-to-debug-vis-and-to-write-own-widget-set)
* files under the www folders are made available under http://&lt;iobrokerIP&gt;:8082/&lt;adapter-name&gt;/
* for this to work the iobroker.vis adapter has to be installed
* delete this folder if you do not plan to export any files this way
* call ```iobroker upload <adapter-name>``` after you change files in the www folder to get the new files uploaded to vis
* the widget folder contains an example of a vis widget
* you might want to start with *widget/<adapter-name>.html* and *widget/js/<adapter-name>.js*
* call ```iobroker visdebug <adapter-name>``` to enable debugging and upload widget to "vis". (This works only from V0.7.15 of js-controller)
* If you do not plan to export any widget then delete the whole widget folder and remove the ```"restartAdapters": ["vis"]``` statement from *io-package.json*
* After admin/index.html is changed you must execute ```iobroker upload mynewname``` to see changes in admin console. The same is valid for any files in *admin* and *www* directory
7. change version: edit package.json and then call ```grunt p``` in your adapter directory.
8. share it with the community
## Usage
After the adapter is started, you can check following requests:
http://ip:9090/api/system.adapter.admin.0.memHeapTotal
http://ip:9090/api/plain/system.adapter.admin.0.memHeapTotal
## Changelog
#### 0.1.0
* (bluefox) initial release
## License
The MIT License (MIT)
Copyright (c) 2017 @@Author@@ <@@email@@>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

212
admin/index.html Normal file
View File

@@ -0,0 +1,212 @@
<html>
<!-- these 4 files always have to be included -->
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
<!-- optional: use jqGrid
<link rel="stylesheet" type="text/css" href="../../lib/css/jqGrid/ui.jqgrid-4.5.4.css"/>
<script type="text/javascript" src="../../lib/js/jqGrid/jquery.jqGrid-4.5.4.min.js"></script>
<script type="text/javascript" src="../../lib/js/jqGrid/i18n/grid.locale-all.js"></script>
-->
<!-- optional: use multiselect
<link rel="stylesheet" type="text/css" href="../../lib/css/jquery.multiselect-1.13.css"/>
<script type="text/javascript" src="../../lib/js/jquery.multiselect-1.13.min.js"></script>
-->
<!-- these two file always have to be included -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<!-- you have to define 2 functions in the global scope: -->
<script type="text/javascript">
// Dictionary (systemDictionary is global variable from adapter-settings.js)
systemDictionary = {
"template-rest adapter settings": {"de": "Beispiel", "ru": "Пример"},
"test1": {"en": "Test 1", "de": "Test 1", "ru": "Тест 1"},
"test2": {"en": "Test 2", "de": "Test 2", "ru": "Тест 2"},
"Run as:": {"de": "Laufen unter Anwender:", "ru": "Запустить от пользователя:"},
"IP:": {"de": "IP:", "ru": "IP:"},
"Port:": {"de": "Port:", "ru": "Порт:"},
"Secure(HTTPS):": {"de": "Verschlüsselung(HTTPS):", "ru": "Шифрование(HTTPS):"},
"Authentication:": {"de": "Authentifikation:", "ru": "Аутентификация:"},
"Listen on all IPs": {"en": "Listen on all IPs", "de": "An allen IP Adressen hören", "ru": "Открыть сокет на всех IP адресах"},
"help_tip": {
"en": "On save the adapter restarts with new configuration immediately",
"de": "Beim Speichern von Einstellungen der Adapter wird sofort neu gestartet.",
"ru": "Сразу после сохранения настроек драйвер перезапуститься с новыми значениями"
},
"Public certificate:": {"en": "Public certificate:", "de": "Publikzertifikat:", "ru": "'Public' сертификат:"},
"Private certificate:": {"en": "Private certificate:", "de": "Privatzertifikat:", "ru": "'Private' сертификат:"},
"Chained certificate:": {"en": "Chained certificate:", "de": "Kettenzertifikat:", "ru": "'Chained' сертификат:"},
"Let's Encrypt settings": {
"en": "Let's Encrypt settings",
"de": "Einstellungen Let's Encrypt",
"ru": "Настройкт Let's Encrypt"
},
"Use Lets Encrypt certificates:": {
"en": "Use Let's Encrypt certificates:",
"de": "Benutzen Let's Encrypt Zertifikate:",
"ru": "Использовать сертификаты Let's Encrypt:"
},
"Use this instance for automatic update:": {
"en": "Use this instance for automatic update:",
"de": "Benutze diese Instanz für automatische Updates:",
"ru": "Обновлять сертификаты в этом драйвере:"
},
"Port to check the domain:": {
"en": "Port to check the domain:",
"de": "Port um die Domain zu prüfen:",
"ru": "Порт для проверки доменного имени:"
}
};
function showHideSettings() {
if ($('#secure').prop('checked')) {
$('#_certPublic').show();
$('#_certPrivate').show();
$('#_certChained').show();
$('.le-settings').show();
if ($('#leEnabled').prop('checked')) {
$('.le-sub-settings').show();
if ($('#leUpdate').prop('checked')) {
$('.le-sub-settings-update').show();
} else {
$('.le-sub-settings-update').hide();
}
} else {
$('.le-sub-settings').hide();
}
} else {
$('#_certPublic').hide();
$('#_certPrivate').hide();
$('#_certChained').hide();
$('#auth').prop('checked', false);
$('.le-settings').hide();
}
if ($('#auth').prop('checked')) {
$('#secure').prop('checked', true);
$('#defaultUser').val('admin');
$('.defaultUser').hide();
$('#_ttl').show();
} else {
$('.defaultUser').show();
$('#_ttl').hide();
}
}
// the function loadSettings has to exist ...
function load(settings, onChange) {
if (!settings) return;
// this functions are loaded from library
getIPs(function(ips) {
for (var i = 0; i < ips.length; i++) {
$('#bind').append('<option value="' + ips[i].address + '">' + ips[i].name + '</option>');
}
$('#bind.value').val(settings.bind);
});
// example: select elements with id=key and class=value and insert value
$('.value').each(function () {
var key = $(this).attr('id');
// example: select elements with id=key and class=value and insert value
if ($('#' + key + '.value').attr('type') === 'checkbox') {
$('#' + key + '.value').prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$('#' + key + '.value').val(settings[key]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
// Signal to admin, that no changes yet
onChange(false);
// this functions are loaded from library
fillSelectCertificates('#certPublic', 'public', settings.certPublic);
fillSelectCertificates('#certPrivate', 'private', settings.certPrivate);
fillSelectCertificates('#certChained', 'chained', settings.certChained);
fillUsers('#defaultUser', settings.defaultUser);
$('#auth').change(function () {
if ($(this).prop('checked')) $('#secure').prop('checked', true);
showHideSettings();
});
$('#secure').change(function () {
showHideSettings();
});
showHideSettings();
}
// ... and the function save has to exist.
// you have to make sure the callback is called with the settings object as first param!
function save(callback) {
// example: select elements with class=value and build settings object
var obj = {};
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
callback(obj);
}
</script>
<!-- you have to put your config page in a div with id adapter-container -->
<div id="adapter-container">
<table><tr>
<td><img src="template-rest.png"/></td>
<td><h3 class="translate">template-rest adapter settings</h3></td>
</tr></table>
<table>
<tr><td class="translate">test1</td><td> <input class="value" id="test1"/></td></tr>
<tr><td class="translate">test2</td><td> <input class="value" id="test2"/></td></tr>
<tr><td colspan="2"><h4 class="translate">Server settings</h4></td></tr>
<tr><td class="translate">IP:</td><td> <select class="value" id="bind"></select></td></tr>
<tr><td class="translate">Port:</td><td> <input class="value" id="port" size="5" maxlength="5"/></td></tr>
<tr><td class="translate">Secure(HTTPS):</td><td> <input class="value" id="secure" type="checkbox" /></td></tr>
<tr><td class="translate">Authentication:</td><td><input class="value" id="auth" type="checkbox" /></td></tr>
<tr id="_certPublic">
<td class="translate">Public certificate:</td>
<td><select id="certPublic" class="value"></select></td>
</tr>
<tr id="_certPrivate">
<td class="translate">Private certificate:</td>
<td><select id="certPrivate" class="value"></select></td>
</tr>
<tr id="_certChained">
<td class="translate">Chained certificate:</td>
<td><select id="certChained" class="value"></select></td>
</tr>
<tr class="defaultUser"><td class="translate">Run as:</td><td><select class="value" id="defaultUser" /></td></tr>
<tr><td colspan="2"><h4 class="translate">Polling settings</h4></td></tr>
<tr><td class="translate">URL:</td><td> <input class="value" id="pollURL"/></td></tr>
<tr><td class="translate">Interval:</td><td> <input class="value" id="interval" size="7" maxlength="7"/></td></tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr class="le-settings"><td colspan="2"><h3 class="translate">Let's Encrypt settings</h3></tr>
<tr class="le-settings"><td><label for="leEnabled" class="translate">Use Lets Encrypt certificates:</label></td><td><input class="value" id="leEnabled" type="checkbox" /></td></tr>
<tr class="le-settings le-sub-settings"><td><label for="leUpdate" class="translate">Use this instance for automatic update:</label></td><td><input class="value" id="leUpdate" type="checkbox" /></td></tr>
<tr class="le-settings le-sub-settings le-sub-settings-update"><td><label for="lePort" class="translate">Port to check the domain:</label></td><td><input class="value number" id="lePort" type="number" size="5" maxlength="5" /></td></tr>
</table>
</div>
</html>

BIN
admin/template-rest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

25
appveyor.yml Normal file
View File

@@ -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://github.com/ioBroker/ioBroker.js-controller/tarball/master --production'
test_script:
- echo %cd%
- node --version
- npm --version
- npm test
build: 'off'

65
io-package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"common": {
"name": "template-rest",
"version": "0.1.0",
"title": "Javascript/Node.js based template-rest adapter",
"desc": {
"en": "ioBroker template-rest Adapter",
"de": "ioBroker template-rest Adapter",
"ru": "ioBroker template-rest драйвер как образец"
},
"news": {
"0.1.0" :{
"en": "initial checkin",
"de": "initial checkin",
"ru": "первая версия"
},
"0.0.9" :{
"en": "previous version",
"de": "vorherige Version",
"ru": "предыдущая версия"
},
"0.0.8" :{
"en": "older version (up to 5 versions)",
"de": "noch vorherige Version (bis zu 5 Versionen)",
"ru": "более предыдущая версия (до 5ти версий)"
}
},
"authors": [
"my Name <my@email.com>"
],
"platform": "Javascript/Node.js",
"mode": "daemon",
"icon": "template-rest.png",
"enabled": true,
"extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.template-rest/master/admin/template-rest.png",
"keywords": ["template-rest", "REST"],
"readme": "https://github.com/ioBroker/ioBroker.template-rest/blob/master/README.md",
"loglevel": "info",
"type": "communication",
"dependencies": [{"js-controller": ">=0.12.0"}]
},
"native": {
"test1": true,
"test2": 42,
"port": 9090,
"auth": false,
"secure": false,
"bind": "0.0.0.0",
"defaultUser": "admin",
"certPublic": "defaultPublic",
"certPrivate": "defaultPrivate",
"certChained": "",
"pollURL": "",
"interval": 30000,
"leEnabled": false,
"leUpdate": false,
"leCheckPort": 80
},
"objects": [
]
}

83
lib/utils.js Normal file
View File

@@ -0,0 +1,83 @@
'use strict';
const fs = require('fs');
const path = require('path');
let controllerDir;
let appName;
/**
* returns application name
*
* The name of the application can be different and this function finds it out.
*
* @returns {string}
*/
function getAppName() {
const parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 2].split('.')[0];
}
/**
* looks for js-controller home folder
*
* @param {boolean} isInstall
* @returns {string}
*/
function getControllerDir(isInstall) {
// Find the js-controller location
const possibilities = [
'iobroker.js-controller',
'ioBroker.js-controller',
];
/** @type {string} */
let controllerPath;
for (const pkg of possibilities) {
try {
const possiblePath = require.resolve(pkg);
if (fs.existsSync(possiblePath)) {
controllerPath = possiblePath;
break;
}
} catch (e) { /* not found */ }
}
if (controllerPath == null) {
if (!isInstall) {
console.log('Cannot find js-controller');
process.exit(10);
} else {
process.exit();
}
}
// we found the controller
return path.dirname(controllerPath);
}
/**
* reads controller base settings
*
* @alias getConfig
* @returns {object}
*/
function getConfig() {
let configPath;
if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', appName + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else {
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
}
}
appName = getAppName();
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
const adapter = require(path.join(controllerDir, 'lib/adapter.js'));
exports.controllerDir = controllerDir;
exports.getConfig = getConfig;
exports.Adapter = adapter;
exports.appName = appName;

318
main.js Normal file
View File

@@ -0,0 +1,318 @@
/**
*
* template adapter
*
*
* file io-package.json comments:
*
* {
* "common": {
* "name": "template", // name has to be set and has to be equal to adapters folder name and main file name excluding extension
* "version": "0.0.0", // use "Semantic Versioning"! see http://semver.org/
* "title": "Node.js template Adapter", // Adapter title shown in User Interfaces
* "authors": [ // Array of authord
* "name <mail@template.com>"
* ]
* "desc": "template adapter", // Adapter description shown in User Interfaces. Can be a language object {de:"...",ru:"..."} or a string
* "platform": "Javascript/Node.js", // possible values "javascript", "javascript/Node.js" - more coming
* "mode": "daemon", // possible values "daemon", "schedule", "subscribe"
* "schedule": "0 0 * * *" // cron-style schedule. Only needed if mode=schedule
* "loglevel": "info" // Adapters Log Level
* },
* "native": { // the native object is available via adapter.config in your adapters code - use it for configuration
* "test1": true,
* "test2": 42,
*
* "port": 9090, // port where the REST service will be started. It should has name "port", because it can be changed from console.
* "auth": false, // if basic authentication is required. If enabled, the secure connection should be used.
* "secure": false, // If SSL communication must be used. Should has the name "secure", because it can be changed from console.
* "bind": "0.0.0.0", // Specific address, where the server will be started. "0.0.0.0" means that it will be available for all addresses. Should has the name "bind", because it can be changed from console.
* "defaultUser": "admin", // If authentication is disabled, here can be specified as which user the requests will be done to iobroker DB.
* "certPublic": "defaultPublic",// Required for SSL communication: name of some public certificate from iobroker DB
* "certPrivate": "defaultPrivate"// Required for SSL communication: name of some private key from iobroker DB
*
* "pollURL": "", // Request all 30 seconds the JSON from this URL, parse it and store in ioBroker
* "interval": 30000 // polling interval
* }
* }
*
* You can read here, how REST API could be implementer: https://scotch.io/tutorials/build-a-restful-api-using-node-and-express-4
*/
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
// you have to require the utils module and call adapter function
var utils = require(__dirname + '/lib/utils'); // Get common adapter utils
// load additional libraries
var express = require('express'); // call express
var request = null; // will be initialized later if polling enabled
// you have to call the adapter function and pass a options object
// name has to be set and has to be equal to adapters folder name and main file name excluding extension
// adapter will be restarted automatically every time as the configuration changed, e.g system.adapter.template.0
var adapter = utils.Adapter('template-rest');
var LE = require(utils.controllerDir + '/lib/letsencrypt.js');
// REST server
var webServer = null;
var app = null;
var router = null;
var timer = null;
// is called when adapter shuts down - callback has to be called under any circumstances!
adapter.on('unload', function (callback) {
try {
adapter.log.info('cleaned everything up...');
if (webServer) {
webServer.close();
webServer = null;
}
if (timer) clearInterval(timer);
callback();
} catch (e) {
callback();
}
});
// is called if a subscribed object changes
adapter.on('objectChange', function (id, obj) {
// Warning, obj can be null if it was deleted
adapter.log.info('objectChange ' + id + ' ' + JSON.stringify(obj));
});
// is called if a subscribed state changes
adapter.on('stateChange', function (id, state) {
// Warning, state can be null if it was deleted
adapter.log.info('stateChange ' + id + ' ' + JSON.stringify(state));
// you can use the ack flag to detect if it is status (true) or command (false)
if (state && !state.ack) {
adapter.log.info('ack is not set!');
}
});
// Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ...
adapter.on('message', function (obj) {
if (typeof obj == 'object' && obj.message) {
if (obj.command == 'send') {
// e.g. send email or pushover or whatever
console.log('send command');
// Send response in callback if required
if (obj.callback) adapter.sendTo(obj.from, obj.command, 'Message received', obj.callback);
}
}
});
// is called when databases are connected and adapter received configuration.
// start here!
adapter.on('ready', function () {
main();
});
function addRoutes(_router) {
// test route to make sure everything is working (accessed at GET http://localhost:9090/api)
_router.get('/', function (req, res) {
res.json({message: 'Welcome to our JSON REST api!'});
});
_router.get('/plain/', function(req, res) {
res.send('Welcome to our text REST api!');
});
// on routes that end in /plain/:id
// ----------------------------------------------------
_router.route('/plain/:id')
// get the bear with that id (accessed at GET http://localhost:8080/api/plain/:id)
.get(function (req, res) {
adapter.getForeignState(req.params.id, {user: req.user}, function (err, state) {
if (err) {
res.status(500).send(err);
} else if (!state) {
res.status(500).send('not found');
} else {
res.send('Value: ' + state.val);
}
});
})// create a handler for post (accessed at POST http://localhost:8080/api/bears)
.post(function (req, res) {
adapter.setForeignState(req.params.id, req.body, {user: req.user}, function (err, state) {
if (err) {
res.status(500).send(err);
} else if (!state) {
res.status(500).send('not found');
} else {
res.send('Value used');
}
});
});
// handler for get over JSON
_router.route('/:id')
// get the bear with that id (accessed at GET http://localhost:8080/api/plain/:id)
.get(function (req, res) {
adapter.getForeignState(req.params.id, {user: req.user}, function (err, state) {
if (err) {
res.status(500).send({error: err});
} else {
res.json(state);
}
});
});
}
function initWebServer(settings) {
app = express();
router = express.Router();
// install authentication
app.get('/', function (req, res) {
if (settings.auth) {
var b64auth = (req.headers.authorization || '').split(' ')[1] || '';
var loginPass = new Buffer(b64auth, 'base64').toString().split(':');
var login = loginPass[0];
var password = loginPass[1];
// Check in ioBroker user and password
adapter.checkPassword(login, password, function (result) {
if (!result) {
adapter.log.error('Wrong user or password: ' + login);
res.set('WWW-Authenticate', 'Basic realm="nope"');
res.status(401).send('You shall not pass.');
} else {
req.user = login;
}
});
} else {
req.user = settings.defaultUser;
}
});
// add route cases
addRoutes(router);
// REGISTER OUR ROUTES -------------------------------
// all of our routes will be prefixed with /api
app.use('/api', router);
if (settings.port) {
if (settings.secure) {
if (!adapter.config.certificates) {
adapter.log.error('certificates missing');
return null;
}
}
webServer = LE.createServer(app, adapter.config, adapter.config.certificates, adapter.config.leConfig, adapter.log);
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);
}
webServer.listen(port, settings.bind, function() {
adapter.log.info('Server listening on http' + (settings.secure ? 's' : '') + '://' + settings.bind + ':' + port);
});
});
} else {
adapter.log.error('port missing');
process.exit(1);
}
}
function pollData() {
// you can read about module "request" here: https://www.npmjs.com/package/request
request = request || require('request'); // load library
request(adapter.config.pollURL, function (error, response, body) {
if (error || response.statusCode !== 200) {
adapter.log.error(error || response.statusCode);
} else {
// try to parse answer
try {
var data = JSON.parse(body);
// do something with data
adapter.log.info(JSON.parse(data));
} catch (e) {
adapter.log.error('Cannot parse answer');
}
}
});
}
function main() {
// The adapters config (in the instance object everything under the attribute "native") is accessible via
// adapter.config:
adapter.log.info('config test1: ' + adapter.config.test1);
adapter.log.info('config test1: ' + adapter.config.test2);
/**
*
* For every state in the system there has to be also an object of type state
*
* Here a simple template for a boolean variable named "testVariable"
*
* Because every adapter instance uses its own unique namespace variable names can't collide with other adapters variables
*
*/
adapter.setObject('testVariable', {
type: 'state',
common: {
name: 'testVariable',
type: 'boolean',
role: 'indicator'
},
native: {}
});
// in this template all states changes inside the adapters namespace are subscribed
adapter.subscribeStates('*');
/**
* setState examples
*
* you will notice that each setState will cause the stateChange event to fire (because of above subscribeStates cmd)
*
*/
// the variable testVariable is set to true as command (ack=false)
adapter.setState('testVariable', true);
// same thing, but the value is flagged "ack"
// ack should be always set to true if the value is received from or acknowledged from the target system
adapter.setState('testVariable', {val: true, ack: true});
// same thing, but the state is deleted after 30s (getState will return null afterwards)
adapter.setState('testVariable', {val: true, ack: true, expire: 30});
// try to load certificates
if (adapter.config.secure) {
// Load certificates
// Load certificates
adapter.getCertificates(function (err, certificates, leConfig) {
adapter.config.certificates = certificates;
adapter.config.leConfig = leConfig;
initWebServer(adapter.config);
});
} else {
initWebServer(adapter.config);
}
// Convert port to number
adapter.config.interval = parseInt(adapter.config.interval, 10);
// If interval and URl are set => poll it every X milliseconds
if (adapter.config.interval && adapter.config.pollURL) {
// initial poll
pollData();
// poll every x milliseconds
timer = setInterval(pollData, adapter.config.interval);
}
}

47
package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "iobroker.template-rest",
"version": "0.1.0",
"description": "ioBroker template-rest Adapter",
"author": {
"name": "@@Author@@",
"email": "@@email@@"
},
"contributors": [
{
"name": "@@Author@@",
"email": "@@email@@"
}
],
"homepage": "https://github.com/ioBroker/ioBroker.template-rest",
"license": "MIT",
"keywords": [
"ioBroker",
"template-rest",
"REST API"
],
"repository": {
"type": "git",
"url": "https://github.com/ioBroker/ioBroker.template-rest"
},
"dependencies": {
"express": "^4.13.4",
"request": "^2.72.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"
},
"main": "main.js",
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit"
},
"bugs": {
"url": "https://github.com/ioBroker/ioBroker.template-rest/issues"
},
"readmeFilename": "README.md"
}

17
tasks/jscs.js Normal file
View File

@@ -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')
}
};

36
tasks/jscsRules.js Normal file
View File

@@ -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"]
};

17
tasks/jshint.js Normal file
View File

@@ -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'
]
};

728
test/lib/setup.js Normal file
View File

@@ -0,0 +1,728 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
// check if tmp directory exists
var fs = require('fs');
var path = require('path');
var child_process = require('child_process');
var rootDir = path.normalize(__dirname + '/../../');
var pkg = require(rootDir + 'package.json');
var debug = typeof v8debug === 'object';
pkg.main = pkg.main || 'main.js';
var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/');
adapterName = adapterName[adapterName.length - 2];
var adapterStarted = false;
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 3].split('.')[0];
}
var appName = getAppName().toLowerCase();
var objects;
var states;
var pid = null;
function copyFileSync(source, target) {
var targetFile = target;
//if target is a directory a new file with the same name will be created
if (fs.existsSync(target)) {
if ( fs.lstatSync( target ).isDirectory() ) {
targetFile = path.join(target, path.basename(source));
}
}
try {
fs.writeFileSync(targetFile, fs.readFileSync(source));
}
catch (err) {
console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)");
}
}
function copyFolderRecursiveSync(source, target, ignore) {
var files = [];
var base = path.basename(source);
if (base === adapterName) {
base = pkg.name;
}
//check if folder needs to be created or integrated
var targetFolder = path.join(target, base);
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
//copy
if (fs.lstatSync(source).isDirectory()) {
files = fs.readdirSync(source);
files.forEach(function (file) {
if (ignore && ignore.indexOf(file) !== -1) {
return;
}
var curSource = path.join(source, file);
var curTarget = path.join(targetFolder, file);
if (fs.lstatSync(curSource).isDirectory()) {
// ignore grunt files
if (file.indexOf('grunt') !== -1) return;
if (file === 'chai') return;
if (file === 'mocha') return;
copyFolderRecursiveSync(curSource, targetFolder, ignore);
} else {
copyFileSync(curSource, curTarget);
}
});
}
}
if (!fs.existsSync(rootDir + 'tmp')) {
fs.mkdirSync(rootDir + 'tmp');
}
function storeOriginalFiles() {
console.log('Store original files...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) {
objects['system.adapter.admin.0'].common.enabled = false;
}
if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) {
objects['system.adapter.admin.1'].common.enabled = false;
}
fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects));
try {
f = fs.readFileSync(dataDir + 'states.json');
fs.writeFileSync(dataDir + 'states.json.original', f);
}
catch (err) {
console.log('no states.json found - ignore');
}
}
function restoreOriginalFiles() {
console.log('restoreOriginalFiles...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json.original');
fs.writeFileSync(dataDir + 'objects.json', f);
try {
f = fs.readFileSync(dataDir + 'states.json.original');
fs.writeFileSync(dataDir + 'states.json', f);
}
catch (err) {
console.log('no states.json.original found - ignore');
}
}
function checkIsAdapterInstalled(cb, counter, customName) {
customName = customName || pkg.name.split('.').pop();
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsAdapterInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.' + customName + '.0']) {
console.log('checkIsAdapterInstalled: ready!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
} else {
console.warn('checkIsAdapterInstalled: still not ready');
}
} catch (err) {
}
if (counter > 20) {
console.error('checkIsAdapterInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsAdapterInstalled: wait...');
setTimeout(function() {
checkIsAdapterInstalled(cb, counter + 1);
}, 1000);
}
}
function checkIsControllerInstalled(cb, counter) {
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsControllerInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0']) {
console.log('checkIsControllerInstalled: installed!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
}
} catch (err) {
}
if (counter > 20) {
console.log('checkIsControllerInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsControllerInstalled: wait...');
setTimeout(function() {
checkIsControllerInstalled(cb, counter + 1);
}, 1000);
}
}
function installAdapter(customName, cb) {
if (typeof customName === 'function') {
cb = customName;
customName = null;
}
customName = customName || pkg.name.split('.').pop();
console.log('Install adapter...');
var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js';
// make first install
if (debug) {
child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
} else {
// add controller
var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
waitForEnd(_pid, function () {
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
});
}
}
function waitForEnd(_pid, cb) {
if (!_pid) {
cb(-1, -1);
return;
}
_pid.on('exit', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
_pid.on('close', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
}
function installJsController(cb) {
console.log('installJsController...');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') ||
!fs.existsSync(rootDir + 'tmp/' + appName + '-data')) {
// try to detect appName.js-controller in node_modules/appName.js-controller
// travis CI installs js-controller into node_modules
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"');
// copy all
// stop controller
console.log('Stop controller if running...');
var _pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js stop', {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// copy all files into
if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp');
if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){
console.log('Copy js-controller...');
copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/');
}
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js setup first --console', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
__pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(__pid, function () {
checkIsControllerInstalled(function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
console.log('Setup finished.');
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
});
} else {
// check if port 9000 is free, else admin adapter will be added to running instance
var client = new require('net').Socket();
client.connect(9000, '127.0.0.1', function() {
console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.');
process.exit(0);
});
setTimeout(function () {
client.destroy();
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => install from git');
child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', {
cwd: rootDir + 'tmp/',
stdio: [0, 1, 2]
});
} else {
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
child_process.exec('node ' + appName + '.js setup first', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
child_process.fork(appName + '.js', ['setup', 'first'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
}
// let npm install admin and run setup
checkIsControllerInstalled(function () {
var _pid;
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
}, 1000);
}
} else {
setTimeout(function () {
console.log('installJsController: js-controller installed');
if (cb) cb(false);
}, 0);
}
}
function copyAdapterToController() {
console.log('Copy adapter...');
// Copy adapter to tmp/node_modules/appName.adapter
copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']);
console.log('Adapter copied.');
}
function clearControllerLog() {
var dirPath = rootDir + 'tmp/log';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear controller log...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Controller log cleared');
} catch (err) {
console.error('cannot clear log: ' + err);
}
}
}
function clearDB() {
var dirPath = rootDir + 'tmp/iobroker-data/sqlite';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear sqlite DB...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Clear sqlite DB');
} catch (err) {
console.error('cannot clear DB: ' + err);
}
}
}
function setupController(cb) {
installJsController(function (isInited) {
clearControllerLog();
clearDB();
if (!isInited) {
restoreOriginalFiles();
copyAdapterToController();
}
// read system.config object
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var objs;
try {
objs = fs.readFileSync(dataDir + 'objects.json');
objs = JSON.parse(objs);
}
catch (e) {
console.log('ERROR reading/parsing system configuration. Ignore');
objs = {'system.config': {}};
}
if (!objs || !objs['system.config']) {
objs = {'system.config': {}};
}
if (cb) cb(objs['system.config']);
});
}
function startAdapter(objects, states, callback) {
if (adapterStarted) {
console.log('Adapter already started ...');
if (callback) callback(objects, states);
return;
}
adapterStarted = true;
console.log('startAdapter...');
if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) {
try {
if (debug) {
// start controller
pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
} else {
// start controller
pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
}
} catch (error) {
console.error(JSON.stringify(error));
}
} else {
console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main);
}
if (callback) callback(objects, states);
}
function startController(isStartAdapter, onObjectChange, onStateChange, callback) {
if (typeof isStartAdapter === 'function') {
callback = onStateChange;
onStateChange = onObjectChange;
onObjectChange = isStartAdapter;
isStartAdapter = true;
}
if (onStateChange === undefined) {
callback = onObjectChange;
onObjectChange = undefined;
}
if (pid) {
console.error('Controller is already started!');
} else {
console.log('startController...');
adapterStarted = false;
var isObjectConnected;
var isStatesConnected;
var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer');
objects = new Objects({
connection: {
"type" : "file",
"host" : "127.0.0.1",
"port" : 19001,
"user" : "",
"pass" : "",
"noFileCache": false,
"connectTimeout": 2000
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.warn(msg);
},
error: function (msg) {
console.error(msg);
}
},
connected: function () {
isObjectConnected = true;
if (isStatesConnected) {
console.log('startController: started!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onObjectChange
});
// Just open in memory DB itself
var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer');
states = new States({
connection: {
type: 'file',
host: '127.0.0.1',
port: 19000,
options: {
auth_pass: null,
retry_max_delay: 15000
}
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.log(msg);
},
error: function (msg) {
console.log(msg);
}
},
connected: function () {
isStatesConnected = true;
if (isObjectConnected) {
console.log('startController: started!!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onStateChange
});
}
}
function stopAdapter(cb) {
if (!pid) {
console.error('Controller is not running!');
if (cb) {
setTimeout(function () {
cb(false);
}, 0);
}
} else {
adapterStarted = false;
pid.on('exit', function (code, signal) {
if (pid) {
console.log('child process terminated due to receipt of signal ' + signal);
if (cb) cb();
pid = null;
}
});
pid.on('close', function (code, signal) {
if (pid) {
if (cb) cb();
pid = null;
}
});
pid.kill('SIGTERM');
}
}
function _stopController() {
if (objects) {
objects.destroy();
objects = null;
}
if (states) {
states.destroy();
states = null;
}
}
function stopController(cb) {
var timeout;
if (objects) {
console.log('Set system.adapter.' + pkg.name + '.0');
objects.setObject('system.adapter.' + pkg.name + '.0', {
common:{
enabled: false
}
});
}
stopAdapter(function () {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
_stopController();
if (cb) {
cb(true);
cb = null;
}
});
timeout = setTimeout(function () {
timeout = null;
console.log('child process NOT terminated');
_stopController();
if (cb) {
cb(false);
cb = null;
}
pid = null;
}, 5000);
}
// Setup the adapter
function setAdapterConfig(common, native, instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
if (common) objects[id].common = common;
if (native) objects[id].native = native;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects));
}
// Read config of the adapter
function getAdapterConfig(instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
return objects[id];
}
if (typeof module !== undefined && module.parent) {
module.exports.getAdapterConfig = getAdapterConfig;
module.exports.setAdapterConfig = setAdapterConfig;
module.exports.startController = startController;
module.exports.stopController = stopController;
module.exports.setupController = setupController;
module.exports.stopAdapter = stopAdapter;
module.exports.startAdapter = startAdapter;
module.exports.installAdapter = installAdapter;
module.exports.appName = appName;
module.exports.adapterName = adapterName;
module.exports.adapterStarted = adapterStarted;
}

77
test/testApi.js Normal file
View File

@@ -0,0 +1,77 @@
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
var request = require('request');
var objects = null;
var states = null;
process.env.NO_PROXY = '127.0.0.1';
describe('Test RESTful API', function() {
before('Test RESTful API: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
var brokerStarted = false;
setup.setupController(function () {
var config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
config.native.port = 18183;
setup.setAdapterConfig(config.common, config.native);
setup.startController(function (_objects, _states) {
objects = _objects;
states = _states;
// give some time to start server
setTimeout(function () {
_done();
}, 5000);
});
});
});
it('Test RESTful API: get /api/plain - must return welcome text', function (done) {
request('http://127.0.0.1:18183/api/plain', function (error, response, body) {
console.log('get /api/plain => ' + body);
expect(error).to.be.not.ok;
expect(body).to.be.equal('Welcome to our text REST api!');
done();
});
});
it('Test RESTful API: get /api - must return welcome text', function (done) {
request('http://127.0.0.1:18183/api', function (error, response, body) {
console.log('get /api => ' + body);
expect(error).to.be.not.ok;
expect(body).to.be.equal('{"message":"Welcome to our JSON REST api!"}');
done();
});
});
it('Test RESTful API: get /api/id - must return value', function (done) {
request('http://127.0.0.1:18183/api/system.adapter.template-rest.0.alive', function (error, response, body) {
console.log('get /api => ' + body);
expect(error).to.be.not.ok;
var data = JSON.parse(body);
expect(data.val).to.be.true;
done();
});
});
it('Test RESTful API: get /api/plain/id - must return value', function (done) {
request('http://127.0.0.1:18183/api/plain/system.adapter.template-rest.0.alive', function (error, response, body) {
console.log('get /api => ' + body);
expect(error).to.be.not.ok;
expect(body).to.be.equal('Value: true');
done();
});
});
after('Test RESTful API: Stop js-controller', function (done) {
this.timeout(6000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
done();
});
});
});

91
test/testPackageFiles.js Normal file
View File

@@ -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 <my@email.com>');
}
}
else {
expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
console.log();
}
expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
console.log('WARNING: titleLang is not existing in io-package.json. Please add');
console.log();
}
if (
ioPackage.common.title.indexOf('iobroker') !== -1 ||
ioPackage.common.title.indexOf('ioBroker') !== -1 ||
ioPackage.common.title.indexOf('adapter') !== -1 ||
ioPackage.common.title.indexOf('Adapter') !== -1
) {
console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
console.log();
}
if (ioPackage.common.name.indexOf('vis-') !== 0) {
if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
console.log('WARNING: Admin3 support is missing! Please add it');
console.log();
}
if (ioPackage.common.materialize) {
expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
}
}
var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
if (fileContentReadme.indexOf('## Changelog') === -1) {
console.log('Warning: The README.md should have a section ## Changelog');
console.log();
}
expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
if (!licenseFileExists) {
console.log('Warning: The License should also exist as LICENSE file');
console.log();
}
if (fileContentReadme.indexOf('## License') === -1) {
console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
console.log();
}
done();
});
});

81
test/testSsl.js Normal file
View File

@@ -0,0 +1,81 @@
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
var request = require('request');
var objects = null;
var states = null;
process.env.NO_PROXY = '127.0.0.1';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
describe('Test RESTful API SSL', function() {
before('Test RESTful API SSL: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
var brokerStarted = false;
setup.setupController(function () {
var config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
config.native.port = 18184;
config.native.auth = true;
config.native.secure = true;
setup.setAdapterConfig(config.common, config.native);
setup.startController(function (_objects, _states) {
objects = _objects;
states = _states;
// give some time to start server
setTimeout(function () {
_done();
}, 5000);
});
});
});
it('Test RESTful API SSL: get /api/plain - must return welcome text', function (done) {
request('https://admin:iobroker@127.0.0.1:18184/api/plain', function (error, response, body) {
console.log('get /api/plain => ' + body);
expect(error).to.be.not.ok;
expect(body).to.be.equal('Welcome to our text REST api!');
done();
});
});
it('Test RESTful API SSL: get /api - must return welcome text', function (done) {
request('https://admin:iobroker@127.0.0.1:18184/api', function (error, response, body) {
console.log('get /api => ' + body);
expect(error).to.be.not.ok;
expect(body).to.be.equal('{"message":"Welcome to our JSON REST api!"}');
done();
});
});
it('Test RESTful API SSL: get /api/id - must return value', function (done) {
request('https://admin:iobroker@127.0.0.1:18184/api/system.adapter.template-rest.0.alive', function (error, response, body) {
console.log('get /api => ' + body);
expect(error).to.be.not.ok;
var data = JSON.parse(body);
expect(data.val).to.be.true;
done();
});
});
it('Test RESTful API SSL: get /api/plain/id - must return value', function (done) {
request('https://admin:iobroker@127.0.0.1:18184/api/plain/system.adapter.template-rest.0.alive', function (error, response, body) {
console.log('get /api => ' + body);
expect(error).to.be.not.ok;
expect(body).to.be.equal('Value: true');
done();
});
});
after('Test RESTful API SSL: Stop js-controller', function (done) {
this.timeout(6000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
done();
});
});
});