Initial commit

master
zhongjin 6 years ago
commit ad1e4b76d6

3
.gitignore vendored

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

@ -0,0 +1,10 @@
Gruntfile.js
tasks
node_modules
.idea
.git
/node_modules
test
tmp
.travis.yml
appveyor.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

@ -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('yunkong2.'.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') || 'bluefox';
var email = grunt.option('email') || 'dogafox@gmail.com';
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/owntracks.png')) {
fs.renameSync(__dirname + '/admin/owntracks.png', __dirname + '/admin/' + newname + '.png');
}
if (fs.existsSync(__dirname + '/widgets/owntracks.html')) {
fs.renameSync(__dirname + '/widgets/owntracks.html', __dirname + '/widgets/' + newname + '.html');
}
if (fs.existsSync(__dirname + '/widgets/template/js/owntracks.js')) {
fs.renameSync(__dirname + '/widgets/template/js/owntracks.js', __dirname + '/widgets/template/js/' + newname + '.js');
}
if (fs.existsSync(__dirname + '/widgets/owntracks')) {
fs.renameSync(__dirname + '/widgets/owntracks', __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)) : 'Owntracks'
},
{
match: /bluefox/g,
replacement: author
},
{
match: /dogafox@gmail.com/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/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_jscsRules: {
options: {
url: 'https://raw.githubusercontent.com/yunkong2/yunkong2.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'
]);
};

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 'bluefox' <'dogafox@gmail.com'>
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.

@ -0,0 +1,82 @@
![Logo](admin/owntracks.png)
# yunkong2.owntracks
=================
[![NPM version](http://img.shields.io/npm/v/yunkong2.owntracks.svg)](https://www.npmjs.com/package/yunkong2.owntracks)
[![Downloads](https://img.shields.io/npm/dm/yunkong2.owntracks.svg)](https://www.npmjs.com/package/yunkong2.owntracks)
[![NPM](https://nodei.co/npm/yunkong2.owntracks.png?downloads=true)](https://nodei.co/npm/yunkong2.owntracks/)
[OwnTracks](http://owntracks.org/) is an app for android and iOS.
Link for:
- Andorid - [https://play.google.com/store/apps/details?id=org.owntracks.android](https://play.google.com/store/apps/details?id=org.owntracks.android)
- iOS - [https://itunes.apple.com/de/app/owntracks/id692424691?mt=8](https://itunes.apple.com/de/app/owntracks/id692424691?mt=8)
App sends continuously your position (position of device) to some specific server. In our case it will be yunkong2 server.
The MQTT protocol will be used for communication.
##Usage
OwnTracks Adapter starts on port 1883 (configurable) a MQTT server to receive the messages from devices with coordinates.
The problem is that this server must be reachable from internet.
Normally there is a router or firewall, that must be configured to forward traffic.
Settings in App:
- Connection/Mode - MQTT private
- Connection/Host/Host - IP address of your system or DynDNS domain. E.g. http://www.noip.com/ lets use domain name instead of IP address.
- Connection/Host/Port - 1883 or your port on your router
- Connection/Host/WebSockets - false
- Connection/Identification/Username - yunkong2
- Connection/Identification/Password - from adapter settings
- Connection/Identification/DeviceID - Name of device or person. For this device the states will be created. E.g. if deviceID is "Mark", following states will be created after first contact:
- owntracks.0.users.Mark.longitude
- owntracks.0.users.Mark.latitude
- Connection/Identification/TrackerID - Short name of user (up to 2 letters) to write it on map.
- Connection/Security/TLS - off
### Icons
You can define for every user an icon. Just upload per drag&drop or with mouse click you image. It will be automatically scaled to 64x64.
The name must be equal to DeviceID in OwnTracks app.
![Settings](img/settings1.png)
## Changelog
#### 0.3.0 (2018-06-05)
* (matspi) Fix handling of publish messages
#### 0.2.0 (2017-01-03)
* (jp112sdl) added two properties timestamp and datetime
#### 0.1.1 (2016-09-05)
* (bluefox) add pictures
#### 0.1.0 (2016-09-04)
* (bluefox) initial release
## License
The MIT License (MIT)
Copyright (c) 2016-2017 bluefox<dogafox@gmail.com>
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.

@ -0,0 +1,481 @@
<html>
<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>
<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>
<style>
#drop-zone {
width: calc(100% - 10px);
height: calc(100% - 10px);
position: absolute;
opacity: 0.8;
top: 0;
left: 0;
background: #eee;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
border-radius: 15px;
z-index: 1;
font-size: 32px;
font-weight: bold;
text-align: center;
border: 5px dashed darkgray;
}
.dropZone-error {
background: #faa !important;
color: #f00;
}
</style>
<script type="text/javascript">
var onChange;
systemDictionary = {
"OwnTracks adapter settings": {
"en": "OwnTracks adapter settings",
"de": "OwnTracks adapter settings",
"ru": "Настройки драйвера OwnTracks"
},
"Server settings": {"en": "Server settings", "de": "Server settings", "ru": "Настройки сервера settings"},
"User:": {"en": "User:", "de": "Anwender:", "ru": "Пользователь:"},
"Password:": {"en": "Password:", "de": "Kennwort:", "ru": "Пароль:"},
"Password repeat:": {"en": "Password repeat:", "de": "Kennwort-Wiederholung:", "ru": "Повтор пароля:"},
"Empty name not allowed!": {
"en": "Empty name not allowed!",
"de": "Ohne Namen ist nicht erlaubt!",
"ru": "Пустое имя пользователя не разрешено!"
},
"Ok": {"en": "Ok", "de": "Ok", "ru": "Ok"},
"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": "Порт для проверки доменного имени:"
},
"Drop the files here": {"en": "Drop the files here", "de": "Die Datei hier platzieren", "ru": "Перетащить файлы сюда"},
"Unknown file format!": {"en": "Unknown file format!", "de": "Unbekannter Dateiformat!", "ru": "Неизвестный формат файла"},
"Cannot read file!": {"en": "Cannot read file!", "de": "Kann die Datei nicht lesen!", "ru": "Невозможно прочитать файл!"},
"File is too big!": {"en": "File is too big!", "de": "Datei ist zu groß!", "ru": "Слишком большой файл!"}
};
function encrypt(key, value) {
var result = '';
for(var i = 0; i < value.length; i++) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
function decrypt(key, value) {
var result = '';
for(var i = 0; i < value.length; i++) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
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();
$('.le-settings').hide();
}
}
function fileHandler(event) {
event.preventDefault();
var file = event.dataTransfer ? event.dataTransfer.files[0] : event.target.files[0];
var $dz = $('#drop-zone');
if (file.size > 1000000) {
$('#drop-text').html(_('File is too big!'));
$dz.addClass('dropZone-error').animate({opacity: 0}, 1000, function () {
$dz.hide().removeClass('dropZone-error').css({opacity: 1});
showMessage(_('File is too big!'));
$('#drop-text').html(_('Drop the files here'));
});
return false;
}
$dz.show();
var reader = new FileReader();
reader.onload = function (evt) {
var text;
try {
text = evt.target.result; // string has form data:;base64,TEXT==
if ($('#drop-file').data('index') !== null) {
resizeImage(text, function (result) {
var pictures = getPictures();
pictures[$('#drop-file').data('index')].base64 = result;
onChange();
showPictures(pictures);
});
} else {
resizeImage(text, function (result) {
addPicture(null, result);
});
}
$dz.hide().removeClass('dropZone-error').css({opacity: 1});
} catch (err) {
console.error(err);
$('#drop-text').html(_('Cannot read file!'));
$dz.addClass('dropZone-error').animate({opacity: 0}, 1000, function () {
$dz.hide().removeClass('dropZone-error').css({opacity: 1});
showMessage(_('Cannot read file!'));
$('#drop-text').html(_('Drop the files here'));
});
}
};
reader.readAsDataURL(file);
}
function showPictureLine(pictures, i) {
var text = '<tr>';
text += '<td><input type="text" style="width: 100%" class="pictures-value" data-name="name" value="' + pictures[i].name + '" data-index="' + i +'"></td>';
text += '<td>' +
'<img width="64" src="' + pictures[i].base64 + '" class="pictures-value" data-name="base64" data-index="' + i +'" />' +
'<button class="pictures-upload" data-index="' + i +'"></td>';
text += '<td><button class="pictures-delete" data-index="' + i +'"></td>';
text += '</tr>';
return text;
}
function resizeImage(srcBase64, callback) {
var maxW = 64;
var maxH = 64;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var cw = canvas.width;
var ch = canvas.height;
var img = new Image;
img.onload = function() {
var iw = img.width;
var ih = img.height;
var scale = Math.min((maxW / iw), (maxH / ih));
var iwScaled = iw*scale;
var ihScaled = ih*scale;
canvas.width = iwScaled;
canvas.height = ihScaled;
ctx.drawImage(img,0,0,iwScaled,ihScaled);
callback(canvas.toDataURL());
};
try {
img.src = srcBase64;
} catch (e) {
callback(srcBase64);
}
}
function showPictures(pictures) {
var text = '';
for (var i = 0; i < pictures.length; i++) {
text += showPictureLine(pictures, i);
}
$('#pictures').html(text);
// init inputs and buttons
$('.pictures-value').change(function () {
onChange();
}).keyup(function () {
$(this).trigger('change');
});
$('.pictures-delete').button({
icons: {
primary: 'ui-icon-trash'
},
text: false
})
.css({width: 18, height: 18, float: 'right', 'margin-top': -5})
.attr('title', _('Delete picture and person'))
.click(function () {
// todo are you sure
var index = $(this).data('index');
var pictures = getPictures();
pictures.splice(index, 1);
showPictures(pictures);
});
$('.pictures-upload').button({
icons: {
primary: 'ui-icon-arrowthickstop-1-s'
},
text: false
})
.attr('title', _('Upload picture'))
.css({width: 18, height: 18, float: 'right', 'margin-top': 20})
.click(function () {
$('#drop-file').data('index', $(this).data('index'));
$('#drop-file').trigger('click');
});
}
function getPictures() {
var pictures = [];
$('.pictures-value').each(function () {
var index = $(this).data('index');
var name = $(this).data('name');
pictures[index] = pictures[index] || {};
if ($(this).prop('tagName') === 'IMG') {
pictures[index][name] = $(this).attr('src');
} else {
pictures[index][name] = $(this).val();
}
});
return pictures;
}
function addPicture(name, base64) {
var pictures = getPictures();
var id = 1;
if (!name) {
var found;
do {
found = false;
name = _('User') + ' ' + id;
for (var i = 0; i < pictures.length; i++) {
if (pictures[i].name === name) {
found = true;
id++
}
}
} while (found);
}
pictures.push({name: name, base64: base64 || ''});
onChange();
showPictures(pictures);
}
// the function loadSettings has to exist ...
function load(settings, _onChange) {
if (!settings) return;
onChange = _onChange;
$('#tabs').tabs({
activate: function (event, ui) {
if (ui.newPanel.selector == '#tabs-2') {
$('#drop-zone').show().css({opacity: 1}).animate({opacity: 0}, 2000, function () {
$('#drop-zone').hide().css({opacity: 1});
});
}
}
});
var $dropZone = $('#adapter-container');
if (typeof(window.FileReader) !== 'undefined' && !$dropZone.data('installed')) {
$dropZone.data('installed', true);
var $dz = $('#drop-zone');
$('#drop-text').html(_('Drop the files here'));
$dropZone[0].ondragover = function() {
$dz.unbind('click');
$dz.show();
return false;
};
$dz.click(function () {
$dz.hide();
});
$dz[0].ondragleave = function() {
$dz.hide();
return false;
};
$dz[0].ondrop = function (e) {
$('#drop-file').data('index', null);
fileHandler(e);
};
}
$('#drop-file').change(fileHandler);
// 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);
});
$('.value').each(function () {
var key = $(this).attr('id');
if ($('#' + key + '.value').attr('type') === 'checkbox') {
$('#' + key + '.value').prop('checked', settings[key]).change(function() {
_onChange();
});
} else {
if (key === 'pass') settings[key] = decrypt('Zgfr56gFe87jJOM', settings[key]);
$('#' + key + '.value').val(settings[key]).change(function() {
_onChange();
}).keyup(function() {
_onChange();
});
}
});
settings.pictures = settings.pictures || [];
$('#passRepeat').val($('#pass').val());
// 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);
$('#secure').change(showHideSettings);
showPictures(settings.pictures);
$('#add').button({
icons: {
primary: 'ui-icon-plus'
},
text: false
})
.css({width: 18, height: 18, margin: 5})
.attr('title', _('Add picture for person'))
.click(function () {
addPicture();
});
showHideSettings();
}
function save(callback) {
var obj = {};
$('.value').each(function () {
var $this = $(this);
var key = $this.attr('id');
if ($this.attr('type') === 'checkbox') {
obj[key] = $this.prop('checked');
} else {
if (key === 'pass') {
obj[key] = encrypt('Zgfr56gFe87jJOM', $this.val());
} else {
obj[key] = $this.val()
}
}
});
if (!obj.user) {
showMessage(_('Empty name not allowed!'));
return;
}
obj.pictures = getPictures();
callback(obj);
}
</script>
<div id="adapter-container">
<table><tr>
<td><img src="owntracks.png"/></td>
<td><h3 class="translate">OwnTracks adapter settings</h3></td>
</tr></table>
<div id="tabs">
<ul>
<li><a href="#tabs-1" class="translate">General</a></li>
<li><a href="#tabs-2" class="translate">Pictures</a></li>
</ul>
<div id="tabs-1">
<table>
<tr><td colspan="2"><h4 class="translate">Server settings</h4></td></tr>
<tr><td><label class="translate" for="bind">IP:</label></td><td> <select class="value" id="bind"></select></td></tr>
<tr><td><label class="translate" for="port">Port:</label></td><td> <input class="value" id="port" size="5" maxlength="5"/></td></tr>
<tr><td><label class="translate" for="user">User:</label></td><td> <input class="value" id="user"/></td></tr>
<tr><td><label class="translate" for="pass">Password:</label></td><td> <input class="value" id="pass" type="password"/></td></tr>
<tr><td><label class="translate" for="passRepeat">Password repeat:</label></td><td><input id="passRepeat" type="password"/></td></tr>
<!--
<tr><td><label class="translate" for="secure">Secure(HTTPS):</label></td><td> <input class="value" id="secure" type="checkbox" /></td></tr>
<tr id="_certPublic">
<td><label class="translate" for="certPublic">Public certificate:</label></td>
<td><select id="certPublic" class="value"></select></td>
</tr>
<tr id="_certPrivate">
<td><label class="translate" for="certPrivate">Private certificate:</label></td>
<td><select id="certPrivate" class="value"></select></td>
</tr>
<tr id="_certChained">
<td><label class="translate" for="certChained">Chained certificate:</label></td>
<td><select id="certChained" class="value"></select></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>
<div id="tabs-2">
<div id="drop-zone" style="display: none"><div style="padding-top: 15%" class="translate" id="drop-text"></div>
<input type="file" id="drop-file" style="display: none">
</div>
<button id="add"></button>
<table style="width: 330px">
<thead>
<tr class="ui-widget-header">
<td class="translate" style="width: 200px">Name</td>
<td class="translate">Picture</td>
<td></td>
</tr>
</thead>
<tbody id="pictures"></tbody>
</table>
</div>
</div>
</div>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

@ -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'

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@ -0,0 +1,78 @@
{
"common": {
"name": "owntracks",
"version": "0.3.0",
"title": "OwnTracks adapter",
"desc": {
"en": "yunkong2 OwnTracks Adapter",
"de": "yunkong2 OwnTracks Adapter",
"ru": "yunkong2 OwnTracks драйвер"
},
"news": {
"0.3.0": {
"en": "Fix handling of publish messages",
"de": "Problem in der Abarbeitung von publish-Nachrichten behoben"
},
"0.2.0": {
"en": "added two properties timestamp and datetime",
"de": "Timestamp und datetime sind hinzugefügt",
"ru": "Добавлены время последнего сообщения в разных форматах"
},
"0.1.1": {
"en": "add pictures",
"de": "Unterstützung von Bildern",
"ru": "Возможность добавить иконки"
},
"0.1.0": {
"en": "initial checkin",
"de": "Erste version",
"ru": "первая версия"
}
},
"platform": "Javascript/Node.js",
"mode": "daemon",
"icon": "owntracks.png",
"extIcon": "https://git.spacen.net/yunkong2/yunkong2.owntracks/raw/master/admin/owntracks.png",
"keywords": [
"owntracks",
"position",
"gps",
"geo"
],
"readme": "https://git.spacen.net/yunkong2/yunkong2.owntracks/blob/master/README.md",
"loglevel": "info",
"type": "geoposition",
"authors": [
{
"name": "bluefox",
"email": "dogafox@gmail.com"
}
]
},
"native": {
"port": 1883,
"user": "yunkong2",
"pass": "",
"secure": false,
"pictures": [],
"bind": "0.0.0.0",
"certPublic": "defaultPublic",
"certPrivate": "defaultPrivate",
"certChained": "",
"leEnabled": false,
"leUpdate": false,
"leCheckPort": 80
},
"objects": [],
"instanceObjects": [
{
"_id": "users",
"type": "channel",
"common": {
"role": "users",
"name": "List of users"
},
"native": {}
}
]
}

@ -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;

@ -0,0 +1,374 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
var utils = require(__dirname + '/lib/utils'); // Get common adapter utils
var adapter = utils.Adapter('owntracks');
//var LE = require(utils.controllerDir + '/lib/letsencrypt.js');
var createStreamServer = require('create-stream-server');
var mqtt = require('mqtt-connection');
var server;
var clients = {};
var objects = {};
function decrypt(key, value) {
var result = '';
for (var i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
// 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 (server) {
server.destroy();
server = null;
}
callback();
} catch (e) {
callback();
}
});
adapter.on('ready', main);
function createUser(user) {
var id = adapter.namespace + '.users.' + user.replace(/\s|\./g, '_');
adapter.getForeignObject(id + '.battery', function (err, obj) {
if (!obj) {
adapter.setForeignObject(id + '.battery', {
common: {
name: 'Device battery level for ' + user,
min: 0,
max: 100,
unit: '%',
role: 'battery',
type: 'number'
},
type: 'state',
native: {}
});
}
});
adapter.getForeignObject(id + '.latitude', function (err, obj) {
if (!obj) {
adapter.setForeignObject(id + '.latitude', {
common: {
name: 'Latitude for ' + user,
role: 'gps.latitude',
type: 'number'
},
type: 'state',
native: {}
});
}
});
adapter.getForeignObject(id + '.longitude', function (err, obj) {
if (!obj) {
adapter.setForeignObject(id + '.longitude', {
common: {
name: 'Longitude for ' + user,
role: 'gps.longitude',
type: 'number'
},
type: 'state',
native: {}
});
}
});
adapter.getForeignObject(id + '.accuracy', function (err, obj) {
if (!obj) {
adapter.setForeignObject(id + '.accuracy', {
common: {
name: 'Accuracy for ' + user,
role: 'state',
unit: 'm',
type: 'number'
},
type: 'state',
native: {}
});
}
});
adapter.getForeignObject(id + '.timestamp', function (err, obj) {
if (!obj) {
adapter.setForeignObject(id + '.timestamp', {
common: {
name: 'Timestamp for ' + user,
role: 'state',
type: 'number'
},
type: 'state',
native: {}
});
}
});
adapter.getForeignObject(id + '.datetime', function (err, obj) {
if (!obj) {
adapter.setForeignObject(id + '.datetime', {
common: {
name: 'Datetime for ' + user,
role: 'state',
type: 'string'
},
type: 'state',
native: {}
});
}
});
}
function sendState2Client(client, topic, payload) {
// client has subscription for this ID
if (client._subsID && client._subsID[topic]) {
client.publish({topic: topic, payload: payload});
} else
// Check patterns
if (client._subs) {
for (var s in client._subs) {
if (!client._subs.hasOwnProperty(s)) continue;
if (client._subs[s].regex.exec(topic)) {
client.publish({topic: topic, payload: payload});
break;
}
}
}
}
function processTopic(topic, payload, ignoreClient) {
for (var k in clients) {
// if get and set have different topic names, send state to issuing client too.
if (clients[k] === ignoreClient) continue;
sendState2Client(clients[k], topic, payload);
}
}
var cltFunction = function (client) {
client.on('connect', function (packet) {
client.id = packet.clientId;
if (adapter.config.user) {
if (adapter.config.user != packet.username ||
adapter.config.pass != packet.password) {
adapter.log.warn('Client [' + packet.clientId + '] has invalid password(' + packet.password + ') or username(' + packet.username + ')');
client.connack({returnCode: 4});
if (clients[client.id]) delete clients[client.id];
client.stream.end();
return;
}
}
adapter.log.info('Client [' + packet.clientId + '] connected');
client.connack({returnCode: 0});
clients[client.id] = client;
});
client.on('publish', function (packet) {
var isAck = true;
var topic = packet.topic;
var message = packet.payload;
adapter.log.debug('publish "' + topic + '": ' + message);
if (packet.qos == 1) {
client.puback({ messageId: packet.messageId});
}
else if (packet.qos == 2) {
client.pubrec({ messageId: packet.messageId});
}
// "owntracks/yunkong2/klte":
// {
// "_type":"location", // location, lwt, transition, configuration, beacon, cmd, steps, card, waypoint
// "acc":50, // accuracy of location in meters
// "batt":46, // is the device's battery level in percent (integer)
// "lat":49.0026446, // latitude
// "lon":8.3832128, // longitude
// "tid":"te", // is a configurable tracker-ID - ignored
// "tst":1472987109 // UNIX timestamp in seconds
// }
var parts = topic.split('/');
if (parts[1] !== adapter.config.user) {
adapter.log.warn('publish "' + topic + '": invalid user name - "' + parts[1] + '"');
return;
}
if (!objects[parts[2]]) {
// create object
createUser(parts[2]);
objects[parts[2]] = true;
}
processTopic(topic, message);
try {
var obj = JSON.parse(message);
if (obj._type === 'location') {
if (obj.acc !== undefined) {
adapter.setState('users.' + parts[2] + '.accuracy', {val: obj.acc, ts: obj.tst * 1000, ack: true});
}
if (obj.batt !== undefined) {
adapter.setState('users.' + parts[2] + '.battery', {val: obj.batt, ts: obj.tst * 1000, ack: true});
}
if (obj.lon !== undefined) {
adapter.setState('users.' + parts[2] + '.longitude', {val: obj.lon, ts: obj.tst * 1000, ack: true});
}
if (obj.lat !== undefined) {
adapter.setState('users.' + parts[2] + '.latitude', {val: obj.lat, ts: obj.tst * 1000, ack: true});
}
if (obj.tst !== undefined) {
adapter.setState('users.' + parts[2] + '.timestamp', {val: obj.tst, ts: obj.tst * 1000, ack: true});
var date = new Date(obj.tst * 1000);
var day = '0' + date.getDate();
var month = '0' + (date.getMonth() + 1);
var year = date.getFullYear();
var hours = '0' + date.getHours();
var minutes = '0' + date.getMinutes();
var seconds = '0' + date.getSeconds();
var formattedTime = day.substr(-2) + '.' + month.substr(-2) + '.' + year + ' ' + hours.substr(-2) + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
adapter.setState('users.' + parts[2] + '.datetime', {val: formattedTime, ts: obj.tst * 1000, ack: true});
}
}
} catch (e) {
adapter.log.error('Cannot parse payload: ' + message);
}
});
client.on('subscribe', function (packet) {
var granted = [];
if (!client._subsID) client._subsID = {};
if (!client._subs) client._subs = {};
for (var i = 0; i < packet.subscriptions.length; i++) {
granted.push(packet.subscriptions[i].qos);
var topic = packet.subscriptions[i].topic;
adapter.log.debug('Subscribe on ' + topic);
// if pattern without wildchars
if (topic.indexOf('*') === -1 && topic.indexOf('#') === -1 && topic.indexOf('+') === -1) {
client._subsID[topic] = {
qos: packet.subscriptions[i].qos
};
} else {
// "owntracks/+/+/info" => owntracks\/.+\/.+\/info
var pattern = topic.replace(/\//g, '\\/').replace(/\+/g, '[^\\/]+').replace(/\*/g, '.*');
pattern = '^' + pattern + '$';
// add simple pattern
client._subs[topic] = {
regex: new RegExp(pattern),
qos: packet.subscriptions[i].qos,
pattern: topic
};
}
}
client.suback({granted: granted, messageId: packet.messageId});
//Subscribe on owntracks/+/+
//Subscribe on owntracks/+/+/info
//Subscribe on owntracks/yunkong2/denis/cmd
//Subscribe on owntracks/+/+/event
//Subscribe on owntracks/+/+/waypoint
// send to client all images
if (adapter.config.pictures && adapter.config.pictures.length) {
setTimeout(function () {
for (var p = 0; p < adapter.config.pictures.length; p++) {
var text = adapter.config.pictures[p].base64.split(',')[1]; // string has form data:;base64,TEXT==
sendState2Client(client, 'owntracks/' + adapter.config.user + '/' + adapter.config.pictures[p].name + '/info',
JSON.stringify({
_type: 'card',
name: adapter.config.pictures[p].name,
face: text
})
);
}
}, 200);
}
});
client.on('pingreq', function (packet) {
adapter.log.debug('Client [' + client.id + '] pingreq');
client.pingresp();
});
client.on('disconnect', function (packet) {
adapter.log.info('Client [' + client.id + '] disconnected');
client.stream.end();
});
client.on('close', function (err) {
adapter.log.info('Client [' + client.id + '] closed');
delete clients[client.id];
});
client.on('error', function (err) {
adapter.log.warn('[' + client.id + '] ' + err);
});
};
function initMqttServer(config) {
var serverConfig = {};
var options = {
ssl: config.certificates,
emitEvents: true // default
};
config.port = parseInt(config.port, 10) || 1883;
if (config.ssl) {
serverConfig.mqtts = 'ssl://0.0.0.0:' + config.port;
if (config.webSocket) {
serverConfig.mqtwss = 'wss://0.0.0.0:' + (config.port + 1);
}
} else {
serverConfig.mqtts = 'tcp://0.0.0.0:' + config.port;
if (config.webSocket) {
serverConfig.mqtwss = 'ws://0.0.0.0:' + (config.port + 1);
}
}
server = createStreamServer(serverConfig, options, function (clientStream) {
cltFunction(mqtt(clientStream, {
notData: !options.emitEvents
}));
});
// to start
server.listen(function () {
if (config.ssl) {
adapter.log.info('Starting MQTT (Secure) ' + (config.user ? 'authenticated ' : '') + 'server on port ' + config.port);
if (config.webSocket) {
adapter.log.info('Starting MQTT-WebSocket (Secure) ' + (config.user ? 'authenticated ' : '') + 'server on port ' + (config.port + 1));
}
} else {
adapter.log.info('Starting MQTT ' + (config.user ? 'authenticated ' : '') + 'server on port ' + config.port);
if (config.webSocket) {
adapter.log.info('Starting MQTT-WebSocket ' + (config.user ? 'authenticated ' : '') + 'server on port ' + (config.port + 1));
}
}
});
}
function main() {
//noinspection JSUnresolvedVariable
adapter.config.pass = decrypt('Zgfr56gFe87jJOM', adapter.config.pass);
if (!adapter.config.user) {
adapter.log.error('Empty user name not allowed!');
process.stop(-1);
return;
}
if (adapter.config.secure) {
// Load certificates
adapter.getCertificates(function (err, certificates, leConfig) {
adapter.config.certificates = certificates;
adapter.config.leConfig = leConfig;
initMqttServer(adapter.config);
});
} else {
initMqttServer(adapter.config);
}
}

@ -0,0 +1,51 @@
{
"name": "yunkong2.owntracks",
"version": "0.3.0",
"description": "yunkong2 OwnTracks Adapter",
"author": {
"name": "bluefox",
"email": "dogafox@gmail.com"
},
"contributors": [
{
"name": "bluefox",
"email": "dogafox@gmail.com"
},
{
"name": "matspi",
"email": "matthias.spiller@fari.software"
}
],
"homepage": "https://git.spacen.net/yunkong2/yunkong2.owntracks",
"license": "MIT",
"keywords": [
"yunkong2",
"owntracks"
],
"repository": {
"type": "git",
"url": "https://git.spacen.net/yunkong2/yunkong2.owntracks"
},
"dependencies": {
"create-stream-server": "^0.1.1",
"mqtt-connection": "^2.1.1"
},
"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",
"request": "^2.75.0"
},
"main": "main.js",
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit"
},
"bugs": {
"url": "https://git.spacen.net/yunkong2/yunkong2.owntracks/issues"
},
"readmeFilename": "README.md"
}

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

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

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

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

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

@ -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('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();
});
});
Loading…
Cancel
Save