Initial commit
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/.idea
|
||||
/package-lock.json
|
||||
/node_modules
|
||||
10
.npmignore
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,113 @@
|
||||

|
||||
# 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
|
After Width: | Height: | Size: 6.0 KiB |
219
admin/index_m.html
Normal 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
@@ -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
@@ -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
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/de/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Das ist die Dokumentation
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
BIN
docs/en/img/picture.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/en/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is Documentation
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
BIN
docs/es/img/picture.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/es/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is Documentation
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
BIN
docs/fr/img/picture.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/fr/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is Documentation
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
BIN
docs/it/img/picture.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/it/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is Documentation
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
BIN
docs/nl/img/picture.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/nl/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is Documentation
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
BIN
docs/pt/img/picture.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/pt/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is Documentation
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
BIN
docs/ru/img/picture.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
3
docs/ru/template.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Это документация
|
||||
|
||||
(Picture)[img/picture.png)
|
||||
406
gulpfile.js
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
365
test/testAdapterWrapperNoInfo.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
364
test/testAdapterWrapperWithInfo.js
Normal 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
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||