Initial commit

This commit is contained in:
2018-08-09 17:23:06 +08:00
commit d9ce220c93
39 changed files with 3985 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/.idea
/package-lock.json
/node_modules

10
.npmignore Normal file
View File

@@ -0,0 +1,10 @@
gulpfile.js
admin/i18n
tasks
node_modules
.idea
.git
/node_modules
test
.travis.yml
appveyor.yml

28
.travis.yml Normal file
View File

@@ -0,0 +1,28 @@
os:
- linux
- osx
language: node_js
node_js:
- '6'
- '8'
- '10'
before_install:
- 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then CC=gcc-4.9; fi'
- 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then CXX=g++-4.9; fi'
before_script:
- export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1)
- 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi'
- npm -v
- npm install -g homebridge
- npm install -g homebridge-http-webhooks
- npm install -g homebridge-sun-position
- export NODE_GLOBAL_DIR=$(npm root -g)
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
- libavahi-compat-libdnssd-dev

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Apollon77 <ingo@fischer-ka.de>
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.

113
README.md Normal file
View File

@@ -0,0 +1,113 @@
![Logo](admin/ham.png)
# yunkong2 Homebridge accessories manager
=================
Use Homebridge plugins in yunkong2 or run a global installed Homebridge as yunkong2 adapter.
All States from Homebridge will be available in yunkong2 too and can also be controlled there.
## Description
This adapter provides two different modes:
### Default (Wrapper) Mode
In the default mode the adapter allows you to use homebridge Plugin Modules directly.
You can explore all available plugins at the NPM website by [searching for the keyword `homebridge-plugin`](https://www.npmjs.com/search?q=homebridge-plugin).
You simply add the list of modules to the Adapter configuration and provide the configuration
in the JSON-editor (see Plugin descriptions).
After this all Homebridge objects will be created in yunkong2 too and all writable objects can
be changed too.
A link of successfully tried plugins with examples can be found here: https://forum.yunkong2.net/viewtopic.php?f=20&t=15021
### Global-Homebridge-Mode
If you already use Homebridge (Apple OpenSource SmartHome) to control your devices
then you can use this existing Homebridge installation and start this Homebridge
installation as yunkong2 process. In this case the Homebridge server is started by yunkong2.
Additionally all states from Homebridge are available as states in yunkong2 and allows to
control from yunkong2.
For this to work you need to provide the location of the systems global node-modules folder. For this call **npm root -g**. Additionally you need to privide the path of the homebridge configuration directory (usually .homebridge in the users folder).
## Following adapters were tested in Default mode
* homebridge-chamberlain v1.0.1 - plugin for Chamberlain garage door openers with MyQ
* homebridge-doorbird v0.0.4 - Plugin for Doorbird
* homebridge-dyson-link v2.2.2 - Dyson Link devices
* homebridge-edomoticz v2.1.11 - A fully-fledged up-to-date Plugin for Domoticz
* homebridge-Fibaro-HC2 v2.1.5 - Fibaro HomeCenter integration
* homebridge-homee v0.2.4 - A fully-fledged up-to--date Plugin for Homee
* homebridge-ikea-tradfri-gateway v1.0.26 - Tradfri
* homebridge-noolite v0.0.29 - Noolite via USB MTRF-64 or МТRF-64 modules
* homebridge-platform-wemo v1.0.1 - Belkin WeMo Platform plugin
* homebridge-seasons v1.0.1 - A plugin to display the current season of the year.
* homebridge-vera v0.8.2 - VeraLink is an application for Z-Wave accessories from Vera (Node.js 8.11.3)
## TODO
* Tests
* More documentation?!
## Changelog
### 0.4.4 (2018.08.07)
* (Apollon77) corrected automatic role determination and bugs fixed
### 0.4.2 (2018.06.25)
* (Apollon77) Fix for global mode
### 0.4.1 (2018.06.21)
* (Apollon77) option to poll values from the plugins added and other optimizations
### 0.3.1 (2018.06.20)
* (kirovilya) Fixed a bug in global mode that values were not reported back to iOS devices
### 0.3.0 (2018.06.20)
* (bluefox) Support of ham plugins was added
### 0.2.6 (2018.06.19)
* (Apollon77) Updates for Homebridge-Wrapper
### 0.2.5 (2018.06.18)
* (Apollon77) Catch all console logs from Homegridge and make available as debug log
### 0.2.4 (2018.06.18)
* (Apollon77) Updates for Homebridge-Wrapper
### 0.2.3 (2018.06.17)
* (Apollon77) Updates for Homebridge-Wrapper
### 0.2.2 (2018.06.17)
* (Bluefox) Fixes for JSON editor in Firefox and Chrome
### 0.2.0/0.2.1 (2018.06.17)
* (Apollon77) Public test version with both modes
* (Bluefox) Admin3
### 0.1.0 (2018.06.09)
* (Apollon77) Update for working mode 1
### 0.0.1 (2018.03.24)
* (kirovilya) initial commit
## License
The MIT License (MIT)
Copyright (c) 2018 Apollon77 <ingo@fischer-ka.de>
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.

BIN
admin/ham.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

219
admin/index_m.html Normal file
View File

@@ -0,0 +1,219 @@
<html>
<head>
<!-- these 4 files always have to be included -->
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/ace-1.2.0/ace.js"></script>
<!-- these files always have to be included -->
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../lib/js/materialize.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<style>
.m .col .select-wrapper+label {
top: -26px;
}
.m span {
font-size: 12px;
}
#wrapperConfig {
width: 100%;
height: 100%;
}
#tab-json>.row {
height: calc(100% - 20px);
}
#tab-json .col {
height: 100%;
}
.adapter-container>.row {
height: 100%;
}
.m .page {
height: calc(100% - 50px);
}
</style>
<!-- you have to define 2 functions in the global scope: -->
<script type="text/javascript">
var editor;
var wrapperConfig;
function initEditor(onChange) {
if (!editor) {
setTimeout(function () {
if ($('#wrapperConfig').is(':visible')) {
editor = ace.edit('wrapperConfig');
editor.getSession().setMode('ace/mode/json');
editor.$blockScrolling = true;
editor.setValue(wrapperConfig);
editor.getSession().on('change', function() {
onChange();
});
} else {
initEditor(onChange);
}
}, 200);
}
}
// the function loadSettings has to exist ...
function load(settings, onChange) {
// example: select elements with id=key and class=value and insert value
if (!settings) return;
if (settings.characteristicPollingInterval === undefined) settings.characteristicPollingInterval = 0;
$('.value').each(function () {
var $key = $(this);
var id = $key.attr('id');
if ($key.attr('type') === 'checkbox') {
// do not call onChange direct, because onChange could expect some arguments
$key.prop('checked', settings[id]).change(function() {
onChange();
});
} else {
// do not call onChange direct, because onChange could expect some arguments
$key.val(settings[id]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
$('.tab-json').on('click', function (e) {
initEditor(onChange);
});
$('#useGlobalHomebridge').on('change', function () {
if ($(this).prop('checked')) {
$('.global').show();
$('.local').hide();
$('.tab-json').addClass('disabled');
} else {
$('.global').hide();
$('.local').show();
$('.tab-json').removeClass('disabled');
}
}).trigger('change');
wrapperConfig = JSON.stringify(settings.wrapperConfig || '{}', null, 2);
list2chips('.libraries', settings.libraries || '', onChange);
onChange(false);
M.updateTextFields(); // function Materialize.updateTextFields(); to reinitialize all the Materialize labels on the page if you are dynamically adding inputs.
}
function list2chips(selector, list, onChange) {
var chips = list.split(/[,;\s]+/);
var data = [];
for (var c = 0; c < chips.length; c++) {
if (chips[c] && chips[c].trim()) {
data.push({tag: chips[c].trim()});
}
}
$(selector).chips({
data: data,
placeholder: _('Module names'),
secondaryPlaceholder: _('Add module'),
onChipAdd: onChange,
onChipDelete: onChange
});
}
function chips2list(selector) {
var data = $(selector).chips('getData');
var text = [];
for (var lib = 0; lib < data.length; lib++) {
text.push(data[lib].tag);
}
return text.join(' ');
}
// ... 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();
}
});
obj.libraries = chips2list('.libraries');
wrapperConfig = editor ? editor.getValue() : wrapperConfig;
var _obj;
try {
_obj = JSON.parse(wrapperConfig);
} catch (e) {
showError(e, _('Parse error'), 'error_outline');
return false;
}
obj.wrapperConfig = _obj;
callback(obj);
}
</script>
</head>
<body>
<!-- you have to put your config page in a div with id adapter-container -->
<div class="m adapter-container">
<div class="row">
<div class="col s12">
<ul class="tabs">
<li class="tab col s4"><a href="#tab-main" class="translate active">Main settings</a></li>
<li class="tab col s4 tab-json"><a href="#tab-json" class="translate">Configuration file</a></li>
</ul>
</div>
<div id="tab-main" class="col s12 page">
<div class="row">
<div class="col s6 m4 l2">
<img src="ham.png" class="logo">
</div>
</div>
<div class="row">
<div class="input-field col s12 m6 l4 xl3">
<input id="useGlobalHomebridge" type="checkbox" class="value" />
<span for="useGlobalHomebridge" class="translate">useGlobalHomebridge</span>
</div>
</div>
<div class="row global">
<div class="input-field col s12 m6 l4 xl3">
<input class="value" id="globalHomebridgeBasePath" type="text">
<label for="globalHomebridgeBasePath">Global Homebridge Path</label>
<span class="translate">Global Homebridge Path</span>
</div>
<div class="input-field col s12 m6 l4 xl3">
<input class="value" id="globalHomebridgeConfigPath" type="text">
<label for="globalHomebridgeConfigPath">Global Homebridge Config Directory Path</label>
<span class="translate">Global Homebridge Config Directory Path</span>
</div>
</div>
<div class="row local">
<div class="col s12">
<label class="translate">Additional npm modules:</label>
<div class="chips libraries"></div>
</div>
</div>
<div class="row local">
<div class="input-field col s4 ">
<input class="value" id="characteristicPollingInterval" type="number" min="0" max="1000000"/>s
<label for="characteristicPollingInterval">Value Polling Interval</label>
<span class="translate">Value Polling Interval</span>
</div>
</div>
</div>
<div id="tab-json" class="col s12 page">
<div id="wrapperConfig">Wrapper-Konfiguration (Non-Global-Homebridge)</div>
</div>
</div>
</div>
</body>
</html>

26
admin/words.js Normal file
View File

@@ -0,0 +1,26 @@
// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n
/*global systemDictionary:true */
'use strict';
systemDictionary = {
"Add module": { "en": "Add module", "de": "Modul hinzufügen", "ru": "Добавить модуль", "pt": "Adicionar módulo", "nl": "Module toevoegen", "fr": "Add module", "it": "Aggiungi modulo", "es": "Agregar módulo", "pl": "Dodaj moduł"},
"Additional npm modules:": { "en": "Additional npm modules:", "de": "Zusätzliche npm-Module:", "ru": "Дополнительные модули npm:", "pt": "Módulos adicionais npm:", "nl": "Extra npm-modules:", "fr": "Additional npm modules:", "it": "Moduli aggiuntivi di npm:", "es": "Módulos npm adicionales:", "pl": "Dodatkowe moduły npm:"},
"Configuration file": { "en": "Configuration file", "de": "Konfigurationsdatei", "ru": "Файл конфигурации", "pt": "Arquivo de configuração", "nl": "Configuratiebestand", "fr": "Configuration file", "it": "File di configurazione", "es": "Archivo de configuración", "pl": "Plik konfiguracyjny"},
"Global Homebridge Config Directory Path": { "en": "Global Homebridge Config Directory Path", "de": "Globaler Homebridge-Konfigurationsverzeichnispfad", "ru": "Путь к каталогу Global Homebridge Config", "pt": "Caminho global do diretório de configuração do Homebridge", "nl": "Wereldwijd Homebridge Config Directory-pad", "fr": "Global Homebridge Config Directory Path", "it": "Percorso globale della directory di configurazione di Homebridge", "es": "Ruta del directorio de Global Homebridge Config", "pl": "Ścieżka katalogu konfiguracji globalnej Homebridge"},
"Global Homebridge Path": { "en": "Global Homebridge Path", "de": "Globaler Homebridge-Pfad", "ru": "Глобальный путь на мосту", "pt": "Caminho Global Homebridge", "nl": "Wereldwijd homebridge pad", "fr": "Global Homebridge Path", "it": "Percorso globale di Homebridge", "es": "Ruta Global Homebridge", "pl": "Global Homebridge Path"},
"Main settings": { "en": "Main settings", "de": "Haupteinstellungen", "ru": "Основные настройки", "pt": "Configurações principais", "nl": "Belangrijkste instellingen", "fr": "Réglages principaux", "it": "Impostazioni principali", "es": "Ajustes principales", "pl": "Ustawienia główne"},
"Module names": { "en": "Module names", "de": "Modulnamen", "ru": "Имена модулей", "pt": "Nomes de módulos", "nl": "Module namen", "fr": "Module names", "it": "Nomi dei moduli", "es": "Nombres de módulos", "pl": "Nazwy modułów"},
"useGlobalHomebridge": { "en": "Use global Homebridge", "de": "Verwenden Sie globale Homebridge", "ru": "Использовать глобальный Homebridge", "pt": "Use Homebridge global", "nl": "Gebruik wereldwijde Homebridge", "fr": "useGlobalHomebridge", "it": "Usa Homebridge globale", "es": "Utilice Global Homebridge", "pl": "Użyj globalnego rozwiązania Homebridge"},
"wrapperConfig": { "en": "Wrapper configuration", "de": "Wrapper-Konfiguration", "ru": "Конфигурация Wrapper", "pt": "Configuração do invólucro", "nl": "Wrapper-configuratie", "fr": "wrapperConfig", "it": "Configurazione wrapper", "es": "Configuración del contenedor", "pl": "Konfiguracja opakowania"},
"Value Polling Interval": {
"en": "Value Polling Interval",
"de": "Wertabfrageintervall",
"ru": "Интервал опроса значений",
"pt": "Intervalo de Polling de Valor",
"nl": "Value Polling Interval",
"fr": "Intervalle d'interrogation de valeur",
"it": "Intervallo di polling del valore",
"es": "Intervalo de interrogación de valor",
"pl": "Okres odpytywania wartości"
}
};

35
appveyor.yml Normal file
View File

@@ -0,0 +1,35 @@
version: 'test-{build}'
environment:
matrix:
- nodejs_version: '6'
- nodejs_version: '8'
- nodejs_version: '10'
platform:
- x86
- x64
clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%'
install:
- ps: 'Install-Product node $env:nodejs_version $env:platform'
- ps: '$NpmVersion = (npm -v).Substring(0,1)'
- ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }'
- ps: npm --version
- 'appveyor DownloadFile https://github.com/Apollon77/SupportingFiles/raw/master/appveyor/bonjour/bonjourcore2.msi'
- msiexec /i bonjourcore2.msi /qn
- del bonjourcore2.msi
- 'appveyor DownloadFile https://github.com/Apollon77/SupportingFiles/raw/master/appveyor/bonjour/bonjoursdksetup.exe'
- bonjoursdksetup.exe /quiet
- del bonjoursdksetup.exe
- 'set BONJOUR_SDK_HOME=C:\Program Files\Bonjour SDK'
- npm install -g homebridge
- npm install -g homebridge-http-webhooks
- npm install -g homebridge-sun-position
- for /f "delims=" %%A in ('npm root -g') do set "NODE_GLOBAL_DIR=%%A"
- npm install
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
test_script:
- echo %cd%
- node --version
- npm --version
- npm test
build: 'off'

BIN
docs/de/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/de/template.md Normal file
View File

@@ -0,0 +1,3 @@
# Das ist die Dokumentation
(Picture)[img/picture.png)

BIN
docs/en/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/en/template.md Normal file
View File

@@ -0,0 +1,3 @@
# This is Documentation
(Picture)[img/picture.png)

BIN
docs/es/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/es/template.md Normal file
View File

@@ -0,0 +1,3 @@
# This is Documentation
(Picture)[img/picture.png)

BIN
docs/fr/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/fr/template.md Normal file
View File

@@ -0,0 +1,3 @@
# This is Documentation
(Picture)[img/picture.png)

BIN
docs/it/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/it/template.md Normal file
View File

@@ -0,0 +1,3 @@
# This is Documentation
(Picture)[img/picture.png)

BIN
docs/nl/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/nl/template.md Normal file
View File

@@ -0,0 +1,3 @@
# This is Documentation
(Picture)[img/picture.png)

BIN
docs/pt/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/pt/template.md Normal file
View File

@@ -0,0 +1,3 @@
# This is Documentation
(Picture)[img/picture.png)

BIN
docs/ru/img/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

3
docs/ru/template.md Normal file
View File

@@ -0,0 +1,3 @@
# Это документация
(Picture)[img/picture.png)

406
gulpfile.js Normal file
View File

@@ -0,0 +1,406 @@
'use strict';
var gulp = require('gulp');
var fs = require('fs');
var srcDir = __dirname + '/';
var pkg = require('./package.json');
var iopackage = require('./io-package.json');
var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
/*var appName = getAppName();
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 1].split('.')[0].toLowerCase();
}
*/
var languages = {
en: {},
de: {},
ru: {},
pt: {},
nl: {},
fr: {},
it: {},
es: {},
pl: {}
};
function lang2data(lang, isFlat) {
var str = isFlat ? '' : '{\n';
var count = 0;
for (var w in lang) {
if (lang.hasOwnProperty(w)) {
count++;
if (isFlat) {
str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n';
} else {
var key = ' "' + w.replace(/"/g, '\\"') + '": ';
str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n';
}
}
}
if (!count) return isFlat ? '' : '{\n}';
if (isFlat) {
return str;
} else {
return str.substring(0, str.length - 2) + '\n}';
}
}
function readWordJs(src) {
var fs = require('fs');
try {
var words;
if (fs.existsSync(src + 'js/words.js')) {
words = fs.readFileSync(src + 'js/words.js').toString();
} else {
words = fs.readFileSync(src + 'words.js').toString();
}
var lines = words.split(/\r\n|\r|\n/g);
var i = 0;
while (!lines[i].match(/^systemDictionary = {/)) {
i++;
}
lines.splice(0, i);
// remove last empty lines
i = lines.length - 1;
while (!lines[i]) {
i--;
}
if (i < lines.length - 1) {
lines.splice(i + 1);
}
lines[0] = lines[0].replace('systemDictionary = ', '');
lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}');
words = lines.join('\n');
var resultFunc = new Function('return ' + words + ';');
return resultFunc();
} catch (e) {
return null;
}
}
function padRight(text, totalLength) {
return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : '');
}
function writeWordJs(data, src) {
var fs = require('fs');
var text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n';
text += '/*global systemDictionary:true */\n';
text += '\'use strict\';\n\n';
text += 'systemDictionary = {\n';
for (var word in data) {
if (data.hasOwnProperty(word)) {
text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50);
var line = '';
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' ';
}
}
if (line) {
line = line.trim();
line = line.substring(0, line.length - 1);
}
text += line + '},\n';
}
}
text += '};';
if (fs.existsSync(src + 'js/words.js')) {
fs.writeFileSync(src + 'js/words.js', text);
} else {
fs.writeFileSync(src + 'words.js', text);
}
}
const EMPTY = '';
function words2languages(src) {
var fs = require('fs');
var langs = Object.assign({}, languages);
var data = readWordJs(src);
if (data) {
for (var word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var keys = Object.keys(langs[l]);
keys.sort();
var obj = {};
for (var k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
if (!fs.existsSync(src + 'i18n/' + l)) {
fs.mkdirSync(src + 'i18n/' + l);
}
fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj));
}
} else {
console.error('Cannot read or parse words.js');
}
}
function words2languagesFlat(src) {
var fs = require('fs');
var langs = Object.assign({}, languages);
var data = readWordJs(src);
if (data) {
for (var word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
var keys = Object.keys(langs.en);
keys.sort();
for (var l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var obj = {};
for (var k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
langs[l] = obj;
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var ll in langs) {
if (!langs.hasOwnProperty(ll)) continue;
if (!fs.existsSync(src + 'i18n/' + ll)) {
fs.mkdirSync(src + 'i18n/' + ll);
}
fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en));
}
fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n'));
} else {
console.error('Cannot read or parse words.js');
}
}
function languagesFlat2words(src) {
var fs = require('fs');
var dirs = fs.readdirSync(src + 'i18n/');
var langs = {};
var bigOne = {};
var order = Object.keys(languages);
dirs.sort(function (a, b) {
var posA = order.indexOf(a);
var posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB) return 1;
if (posA < posB) return -1;
return 0;
}
});
var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n');
for (var l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
var lang = dirs[l];
var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n');
langs[lang] = {};
keys.forEach(function (word, i) {
langs[lang][word] = values[i];
});
var words = langs[lang];
for (var word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
var aWords = readWordJs();
var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt'];
if (aWords) {
// Merge words together
for (var w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
bigOne[w] = aWords[w]
}
dirs.forEach(function (lang) {
if (temporaryIgnore.indexOf(lang) !== -1) return;
if (!bigOne[w][lang]) {
console.warn('Missing "' + lang + '": ' + w);
}
});
}
}
}
writeWordJs(bigOne, src);
}
function languages2words(src) {
var fs = require('fs');
var dirs = fs.readdirSync(src + 'i18n/');
var langs = {};
var bigOne = {};
var order = Object.keys(languages);
dirs.sort(function (a, b) {
var posA = order.indexOf(a);
var posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB) return 1;
if (posA < posB) return -1;
return 0;
}
});
for (var l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
var lang = dirs[l];
langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString();
langs[lang] = JSON.parse(langs[lang]);
var words = langs[lang];
for (var word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
var aWords = readWordJs();
var temporaryIgnore = ['pt', 'fr', 'nl', 'it'];
if (aWords) {
// Merge words together
for (var w in aWords) {
if (aWords.hasOwnProperty(w)) {
if (!bigOne[w]) {
console.warn('Take from actual words.js: ' + w);
bigOne[w] = aWords[w]
}
dirs.forEach(function (lang) {
if (temporaryIgnore.indexOf(lang) !== -1) return;
if (!bigOne[w][lang]) {
console.warn('Missing "' + lang + '": ' + w);
}
});
}
}
}
writeWordJs(bigOne, src);
}
gulp.task('adminWords2languages', function (done) {
words2languages('./admin/');
done();
});
gulp.task('adminWords2languagesFlat', function (done) {
words2languagesFlat('./admin/');
done();
});
gulp.task('adminLanguagesFlat2words', function (done) {
languagesFlat2words('./admin/');
done();
});
gulp.task('adminLanguages2words', function (done) {
languages2words('./admin/');
done();
});
gulp.task('updatePackages', function (done) {
iopackage.common.version = pkg.version;
iopackage.common.news = iopackage.common.news || {};
if (!iopackage.common.news[pkg.version]) {
var news = iopackage.common.news;
var newNews = {};
newNews[pkg.version] = {
en: 'news',
de: 'neues',
ru: 'новое'
};
iopackage.common.news = Object.assign(newNews, news);
}
fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4));
done();
});
gulp.task('updateReadme', function (done) {
var readme = fs.readFileSync('README.md').toString();
var pos = readme.indexOf('## Changelog\n');
if (pos !== -1) {
var readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
var readmeEnd = readme.substring(pos + '## Changelog\n'.length);
if (readme.indexOf(version) === -1) {
var timestamp = new Date();
var date = timestamp.getFullYear() + '-' +
('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' +
('0' + (timestamp.getDate()).toString(10)).slice(-2);
var news = '';
if (iopackage.common.news && iopackage.common.news[pkg.version]) {
news += '* ' + iopackage.common.news[pkg.version].en;
}
fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd);
}
}
done();
});
gulp.task('default', ['updatePackages', 'updateReadme']);

49
io-package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"common": {
"name": "ham",
"version": "0.4.4",
"title": "Homebridge accessories manager",
"titleLang": {
"en": "Homebridge accessories manager",
"de": "智能家具网关"
},
"desc": {
"en": "yunkong2 Homebridge Adapter"
},
"authors": [
"Kirov Ilya <kirovilya@gmail.com>",
"Apollon77 <ingo@fischer-ka.de"
],
"docs": {
"en": "docs/en/admin.md"
},
"platform": "Node.js",
"mode": "daemon",
"icon": "ham.png",
"materialize": true,
"enabled": false,
"extIcon": "https://git.spacen.net/yunkong2/yunkong2.ham/raw/master/admin/ham.png",
"keywords": ["homebridge"],
"readme": "https://github.com/yunkong2/yunkong2.ham/blob/master/README.md",
"loglevel": "info",
"type": "iot-systems"
},
"native": {
"useGlobalHomebridge": false,
"globalHomebridgeBasePath": "/usr/local/lib/node_modules/homebridge/",
"globalHomebridgeConfigPath": "/home/pi/.homebridge/",
"libraries": "",
"ignoreInfoAccessoryServices": false,
"characteristicPollingInterval": 0,
"wrapperConfig": {
"description": "Add configuration for your accessories or platforms according to the docs of the plugins.",
"accessories": [
],
"platforms": [
]
}
},
"objects": [
]
}

280
lib/global-handler.js Normal file
View File

@@ -0,0 +1,280 @@
/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
/* jslint esversion: 6 */
'use strict';
const inherits = require('util').inherits;
let User;
let hap;
//let version;
let Server;
//let Plugin;
//let log;
let Characteristic;
let once;
let server;
const charMap = {};
let logger;
let updateState;
let updateDev;
let updateChannel;
let setState;
let mapper;
let ignoreInfoAccessoryServices;
function customStringify(v, func, intent) {
const cache = new Map();
return JSON.stringify(v, function (key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.get(value)) {
// Circular reference found, discard key
return;
}
// Store value in our map
cache.set(value, true);
}
return value;
}, intent);
}
function init(config) {
logger = config.logger;
updateState = config.updateState;
updateDev = config.updateDev;
updateChannel = config.updateChannel;
setState = config.setState;
ignoreInfoAccessoryServices = config.ignoreInfoAccessoryServices;
mapper = require('./mapper')(config);
User = require(config.homebridgeBasePath + 'lib/user').User;
User.setStoragePath(config.homebridgeConfigPath);
hap = require(config.homebridgeBasePath + 'node_modules/hap-nodejs');
//version = require(config.homebridgeBasePath + 'lib/version');
Server = require(config.homebridgeBasePath + 'lib/server').Server;
//Plugin = require(config.homebridgeBasePath + 'lib/plugin').Plugin;
//log = require(config.homebridgeBasePath + 'lib/logger')._system;
once = require(config.homebridgeBasePath + 'node_modules/hap-nodejs/lib/util/once').once;
Characteristic = hap.Characteristic;
inherits(MyBridge, hap.Bridge);
override(MyBridge, function publish(info, allowInsecureRequest) {
logger.debug('yunkong2.ham Bridge publish ' + customStringify(info));
// Вызов метода родительского класса
// Calling the method of the parent class
publish.inherited.call(this, info, allowInsecureRequest);
});
/*
// Переопределение метода в дочернем классе
// Overriding a method in a child class
override(MyBridge, function addService(service) {
// Собственный функционал
// Own Functionality
logger.info('yunkong2.ham Bridge addService '+JSON.stringify(service)); //OK, undefined
// Вызов метода родительского класса
// Calling the method of the parent class
return addService.inherited.call(this, service);
});
*/
function iterateCharArray(chars, accessory, service, dev_idname, sr_id, sr_idname) {
for (const channelIndex in chars) {
if (!chars.hasOwnProperty(channelIndex)) continue;
const char = chars[channelIndex];
const ch_id = char.UUID;
const ch_name = char.displayName;
const ch_val = char.value;
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, ch_name);
const id = dev_idname + '.' + sr_idname + '.' + ch_idname;
const common = mapper.mapCharacteristicProperties(char);
logger.debug('Mapped Common for ' + id + ': ' + JSON.stringify(common));
if (common.write) {
charMap[id] = char; // TODO only if write allowed!
logger.silly('Add object to charmap with id ' + id + '/' + customStringify(char));
}
char.on('change', data => {
logger.debug('Char change event: ' + data.oldValue + ' --> ' + data.newValue);
handleCharValue(accessory, service, char, data.newValue);
});
updateState(dev_idname, sr_idname, ch_idname, ch_name, ch_val, common, ch_id, () => {
char.getValue((err, value) => {
if (err) {
logger.warn('Error while getting current value: ' + err);
return;
}
handleCharValue(accessory, service, char, value);
})
});
}
}
override(MyBridge, function addBridgedAccessory(accessory, deferUpdate) {
// Вызов метода родительского класса
// Calling the method of the parent class
accessory = addBridgedAccessory.inherited.call(this, accessory, deferUpdate);
logger.debug('yunkong2.ham Bridge addBridgedAccessory ' + customStringify(accessory)); //OK
// Новое устройство
// New device
const dev_id = accessory.UUID;
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
const dev_name = accessory.displayName;
const dev_cat = accessory.category;
updateDev(dev_idname, dev_name, dev_cat, dev_id);
for (const index in accessory.services) {
if (!accessory.services.hasOwnProperty(index)) continue;
const service = accessory.services[index];
const sr_id = service.UUID;
const sr_idname = mapper.mapServiceType(sr_id, service.displayName);
const sr_name = service.displayName;
if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') {
continue;
}
logger.silly('Add service class=' + customStringify(service));
updateChannel(dev_idname, sr_idname, sr_name, sr_id);
iterateCharArray(service.characteristics, accessory, service, dev_idname, sr_id, sr_idname);
if (service.optionalCharacteristics) {
iterateCharArray(service.optionalCharacteristics, accessory, service, dev_idname, sr_id, sr_idname);
}
}
return accessory;
});
Server.prototype._createBridge = function() {
logger.debug('yunkong2.ham Bridge create'); //OK
// pull out our custom Bridge settings from config.json, if any
const bridgeConfig = this._config.bridge || {};
// Create our Bridge which will host all loaded Accessories
return new MyBridge(bridgeConfig.name || 'Homebridge', hap.uuid.generate('HomeBridge'));
};
// Updated to compare value differetly. Needed till officially updated
Characteristic.prototype.setValue = function(newValue, callback, context, connectionID) {
if ( newValue instanceof Error ) {
this.status = newValue
} else {
this.status = null;
}
newValue = this.validateValue(newValue); //validateValue returns a value that has be cooerced into a valid value.
var oldValue = this.value;
if (this.listeners('set').length > 0) {
// allow a listener to handle the setting of this value, and wait for completion
this.emit('set', newValue, once(function(err) {
this.status = err;
if (err) {
// pass the error along to our callback
if (callback) callback(err);
}
else {
if (newValue === undefined || newValue === null)
newValue = this.getDefaultValue();
// setting the value was a success; so we can cache it now
this.value = newValue;
if (callback) callback();
if (this.eventOnlyCharacteristic === true || oldValue !== newValue)
this.emit('change', { oldValue:oldValue, newValue:newValue, context:context });
}
}.bind(this)), context, connectionID);
}
else {
if (newValue === undefined || newValue === null)
newValue = this.getDefaultValue();
// no one is listening to the 'set' event, so just assign the value blindly
this.value = newValue;
if (callback) callback();
if (this.eventOnlyCharacteristic === true || oldValue !== newValue)
this.emit('change', { oldValue:oldValue, newValue:newValue, context:context });
}
return this; // for chaining
}
function MyBridge(displayName, serialNumber) {
logger.debug('yunkong2.ham Bridge constructor');
MyBridge.super_.call(this, displayName, serialNumber);
}
}
function registerExistingAccessory(UUID, name) {
mapper.mapAccessoryUUID(UUID, name);
}
function start() {
const insecureAccess = false;
logger.info('Using Homebridge Config Path: ' + User.persistPath());
// Initialize HAP-NodeJS with a custom persist directory
hap.init(User.persistPath());
server = new Server(insecureAccess);
server.run();
}
function end() {
if (server) {
server._teardown();
// Save cached accessories to persist storage.
server._updateCachedAccessories();
}
}
function setValueForCharId(id, value) {
if (charMap[id]) {
logger.debug('set value of char for ' + id);
charMap[id].setValue(value);
}
}
function handleCharValue(accessory, serv, char, newValue){
logger.debug('handleCharValue = ' + newValue);
logger.silly('characteristic = ' + customStringify(char));
logger.silly('accessory =' + customStringify(accessory));
const sr_id = serv.UUID;
const sr_idname = mapper.mapServiceType(sr_id, serv.displayName);
const ch_id = char.UUID;
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, char.displayName);
const dev_id = accessory.UUID;
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
const value = newValue;
setState(dev_idname, sr_idname, ch_idname, value);
}
// Средство для переопределения функций
// Tools for overriding functions
function override(child, fn) {
child.prototype[fn.name] = fn;
fn.inherited = child.super_.prototype[fn.name];
}
exports.init = init;
exports.end = end;
exports.setValueForCharId = setValueForCharId;
exports.start = start;
exports.registerExistingAccessory = registerExistingAccessory;

276
lib/mapper.js Normal file
View File

@@ -0,0 +1,276 @@
/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
/* jslint esversion: 6 */
'use strict';
module.exports = function (config) {
const module = {};
let accTypes;
if (config.useGlobalHomebridge) {
accTypes = require(config.homebridgeBasePath + 'node_modules/hap-nodejs/accessories/types');
}
else {
accTypes = require('homebridge-plugin-wrapper').HapTypes;
}
const forbiddenCharacters = /[\]\[*,;'"`<>\\\s?]/g;
const accessoryNameMap = {};
module.mapAccessoryUUID = function (accessoryUUID, displayName) {
if (accessoryNameMap[accessoryUUID]) {
return accessoryNameMap[accessoryUUID];
}
if (displayName) {
displayName = displayName.replace(forbiddenCharacters, '-');
}
else {
displayName = accessoryUUID;
}
let found = false;
for (const uuid in accessoryNameMap) {
if (accessoryNameMap.hasOwnProperty(uuid) && accessoryNameMap[uuid] === displayName) {
found = true;
break;
}
}
if (found) {
displayName = displayName + '-' + accessoryUUID;
}
accessoryNameMap[accessoryUUID] = displayName;
return displayName;
};
const serviceTypeMap = {};
serviceTypeMap[accTypes.LIGHTBULB_STYPE] = 'Lightbulb';
serviceTypeMap[accTypes.SWITCH_STYPE] = 'Switch';
serviceTypeMap[accTypes.THERMOSTAT_STYPE] = 'Thermostat';
serviceTypeMap[accTypes.GARAGE_DOOR_OPENER_STYPE] = 'Garage-Door-Opener';
serviceTypeMap[accTypes.ACCESSORY_INFORMATION_STYPE] = 'Accessory-Information';
serviceTypeMap[accTypes.FAN_STYPE] = 'Fan';
serviceTypeMap[accTypes.OUTLET_STYPE] = 'Outlet';
serviceTypeMap[accTypes.LOCK_MECHANISM_STYPE] = 'Lock-Mechanism';
serviceTypeMap[accTypes.LOCK_MANAGEMENT_STYPE] = 'Lock-Management';
serviceTypeMap[accTypes.ALARM_STYPE] = 'Alarm';
serviceTypeMap[accTypes.WINDOW_COVERING_STYPE] = 'Window-Covering';
serviceTypeMap[accTypes.OCCUPANCY_SENSOR_STYPE] = 'Occupancy-Sensor';
serviceTypeMap[accTypes.CONTACT_SENSOR_STYPE] = 'Contact-Sensor';
serviceTypeMap[accTypes.MOTION_SENSOR_STYPE] = 'Motion-Sensor';
serviceTypeMap[accTypes.HUMIDITY_SENSOR_STYPE] = 'Humidity-Sensor';
serviceTypeMap[accTypes.TEMPERATURE_SENSOR_STYPE] = 'Temperature-Sensor';
module.mapServiceType = function (serviceUUID, displayName) {
if (displayName) {
displayName = displayName.replace(forbiddenCharacters, '-');
return displayName;
}
if (serviceTypeMap[serviceUUID]) {
return serviceTypeMap[serviceUUID];
}
const pos = serviceUUID.indexOf('-0000-1000-8000-0026BB765291');
if (pos !== -1) {
return serviceUUID.substring(0, pos);
}
return serviceUUID;
};
const characteristicTypeMap = {};
characteristicTypeMap[accTypes.ALARM_CURRENT_STATE_CTYPE] = {name: 'Current-Alarm-State', roleDetail: '.alarm'};
characteristicTypeMap[accTypes.ALARM_TARGET_STATE_CTYPE] = {name: 'Target-Alarm-State', roleDetail: ''};
characteristicTypeMap[accTypes.ADMIN_ONLY_ACCESS_CTYPE] = {name: 'Admin-Only-Access', roleDetail: ''};
characteristicTypeMap[accTypes.AUDIO_FEEDBACK_CTYPE] = {name: 'Audio-Feedback', roleDetail: ''};
characteristicTypeMap[accTypes.BRIGHTNESS_CTYPE] = {name: 'Brightness', roleDetail: '.brightness'};
characteristicTypeMap[accTypes.BATTERY_LEVEL_CTYPE] = {name: 'Battery-Level', roleDetail: '.battery'};
characteristicTypeMap[accTypes.COOLING_THRESHOLD_CTYPE] = {name: 'Cooling-Threshold', roleDetail: '.temperature'};
characteristicTypeMap[accTypes.CONTACT_SENSOR_STATE_CTYPE] = {name: 'Contact-Sensor-State', roleDetail: '.window'}; //??
characteristicTypeMap[accTypes.CURRENT_DOOR_STATE_CTYPE] = {name: 'Current-Door-State', roleDetail: '.door'};
characteristicTypeMap[accTypes.CURRENT_LOCK_MECHANISM_STATE_CTYPE] = {name: 'Current-Lock-Mechanism-State', roleDetail: '.lock'};
characteristicTypeMap[accTypes.CURRENT_RELATIVE_HUMIDITY_CTYPE] = {name: 'Current-Relative-Humidity', roleDetail: '.humidity'};
characteristicTypeMap[accTypes.CURRENT_TEMPERATURE_CTYPE] = {name: 'Current-Temperature', roleDetail: '.temperature'};
characteristicTypeMap[accTypes.HEATING_THRESHOLD_CTYPE] = {name: 'Heating-Threshold', roleDetail: '.temperature'};
characteristicTypeMap[accTypes.HUE_CTYPE] = {name: 'Hue', roleDetail: ''};
characteristicTypeMap[accTypes.IDENTIFY_CTYPE] = {name: 'Identify', roleDetail: ''};
characteristicTypeMap[accTypes.LOCK_MANAGEMENT_AUTO_SECURE_TIMEOUT_CTYPE] = {name: 'Auto-Secure-Timeout', roleDetail: ''};
characteristicTypeMap[accTypes.LOCK_MANAGEMENT_CONTROL_POINT_CTYPE] = {name: 'Control-Point', roleDetail: ''};
characteristicTypeMap[accTypes.LOCK_MECHANISM_LAST_KNOWN_ACTION_CTYPE] = {name: 'Last-Known-Action', roleDetail: ''};
characteristicTypeMap[accTypes.LOGS_CTYPE] = {name: 'Logs', roleDetail: ''};
characteristicTypeMap[accTypes.MANUFACTURER_CTYPE] = {name: 'Manufacturer', roleDetail: ''};
characteristicTypeMap[accTypes.MODEL_CTYPE] = {name: 'Model', roleDetail: ''};
characteristicTypeMap[accTypes.MOTION_DETECTED_CTYPE] = {name: 'Motion-Detected', roleDetail: '.motion'};
characteristicTypeMap[accTypes.NAME_CTYPE] = {name: 'Name', roleDetail: ''};
characteristicTypeMap[accTypes.OBSTRUCTION_DETECTED_CTYPE] = {name: 'Obstruction-Detected', roleDetail: ''};
characteristicTypeMap[accTypes.OUTLET_IN_USE_CTYPE] = {name: 'Outlet-In-Use', roleDetail: 'indicator.working'};
characteristicTypeMap[accTypes.OCCUPANCY_DETECTED_CTYPE] = {name: 'Occupancy-Detected', roleDetail: ''};
characteristicTypeMap[accTypes.POWER_STATE_CTYPE] = {name: 'Power-State', roleDetail: ''};
characteristicTypeMap[accTypes.PROGRAMMABLE_SWITCH_SWITCH_EVENT_CTYPE] = {name: 'Switch-Event-Programmable-Switch', roleDetail: ''};
characteristicTypeMap[accTypes.PROGRAMMABLE_SWITCH_OUTPUT_STATE_CTYPE] = {name: 'Output-State-Programmable-Switch', roleDetail: ''};
characteristicTypeMap[accTypes.ROTATION_DIRECTION_CTYPE] = {name: 'Rotation-Direction', roleDetail: '.direction'};
characteristicTypeMap[accTypes.ROTATION_SPEED_CTYPE] = {name: 'Rotation-Speed', roleDetail: ''};
characteristicTypeMap[accTypes.SATURATION_CTYPE] = {name: 'Saturation', roleDetail: '.color.saturation'};
characteristicTypeMap[accTypes.SERIAL_NUMBER_CTYPE] = {name: 'Serial-Number', roleDetail: ''};
characteristicTypeMap[accTypes.STATUS_LOW_BATTERY_CTYPE] = {name: 'Low-Battery', roleDetail: '.lowbat'};
characteristicTypeMap[accTypes.STATUS_FAULT_CTYPE] = {name: 'Fault', roleDetail: ''};
characteristicTypeMap[accTypes.TARGET_DOORSTATE_CTYPE] = {name: 'Target-Doorstate', roleDetail: '.door'};
characteristicTypeMap[accTypes.TARGET_LOCK_MECHANISM_STATE_CTYPE] = {name: 'Target-Lock-Mechanism-State', roleDetail: '.lock'};
characteristicTypeMap[accTypes.TARGET_RELATIVE_HUMIDITY_CTYPE] = {name: 'Target-Relative-Humidity', roleDetail: '.humidity'};
characteristicTypeMap[accTypes.TARGET_TEMPERATURE_CTYPE] = {name: 'Target-Temperature', roleDetail: '.temperature'};
characteristicTypeMap[accTypes.TEMPERATURE_UNITS_CTYPE] = {name: 'Temperature-Units', roleDetail: ''};
characteristicTypeMap[accTypes.VERSION_CTYPE] = {name: 'Version', roleDetail: ''};
characteristicTypeMap[accTypes.WINDOW_COVERING_TARGET_POSITION_CTYPE] = {name: 'Target-Position-Window-Covering', roleDetail: '.blind'};
characteristicTypeMap[accTypes.WINDOW_COVERING_CURRENT_POSITION_CTYPE] = {name: 'Current-Position-Window-Covering', roleDetail: '.blind'};
characteristicTypeMap[accTypes.WINDOW_COVERING_OPERATION_STATE_CTYPE] = {name: 'Operation-State-Window-Covering', roleDetail: '.working'};
characteristicTypeMap[accTypes.CURRENTHEATINGCOOLING_CTYPE] = {name: 'Current-Heating-Cooling', roleDetail: ''};
characteristicTypeMap[accTypes.TARGETHEATINGCOOLING_CTYPE] = {name: 'Target-Heating-Cooling', roleDetail: ''};
module.mapCharacteristicType = function (serviceUUID, charUUID, displayName) {
if (displayName) {
displayName = displayName.replace(forbiddenCharacters, '-');
return displayName;
}
if (characteristicTypeMap[charUUID]) return characteristicTypeMap[charUUID].name;
const pos = charUUID.indexOf("-0000-1000-8000-0026BB765291");
if (pos !== -1) return charUUID.substring(0, pos);
return charUUID;
};
const characteristicFormats = {
'bool': 'boolean',
'int': 'number',
'float': 'number',
'string': 'string',
'uint8': 'number',
'uint16': 'number',
'uint32': 'number',
'uint64': 'number',
'data': 'string',
'tlv8': 'string',
'array': 'string', //Not in HAP Spec
'dict': 'string' //Not in HAP Spec
};
// Known HomeKit unit types
const characteristicUnits = {
// HomeKit only defines Celsius, for Fahrenheit, it requires iOS app to do the conversion.
'celsius': '°C',
'percentage': '%',
'arcdegrees': '°',
'lux': 'lx',
'seconds': 's'
};
module.mapCharacteristicProperties = function (char) {
const common = {};
if (characteristicFormats[char.props.format]) {
common.type = characteristicFormats[char.props.format];
}
else common.type = 'string';
if (characteristicUnits[char.props.unit]) {
common.unit = characteristicUnits[char.props.unit];
}
else if (char.props.unit) {
common.unit = char.props.unit;
}
if (char.props.minValue !== null && char.props.minValue !== undefined) {
common.min = char.props.minValue;
}
if (char.props.maxValue !== null && char.props.maxValue !== undefined) {
common.max = char.props.maxValue;
}
if (common.min === undefined || common.max === undefined) {
switch(char.props.format) {
case 'int':
if (common.min === undefined) common.min = -2147483648;
if (common.max === undefined) common.max = 2147483647;
break;
case 'uint8':
if (common.min === undefined) common.min = 0;
if (common.max === undefined) common.max = 255;
break;
case 'uint16':
if (common.min === undefined) common.min = 0;
if (common.max === undefined) common.max = 65535;
break;
case 'uint32':
if (common.min === undefined) common.min = 0;
if (common.max === undefined) common.max = 4294967295;
break;
case 'uint64':
if (common.min === undefined) common.min = 0;
if (common.max === undefined) common.max = 18446744073709551615;
break;
}
}
if (char.validValues && char.validValues.length > 0 && common.type !== 'boolean') {
common.states = {};
for (let i = 0; i < char.validValues; i++) {
common.states[char.validValues[i]] = char.validValues[i];
}
}
common.read = (char.props.perms.indexOf('pr') !== -1);
common.write = (char.props.perms.indexOf('pw') !== -1);
if (!common.read && !common.write) {
common.read = true;
}
let roleDetail = null;
if (characteristicTypeMap[char.UUID]) {
roleDetail = characteristicTypeMap[char.UUID].roleDetail;
}
// Try to set roles
let role = '';
if (common.type === 'boolean') {
if (common.read && !common.write) { // Boolean, read-only --> Sensor OR Indicator!
role = 'sensor';
}
else if (common.write && !common.read) { // Boolean, write-only --> Button
role = 'button';
}
else if (common.read && common.write) { // Boolean, read-write --> Switch
role = 'switch';
}
}
else if (common.type === 'number') {
if (common.read && !common.write) { // Number, read-only --> Value
role = 'value';
}
else if (common.write && !common.read) { // Boolean, write-only --> ?? Level?
role = 'level';
}
else if (common.read && common.write) { // Number, read-write --> Level
role = 'level';
}
}
else if (common.type === 'string') {
role = 'text';
}
if (roleDetail && roleDetail !== '' && roleDetail.indexOf('.') === 0 && role !== '') {
role += roleDetail;
}
else if (roleDetail && roleDetail !== '' && roleDetail.indexOf('.') === -1) {
role = roleDetail;
}
if (role !== '') common.role = role;
if (!common.role) common.role = 'state';
return common;
};
return module;
};

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 = [
'yunkong2.js-controller',
'yunkong2.js-controller',
];
/** @type {string} */
let controllerPath;
for (const pkg of possibilities) {
try {
const possiblePath = require.resolve(pkg);
if (fs.existsSync(possiblePath)) {
controllerPath = possiblePath;
break;
}
} catch (e) { /* not found */ }
}
if (!controllerPath) {
if (!isInstall) {
console.log('Cannot find js-controller');
process.exit(10);
} else {
process.exit();
}
}
// we found the controller
return path.dirname(controllerPath);
}
/**
* reads controller base settings
*
* @alias getConfig
* @returns {object}
*/
function getConfig() {
let configPath;
if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', appName + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else {
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
}
}
appName = getAppName();
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
const adapter = require(path.join(controllerDir, 'lib/adapter.js'));
exports.controllerDir = controllerDir;
exports.getConfig = getConfig;
exports.Adapter = adapter;
exports.appName = appName;

148
lib/wrapper-handler.js Normal file
View File

@@ -0,0 +1,148 @@
/* jshint strict: false */
/* jslint node: true */
/* jslint esversion: 6 */
'use strict';
const HomebridgeWrapper = require('homebridge-plugin-wrapper').Wrapper;
let homebridgeWrapper;
const charMap = {};
let logger;
let updateState;
let updateDev;
let updateChannel;
let setState;
let mapper;
let ignoreInfoAccessoryServices;
function customStringify(v, func, intent) {
const cache = new Map();
return JSON.stringify(v, function (key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.get(value)) {
// Circular reference found, discard key
return;
}
// Store value in our map
cache.set(value, true);
}
return value;
}, intent);
}
function init(config) {
logger = config.logger;
updateState = config.updateState;
updateDev = config.updateDev;
updateChannel = config.updateChannel;
setState = config.setState;
ignoreInfoAccessoryServices = config.ignoreInfoAccessoryServices;
mapper = require('./mapper')(config);
homebridgeWrapper = new HomebridgeWrapper(config);
homebridgeWrapper.on('characteristic-value-change', data => {
logger.debug('Char change event: ' + data.oldValue + ' --> ' + data.newValue);
handleCharValue(data.accessory, data.service, data.characteristic, data.newValue);
});
homebridgeWrapper.on('addAccessory', accessory => {
function iterateCharArray(chars, dev_idname, sr_id, sr_idname) {
for (const chindex in chars) {
if (!chars.hasOwnProperty(chindex)) continue;
const char = chars[chindex];
const ch_id = char.UUID;
const ch_name = char.displayName;
const ch_val = char.value;
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, ch_name);
const id = dev_idname + '.' + sr_idname + '.' + ch_idname;
const common = mapper.mapCharacteristicProperties(char);
logger.debug('Mapped Common for ' + id + ': ' + JSON.stringify(common));
if (common.write) {
charMap[id] = char; // TODO only if write allowed!
logger.silly('Add object to charmap with id ' + id + '/' + customStringify(char));
}
updateState(dev_idname, sr_idname, ch_idname, ch_name, ch_val, common, ch_id);
}
}
logger.debug('yunkong2.ham Bridge addBridgedAccessory ' + customStringify(accessory)); //OK
// Новое устройство
// New device
const dev_id = accessory.UUID;
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
const dev_name = accessory.displayName;
const dev_cat = accessory.category;
updateDev(dev_idname, dev_name, dev_cat, dev_id);
for (const index in accessory.services) {
if (!accessory.services.hasOwnProperty(index)) continue;
const service = accessory.services[index];
const sr_id = service.UUID;
const sr_idname = mapper.mapServiceType(sr_id, service.displayName);
const sr_name = service.displayName;
if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') {
continue;
}
logger.silly('Add service class=' + customStringify(service));
updateChannel(dev_idname, sr_idname, sr_name, sr_id);
iterateCharArray(service.characteristics, dev_idname, sr_id, sr_idname);
if (service.optionalCharacteristics) {
iterateCharArray(service.optionalCharacteristics, dev_idname, sr_id, sr_idname);
}
}
});
}
function registerExistingAccessory(UUID, name) {
mapper.mapAccessoryUUID(UUID, name);
}
function start() {
homebridgeWrapper.init();
}
function end() {
homebridgeWrapper.finish();
}
function setValueForCharId(id, value) {
if (charMap[id]) {
logger.debug('set value ' + value + ' of char for ' + id);
charMap[id].setValue(value);
}
}
function handleCharValue(accessory, serv, char, newValue){
const sr_id = serv.UUID;
const sr_idname = mapper.mapServiceType(sr_id, serv.displayName);
const ch_id = char.UUID;
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, char.displayName);
const dev_id = accessory.UUID;
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
const value = newValue;
logger.debug('handleCharValue = ' + newValue);
logger.silly('characteristic = ' + customStringify(char));
logger.silly('accessory =' + customStringify(accessory));
if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') {
return;
}
setState(dev_idname, sr_idname, ch_idname, value);
}
exports.init = init;
exports.end = end;
exports.setValueForCharId = setValueForCharId;
exports.start = start;
exports.registerExistingAccessory = registerExistingAccessory;

300
main.js Normal file
View File

@@ -0,0 +1,300 @@
/* jshint -W097 */
/* jshint -W030 */
/* jshint strict: false */
/* jslint node: true */
/* jslint esversion: 6 */
'use strict';
const nodeFS = require('fs');
const child_process = require('child_process');
// you have to require the utils module and call adapter function
const utils = require(__dirname + '/lib/utils'); // Get common adapter utils
const path = require('path');
// it is not an object.
function createHam(options) {
const dataDir = path.normalize(path.join(utils.controllerDir, require(path.join(utils.controllerDir, 'lib', 'tools.js')).getDefaultDataDir()));
// you have to call the adapter function and pass a options object
// name has to be set and has to be equal to adapters folder name and main file name excluding extension
// adapter will be restarted automatically every time as the configuration changed, e.g system.adapter.template.0
const adapter = new utils.Adapter(options);
let homebridgeHandler;
const attempts = {};
// is called when adapter shuts down - callback has to be called under any circumstances!
adapter.on('unload', callback => {
try {
adapter.log.info('cleaned everything up...');
homebridgeHandler.end();
callback();
} catch (e) {
callback();
}
});
process.on('SIGINT', () => homebridgeHandler.end());
process.on('SIGTERM', () => homebridgeHandler.end());
process.on('uncaughtException', err => {
if (adapter && adapter.log) {
adapter.log.warn('Exception: ' + err);
}
homebridgeHandler.end();
});
// is called if a subscribed state changes
adapter.on('stateChange', (id, state) => {
// Warning, state can be null if it was deleted
adapter.log.info('stateChange ' + id + ' ' + JSON.stringify(state));
id = id.substr(adapter.namespace.length+1);
adapter.log.debug('lookup id: ' + id);
// you can use the ack flag to detect if it is status (true) or command (false)
if (state && !state.ack) {
adapter.log.debug('ack is not set!');
homebridgeHandler.setValueForCharId(id, state.val);
}
});
function updateDev(dev_id, dev_name, dev_type, dev_uuid) {
adapter.log.info('updateDev ' + dev_id + ': name = ' + dev_name + ' /type= ' + dev_type);
// create dev
adapter.getObject(dev_id, (err, obj) => {
if (!err && obj) {
adapter.extendObject(dev_id, {
type: 'device',
common: {name: dev_name},
native: {
UUID: dev_uuid,
displayName: dev_name,
category: dev_type
}
});
}
else {
adapter.setObject(dev_id, {
type: 'device',
common: {name: dev_name},
native: {
UUID: dev_uuid,
displayName: dev_name,
category: dev_type
}
}, {});
}
});
}
function updateChannel(dev_id, ch_id, name, ch_uuid) {
const id = dev_id + '.' + ch_id;
// create channel for dev
adapter.log.info('updateChannel ' + id + ': name = ' + name);
adapter.getObject(id, (err, obj) => {
if (!err && obj) {
adapter.extendObject(id, {
type: 'channel',
common: {name: name},
native: {
UUID: ch_uuid,
displayName: name
}
});
}
else {
adapter.setObject(id, {
type: 'channel',
common: {name: name},
native: {
UUID: ch_uuid,
displayName: name
}
}, {});
}
});
}
function updateState(dev_id, ch_id, st_id, name, value, common, st_uuid, callback) {
const id = dev_id + '.' + ch_id + '.'+ st_id;
if (!common) common = {};
if (common.name === undefined) common.name = name;
if (common.role === undefined) common.role = 'state';
if (common.read === undefined && common.write === undefined) common.read = true;
if (common.type === undefined) common.type = 'string';
if (common.unit === undefined) common.unit = '';
adapter.log.info('updateState ' + id + ': value = ' + value + ' /common= ' + JSON.stringify(common));
adapter.getObject(id, (err, obj) => {
if (!err && obj) {
adapter.extendObject(id, {
type: 'state',
common: common,
native: {
UUID: st_uuid,
displayName: name
}
}, callback);
}
else {
adapter.setObject(id, {
type: 'state',
common: common,
native: {
UUID: st_uuid,
displayName: name
}
}, callback);
}
});
}
function setState(dev_id, ch_id, st_id, value) {
const id = dev_id + '.' + ch_id + '.' + st_id;
adapter.setState(id, value, true);
}
// is called when databases are connected and adapter received configuration.
// start here!
adapter.on('ready', main);
function loadExistingAccessories(callback) {
adapter.getDevices((err, res) => {
if (err) {
adapter.log.error('Can not get all existing devices: ' + err);
return;
}
for (let i = 0; i < res.length; i++) {
if (res[i].native && res[i].native.UUID) {
adapter.log.debug('Remember existing Accessory ' + res[i].native.displayName + ' with UUID ' + res[i].native.UUID);
homebridgeHandler.registerExistingAccessory(res[i].native.UUID, res[i].native.displayName);
}
}
if (callback) callback();
});
}
//Catch Homebridge Console Logging
if (process.argv.indexOf('--logs') === -1 && process.argv.indexOf('-l') === -1) {
console.log = function (logs) {
if (adapter && adapter.log && adapter.log.debug) {
adapter.log.debug(logs);
}
process.stdout.write(logs + '\n');
};
}
function main() {
const usedLogger = {
info: adapter.log.debug,
warn: adapter.log.warn,
debug: adapter.log.silly,
silly: adapter.log.silly
};
if (adapter.config.useGlobalHomebridge) {
homebridgeHandler = require('./lib/global-handler');
homebridgeHandler.init({
logger: usedLogger,
homebridgeBasePath: adapter.config.globalHomebridgeBasePath,
homebridgeConfigPath: adapter.config.globalHomebridgeConfigPath,
updateDev: updateDev,
updateChannel: updateChannel,
updateState: updateState,
setState: setState,
ignoreInfoAccessoryServices: adapter.config.ignoreInfoAccessoryServices
});
}
else {
homebridgeHandler = require('./lib/wrapper-handler');
homebridgeHandler.init({
logger: usedLogger,
homebridgeConfigPath: dataDir + adapter.namespace.replace('.', '_'),
updateDev: updateDev,
updateChannel: updateChannel,
updateState: updateState,
setState: setState,
wrapperConfig: adapter.config.wrapperConfig,
ignoreInfoAccessoryServices: adapter.config.ignoreInfoAccessoryServices,
characteristicPollingInterval: adapter.config.characteristicPollingInterval * 1000
});
}
// in this template all states changes inside the adapters namespace are subscribed
adapter.subscribeStates('*');
installLibraries(() => {
loadExistingAccessories(() => {
homebridgeHandler.start();
options.exitAfter && setTimeout(() => adapter && adapter.stop(), 10000);
});
});
}
function installNpm(npmLib, callback) {
const path = __dirname;
if (typeof npmLib === 'function') {
callback = npmLib;
npmLib = undefined;
}
const cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '"';
adapter.log.info(cmd + ' (System call)');
// Install node modules as system call
// System call used for update of js-controller itself,
// because during installation npm packet will be deleted too, but some files must be loaded even during the install process.
const child = child_process.exec(cmd);
child.stdout.on('data', buf => adapter.log.info(buf.toString('utf8')));
child.stderr.on('data', buf => adapter.log.error(buf.toString('utf8')));
child.on('exit', (code /* , signal */) => {
if (code) {
adapter.log.error('Cannot install ' + npmLib + ': ' + code);
}
// command succeeded
if (typeof callback === 'function') callback(npmLib);
});
}
function installLibraries(callback) {
let allInstalled = true;
if (adapter.config && adapter.config.libraries && !adapter.config.useGlobalHomebridge) {
const libraries = adapter.config.libraries.split(/[,;\s]+/);
for (let lib = 0; lib < libraries.length; lib++) {
if (libraries[lib] && libraries[lib].trim()) {
libraries[lib] = libraries[lib].trim();
if (!nodeFS.existsSync(__dirname + '/node_modules/' + libraries[lib] + '/package.json')) {
if (!attempts[libraries[lib]]) {
attempts[libraries[lib]] = 1;
} else {
attempts[libraries[lib]]++;
}
if (attempts[libraries[lib]] > 3) {
adapter.log.error('Cannot install npm packet: ' + libraries[lib]);
continue;
}
installNpm(libraries[lib], () => installLibraries(callback));
allInstalled = false;
break;
}
}
}
}
if (allInstalled) callback();
}
return adapter;
}
if (!module || !module.parent) {
createHam('ham');
} else {
module.exports = createHam;
}

48
package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"version": "0.4.4",
"name": "yunkong2.ham",
"author": {
"name": "Apollon77",
"email": "ingo@fischer-ka.de"
},
"bugs": {
"url": "https://github.com/yunkong2/yunkong2.ham/issues"
},
"contributors": [
{
"name": "Apollon77",
"email": "ingo@fischer-ka.de"
},
{
"name": "Kirov Ilya",
"email": "kirovilya@gmail.com"
}
],
"dependencies": {
"homebridge-plugin-wrapper": "^0.5.2"
},
"description": "Homebridge accessories manager",
"devDependencies": {
"chai": "^4.1.2",
"gulp": "^3.9.1",
"mocha": "^5.2.0"
},
"optionalDependencies": {},
"homepage": "https://github.com/yunkong2/yunkong2.ham",
"keywords": [
"yunkong2",
"Homebridge",
"Smart Home",
"home automation"
],
"license": "MIT",
"main": "main.js",
"readmeFilename": "README.md",
"repository": {
"type": "git",
"url": "git+https://github.com/yunkong2/yunkong2.ham.git"
},
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit"
}
}

133
test/homebridge/config.json Normal file
View File

@@ -0,0 +1,133 @@
{
"bridge": {
"name": "Test-Bridge",
"username": "EE:22:3D:E3:CE:30",
"port": 61826,
"pin": "031-45-156"
},
"description": "This is an example configuration file with one fake accessory and one fake platform. You can use this as a template for creating your own configuration file containing devices you actually own.",
"accessories": [
{
"accessory" : "SunPosition",
"name" : "Sun",
"location" : {
"lat" : 49.035924,
"long" : 8.345736
}
}
],
"platforms": [
{
"platform": "HttpWebHooks",
"webhook_port": "61828",
"cache_directory": "./.node-persist/storage",
"sensors": [
{
"id": "sensor1",
"name": "Sensor name 1",
"type": "contact"
},
{
"id": "sensor2",
"name": "Sensor name 2",
"type": "motion"
},
{
"id": "sensor3",
"name": "Sensor name 3",
"type": "occupancy"
},
{
"id": "sensor4",
"name": "Sensor name 4",
"type": "smoke"
},
{
"id": "sensor5",
"name": "Sensor name 5",
"type": "temperature"
},
{
"id": "sensor6",
"name": "Sensor name 6",
"type": "humidity"
},
{
"id": "sensor7",
"name": "Sensor name 7",
"type": "airquality"
},
{
"id": "sensor8",
"name": "Sensor name 8",
"type": "airquality"
}
],
"switches": [
{
"id": "switch1",
"name": "Switch name 1",
"on_url": "http://localhost:9080/switch1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch1?off",
"off_method": "GET"
},
{
"id": "switch2",
"name": "Switch name 2",
"on_url": "http://localhost:9080/switch2?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch2?off",
"off_method": "GET"
},
{
"id": "switch3",
"name": "Switch name 3",
"on_url": "http://localhost:9080/switch3?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch3?off",
"off_method": "GET"
}
],
"pushbuttons": [
{
"id": "pushbutton1",
"name": "Push button name 1",
"push_url": "http://localhost:9080/pushbutton1?push",
"push_method": "GET"
}
],
"lights": [
{
"id": "light1",
"name": "Light name 1",
"on_url": "http://localhost:9080/light1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/light1?off",
"off_method": "GET"
}
],
"thermostats": [
{
"id": "thermostat1",
"name": "Thermostat name 1",
"set_target_temperature_url": "http://localhost:9080/thermostat1?targettemperature=%f",
"set_target_heating_cooling_state_url": "http://localhost:9080/thermostat1??targetstate=%b"
}
],
"outlets": [
{
"id": "outlet1",
"name": "Outlet name 1",
"on_url": "http://localhost:9080/outlet1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/outlet1?off",
"off_method": "GET"
}
]
}
]
}

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

View File

@@ -0,0 +1,365 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
const expect = require('chai').expect;
const setup = require(__dirname + '/lib/setup');
const request = require('request');
const http = require('http');
const fs = require('fs');
let objects = null;
let states = null;
let onStateChanged = null;
let onObjectChanged = null;
let sendToID = 1;
const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
let httpServer;
let lastHTTPRequest = null;
function setupHTTPServer(port, callback) {
httpServer = http.createServer((req, res) => {
lastHTTPRequest = req.url;
console.log('HTTP Received: ' + lastHTTPRequest);
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('OK');
}).listen(port);
setTimeout(() => callback(), 5000);
}
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', (err, state) => {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(() => checkConnectionOfAdapter(cb, counter + 1), 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, (err, state) => {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = (id, state) => (id === 'messagebox.system.adapter.test.0') && callback(state.message);
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: Date.now()
}
});
}
describe('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo', () => {
before('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(() => {
const config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
config.native.useGlobalHomebridge = false;
config.native.globalHomebridgeConfigPath = __dirname + "/homebridge/";
config.native.libraries = "homebridge-http-webhooks homebridge-sun-position";
config.native.ignoreInfoAccessoryServices = true;
config.native.characteristicPollingInterval = 30;
config.native.wrapperConfig = {
"accessories": [
{
"accessory" : "SunPosition",
"name" : "Sun",
"location" : {
"lat" : 49.035924,
"long" : 8.345736
}
}
],
"platforms": [
{
"platform": "HttpWebHooks",
"webhook_port": "61828",
"cache_directory": "./.node-persist/storage",
"sensors": [
{
"id": "sensor1",
"name": "Sensor name 1",
"type": "contact"
},
{
"id": "sensor2",
"name": "Sensor name 2",
"type": "motion"
},
{
"id": "sensor3",
"name": "Sensor name 3",
"type": "occupancy"
},
{
"id": "sensor4",
"name": "Sensor name 4",
"type": "smoke"
},
{
"id": "sensor5",
"name": "Sensor name 5",
"type": "temperature"
},
{
"id": "sensor6",
"name": "Sensor name 6",
"type": "humidity"
},
{
"id": "sensor7",
"name": "Sensor name 7",
"type": "airquality"
},
{
"id": "sensor8",
"name": "Sensor name 8",
"type": "airquality"
}
],
"switches": [
{
"id": "switch1",
"name": "Switch name 1",
"on_url": "http://localhost:9080/switch1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch1?off",
"off_method": "GET"
},
{
"id": "switch2",
"name": "Switch name 2",
"on_url": "http://localhost:9080/switch2?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch2?off",
"off_method": "GET"
},
{
"id": "switch3",
"name": "Switch name 3",
"on_url": "http://localhost:9080/switch3?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch3?off",
"off_method": "GET"
},
{
"id": "switch4",
"name": "Switch name*3",
"on_url": "http://localhost:9080/switch3-2?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch3-2?off",
"off_method": "GET"
}
],
"pushbuttons": [
{
"id": "pushbutton1",
"name": "Push button name 1",
"push_url": "http://localhost:9080/pushbutton1?push",
"push_method": "GET"
}
],
"lights": [
{
"id": "light1",
"name": "Light name 1",
"on_url": "http://localhost:9080/light1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/light1?off",
"off_method": "GET"
}
],
"thermostats": [
{
"id": "thermostat1",
"name": "Thermostat name 1",
"set_target_temperature_url": "http://localhost:9080/thermostat1?targettemperature=%f",
"set_target_heating_cooling_state_url": "http://localhost:9080/thermostat1??targetstate=%b"
}
],
"outlets": [
{
"id": "outlet1",
"name": "Outlet name 1",
"on_url": "http://localhost:9080/outlet1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/outlet1?off",
"off_method": "GET"
}
]
}
]
};
setup.setAdapterConfig(config.common, config.native);
setupHTTPServer(9080, () => {
setup.startController(
true,
(id, obj) => {},
(id, state) => {
if (onStateChanged) onStateChanged(id, state);
},
(_objects, _states) => {
objects = _objects;
states = _states;
_done();
});
});
});
});
it('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Check if adapter started', done => {
checkConnectionOfAdapter(res => {
if (res) console.log(res);
expect(res).not.to.be.equal('Cannot check connection');
objects.setObject('system.adapter.test.0', {
common: {
},
type: 'instance'
},
() => {
states.subscribeMessage('system.adapter.test.0');
done();
});
});
}).timeout(60000);
it('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Wait for npm installs', done => {
setTimeout(() => done(), 30000);
}).timeout(60000);
it('Test ' + adapterShortName + ' Wrapper: Verify Init', done => {
expect(fs.existsSync(__dirname + '/homebridge/config.json')).to.be.true;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
states.getState(adapterShortName + '.0.Sun.Accessory-Information.Model', (err, state) => {
expect(err).to.be.null;
expect(state).to.be.undefined;
states.getState(adapterShortName + '.0.Sun.Sun.Altitude', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.exist;
done();
});
});
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test Change from inside', done => {
request('http://localhost:61828/?accessoryId=switch1&state=true', (error, response, body) => {
expect(error).to.be.null;
expect(response && response.statusCode).to.be.equal(200);
setTimeout(function() {
expect(lastHTTPRequest).to.be.null;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.true;
done();
});
}, 2000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic', done => {
states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: false, ack:false}, function (err) {
expect(err).to.not.exist;
setTimeout(function() {
expect(lastHTTPRequest).to.be.equal('/switch1?off');
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
done();
});
}, 2000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic 2', done => {
states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: true, ack:false}, function (err) {
expect(err).to.not.exist;
setTimeout(function() {
expect(lastHTTPRequest).to.be.equal('/switch1?on');
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.true;
done();
});
}, 2000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test Change from inside 2', done => {
lastHTTPRequest = null;
request('http://localhost:61828/?accessoryId=switch1&state=false', (error, response, body) => {
expect(error).to.be.null;
expect(response && response.statusCode).to.be.equal(200);
setTimeout(function() {
expect(lastHTTPRequest).to.be.null;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
setTimeout(() => done(), 40000);
});
}, 2000);
});
}).timeout(50000);
after('Test ' + adapterShortName + ' Wrapper adapter No-AccessoryInfo: Stop js-controller', function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
httpServer.close();
done();
});
});
});

View File

@@ -0,0 +1,364 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
const expect = require('chai').expect;
const setup = require(__dirname + '/lib/setup');
const request = require('request');
const http = require('http');
const fs = require('fs');
let objects = null;
let states = null;
let onStateChanged = null;
let onObjectChanged = null;
let sendToID = 1;
const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
let httpServer;
let lastHTTPRequest = null;
function setupHTTPServer(port, callback) {
httpServer = http.createServer((req, res) => {
lastHTTPRequest = req.url;
console.log('HTTP Received: ' + lastHTTPRequest);
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('OK');
}).listen(port);
setTimeout(() => callback(), 5000);
}
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', (err, state) => {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(() => checkConnectionOfAdapter(cb, counter + 1), 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, (err, state) => {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = (id, state) => (id === 'messagebox.system.adapter.test.0') && callback(state.message);
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: Date.now()
}
});
}
describe('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo', () => {
before('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(() => {
const config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
config.native.useGlobalHomebridge = false;
config.native.globalHomebridgeConfigPath = __dirname + "/homebridge/";
config.native.libraries = "homebridge-http-webhooks homebridge-sun-position";
config.native.ignoreInfoAccessoryServices = false;
config.native.wrapperConfig = {
"accessories": [
{
"accessory" : "SunPosition",
"name" : "Sun",
"location" : {
"lat" : 49.035924,
"long" : 8.345736
}
}
],
"platforms": [
{
"platform": "HttpWebHooks",
"webhook_port": "61828",
"cache_directory": "./.node-persist/storage",
"sensors": [
{
"id": "sensor1",
"name": "Sensor name 1",
"type": "contact"
},
{
"id": "sensor2",
"name": "Sensor name 2",
"type": "motion"
},
{
"id": "sensor3",
"name": "Sensor name 3",
"type": "occupancy"
},
{
"id": "sensor4",
"name": "Sensor name 4",
"type": "smoke"
},
{
"id": "sensor5",
"name": "Sensor name 5",
"type": "temperature"
},
{
"id": "sensor6",
"name": "Sensor name 6",
"type": "humidity"
},
{
"id": "sensor7",
"name": "Sensor name 7",
"type": "airquality"
},
{
"id": "sensor8",
"name": "Sensor name 8",
"type": "airquality"
}
],
"switches": [
{
"id": "switch1",
"name": "Switch name 1",
"on_url": "http://localhost:9080/switch1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch1?off",
"off_method": "GET"
},
{
"id": "switch2",
"name": "Switch name 2",
"on_url": "http://localhost:9080/switch2?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch2?off",
"off_method": "GET"
},
{
"id": "switch3",
"name": "Switch name 3",
"on_url": "http://localhost:9080/switch3?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch3?off",
"off_method": "GET"
},
{
"id": "switch4",
"name": "Switch name*3",
"on_url": "http://localhost:9080/switch3-2?on",
"on_method": "GET",
"off_url": "http://localhost:9080/switch3-2?off",
"off_method": "GET"
}
],
"pushbuttons": [
{
"id": "pushbutton1",
"name": "Push button name 1",
"push_url": "http://localhost:9080/pushbutton1?push",
"push_method": "GET"
}
],
"lights": [
{
"id": "light1",
"name": "Light name 1",
"on_url": "http://localhost:9080/light1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/light1?off",
"off_method": "GET"
}
],
"thermostats": [
{
"id": "thermostat1",
"name": "Thermostat name 1",
"set_target_temperature_url": "http://localhost:9080/thermostat1?targettemperature=%f",
"set_target_heating_cooling_state_url": "http://localhost:9080/thermostat1??targetstate=%b"
}
],
"outlets": [
{
"id": "outlet1",
"name": "Outlet name 1",
"on_url": "http://localhost:9080/outlet1?on",
"on_method": "GET",
"off_url": "http://localhost:9080/outlet1?off",
"off_method": "GET"
}
]
}
]
};
setup.setAdapterConfig(config.common, config.native);
setupHTTPServer(9080, () => {
setup.startController(
true,
(id, obj) => {},
(id, state) => {
if (onStateChanged) onStateChanged(id, state);
},
(_objects, _states) => {
objects = _objects;
states = _states;
_done();
});
});
});
});
it('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Check if adapter started', done => {
checkConnectionOfAdapter(res => {
if (res) console.log(res);
expect(res).not.to.be.equal('Cannot check connection');
objects.setObject('system.adapter.test.0', {
common: {
},
type: 'instance'
},
() => {
states.subscribeMessage('system.adapter.test.0');
done();
});
});
}).timeout(60000);
it('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Wait for npm installs', done => {
setTimeout(() => done(), 30000);
}).timeout(60000);
it('Test ' + adapterShortName + ' Wrapper: Verify Init', done => {
expect(fs.existsSync(__dirname + '/homebridge/config.json')).to.be.true;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
states.getState(adapterShortName + '.0.Sun.Accessory-Information.Model', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.equal('Sun Position');
states.getState(adapterShortName + '.0.Sun.Sun.Altitude', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.exist;
done();
});
});
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test Change from inside', done => {
request('http://localhost:61828/?accessoryId=switch1&state=true', (error, response, body) => {
expect(error).to.be.null;
expect(response && response.statusCode).to.be.equal(200);
setTimeout(function() {
expect(lastHTTPRequest).to.be.null;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.true;
done();
});
}, 2000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic', done => {
states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: false, ack:false}, function (err) {
expect(err).to.not.exist;
setTimeout(function() {
expect(lastHTTPRequest).to.be.equal('/switch1?off');
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
done();
});
}, 2000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic 2', done => {
states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: true, ack:false}, function (err) {
expect(err).to.not.exist;
setTimeout(function() {
expect(lastHTTPRequest).to.be.equal('/switch1?on');
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.true;
done();
});
}, 2000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test Change from inside 2', done => {
lastHTTPRequest = null;
request('http://localhost:61828/?accessoryId=switch1&state=false', (error, response, body) => {
expect(error).to.be.null;
expect(response && response.statusCode).to.be.equal(200);
setTimeout(function() {
expect(lastHTTPRequest).to.be.null;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
done();
});
}, 2000);
});
}).timeout(10000);
after('Test ' + adapterShortName + ' Wrapper adapter With-AccessoryInfo: Stop js-controller', function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
httpServer.close();
done();
});
});
});

233
test/testAdapterXGlobal.js Normal file
View File

@@ -0,0 +1,233 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
const expect = require('chai').expect;
const setup = require(__dirname + '/lib/setup');
const request = require('request');
const http = require('http');
let objects = null;
let states = null;
let onStateChanged = null;
let onObjectChanged = null;
let sendToID = 1;
const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
let httpServer;
let lastHTTPRequest = null;
function setupHTTPServer(port, callback) {
httpServer = http.createServer(function (req, res) {
lastHTTPRequest = req.url;
console.log('HTTP Received: ' + lastHTTPRequest);
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('OK');
}).listen(port);
setTimeout(function() {
callback();
}, 5000);
}
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', (err, state) => {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, (err, state) => {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = (id, state) => (id === 'messagebox.system.adapter.test.0') && callback(state.message);
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: Date.now()
}
});
}
describe('Test ' + adapterShortName + ' Global adapter', () => {
before('Test ' + adapterShortName + ' Global adapter: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(() => {
const config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
config.native.useGlobalHomebridge = true;
config.native.globalHomebridgeBasePath = process.env.NODE_GLOBAL_DIR + "/homebridge/";
config.native.globalHomebridgeConfigPath = __dirname + "/homebridge/";
setup.setAdapterConfig(config.common, config.native);
setupHTTPServer(9080, () => {
setup.startController(true,
(id, obj) => {},
(id, state) => onStateChanged && onStateChanged(id, state),
(_objects, _states) => {
objects = _objects;
states = _states;
_done();
});
});
});
});
/*
ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY
*/
it('Test ' + adapterShortName + ' Global adapter: Check if adapter started', done => {
checkConnectionOfAdapter(res => {
if (res) console.log(res);
expect(res).not.to.be.equal('Cannot check connection');
objects.setObject('system.adapter.test.0', {
common: {
},
type: 'instance'
},
() => {
states.subscribeMessage('system.adapter.test.0');
done();
});
});
}).timeout(60000);
it('Test ' + adapterShortName + ' Wrapper adapter: Wait for init', done => {
setTimeout(() => done(), 20000);
}).timeout(60000);
it('Test ' + adapterShortName + ' Wrapper: Verify Init', done => {
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
states.getState(adapterShortName + '.0.Sun.Accessory-Information.Model', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.equal('Sun Position');
states.getState(adapterShortName + '.0.Sun.Sun.Altitude', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.exist;
done();
});
});
});
});
it('Test ' + adapterShortName + ' Wrapper: Test Change from inside', done => {
request('http://localhost:61828/?accessoryId=switch1&state=true', (error, response, body) => {
expect(error).to.be.null;
expect(response && response.statusCode).to.be.equal(200);
setTimeout(function() {
expect(lastHTTPRequest).to.be.null;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.true;
done();
});
}, 3000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic', done => {
states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: false, ack: false}, err => {
expect(err).to.not.exist;
setTimeout(function() {
expect(lastHTTPRequest).to.be.equal('/switch1?off');
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
done();
});
}, 3000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test change via characteristic 2', done => {
states.setState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', {val: true, ack: false}, err => {
expect(err).to.not.exist;
setTimeout(function() {
expect(lastHTTPRequest).to.be.equal('/switch1?on');
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.true;
done();
});
}, 3000);
});
}).timeout(10000);
it('Test ' + adapterShortName + ' Wrapper: Test Change from inside 2', done => {
lastHTTPRequest = null;
request('http://localhost:61828/?accessoryId=switch1&state=false', (error, response, body) => {
expect(error).to.be.null;
expect(response && response.statusCode).to.be.equal(200);
setTimeout(function() {
expect(lastHTTPRequest).to.be.null;
states.getState(adapterShortName + '.0.Switch-name-1.Switch-name-1.On', (err, state) => {
expect(err).to.not.exist;
expect(state.val).to.be.false;
done();
});
}, 3000);
});
}).timeout(10000);
after('Test ' + adapterShortName + ' Global adapter: Stop js-controller', function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
httpServer.close();
done();
});
});
});

93
test/testPackageFiles.js Normal file
View File

@@ -0,0 +1,93 @@
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
'use strict';
const expect = require('chai').expect;
const fs = require('fs');
describe('Test package.json and io-package.json', () => {
it('Test package files', done => {
console.log();
const fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
const ioPackage = JSON.parse(fileContentIOPackage);
const fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
const npmPackage = JSON.parse(fileContentNPMPackage);
expect(ioPackage).to.be.an('object');
expect(npmPackage).to.be.an('object');
expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;
expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);
if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
console.log();
}
expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;
if (ioPackage.common.name.indexOf('template') !== 0) {
if (Array.isArray(ioPackage.common.authors)) {
expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
if (ioPackage.common.authors.length === 1) {
expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <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;
}
}
const licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
const fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
if (fileContentReadme.indexOf('## Changelog') === -1) {
console.log('Warning: The README.md should have a section ## Changelog');
console.log();
}
expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
if (!licenseFileExists) {
console.log('Warning: The License should also exist as LICENSE file');
console.log();
}
if (fileContentReadme.indexOf('## License') === -1) {
console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
console.log();
}
done();
});
});