Initial commit
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.git
|
||||
.idea
|
||||
node_modules
|
||||
nbproject
|
||||
admin/i18n/flat.txt
|
||||
admin/i18n/*/flat.txt
|
||||
/tmp
|
||||
mockserver-netty-3.10.8-jar-with-dependencies.jar
|
||||
/.nyc_output
|
||||
/.instrument
|
||||
/coverage
|
13
.npmignore
Normal file
@ -0,0 +1,13 @@
|
||||
gulpfile.js
|
||||
admin/i18n
|
||||
tasks
|
||||
node_modules
|
||||
.idea
|
||||
.git
|
||||
/node_modules
|
||||
test
|
||||
.travis.yml
|
||||
appveyor.yml
|
||||
/.nyc_output
|
||||
/.vscode
|
||||
/.instrument
|
24
.travis.yml
Normal file
@ -0,0 +1,24 @@
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "8"
|
||||
- "10"
|
||||
- "node"
|
||||
- "lts/*"
|
||||
before_script:
|
||||
- export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1)
|
||||
- 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi'
|
||||
- npm -v
|
||||
- npm install winston@2.3.1
|
||||
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
33
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible Node.js debug attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Mocha Tests",
|
||||
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"-u",
|
||||
"tdd",
|
||||
"--debug",
|
||||
"--no-timeouts",
|
||||
"--colors",
|
||||
"${workspaceRoot}/test"
|
||||
],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceRoot}\\main.js",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"args": [
|
||||
"--force"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
// Platzieren Sie Ihre Einstellungen in dieser Datei, um Standard- und Benutzereinstellungen zu überschreiben.
|
||||
{
|
||||
}
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Michael Schroeder <klf200@gmx.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.
|
114
README.md
Normal file
@ -0,0 +1,114 @@
|
||||
![Logo](admin/klf200.png)
|
||||
# yunkong2.klf200
|
||||
|
||||
[![Travis CI](https://travis-ci.org/MiSchroe/yunkong2.klf200.svg?branch=master)](https://travis-ci.org/MiSchroe/yunkong2.klf200)
|
||||
[![Build status](https://ci.appveyor.com/api/projects/status/t28nlps5c99jy5v7/branch/master?svg=true)](https://ci.appveyor.com/project/MiSchroe/yunkong2-klf200/branch/master)
|
||||
[![GitHub issues](https://img.shields.io/github/issues/MiSchroe/yunkong2.klf200.svg)](https://github.com/MiSchroe/yunkong2.klf200/issues)
|
||||
[![GitHub license](https://img.shields.io/github/license/MiSchroe/yunkong2.klf200.svg)](https://github.com/MiSchroe/yunkong2.klf200/blob/master/LICENSE)
|
||||
|
||||
[![NPM version](https://img.shields.io/npm/v/yunkong2.klf200.svg)](https://www.npmjs.com/package/yunkong2.klf200)
|
||||
[![Downloads](https://img.shields.io/npm/dm/yunkong2.klf200.svg)](https://www.npmjs.com/package/yunkong2.klf200)
|
||||
|
||||
[![NPM](https://nodei.co/npm/yunkong2.klf200.png?downloads=true)](https://nodei.co/npm/yunkong2.klf200/)
|
||||
|
||||
This adapter is for controlling a VELUX® KLF-200 interface. This adapter is neither an official VELUX product nor is it supported by the company that owns the VELUX products.
|
||||
|
||||
The main intention of this adapter is to control electric roof windows and/or electric blinds or roller shutters. Though the KLF-200 interface is able to connect to further devices like lights, switches, canvas blinds etc. I haven't developed the adapter for use with these kind of devices. Thus, it could be possible, that these devices could be controlled by this adapter, too.
|
||||
|
||||
The adapter works with the internal REST API of the KLF-200 interface and you don't need to wire the inputs and outputs of the box.
|
||||
|
||||
## User documentation
|
||||
|
||||
You can find the user documentation in several languages:
|
||||
|
||||
![English flag](img/united-kingdom-flag-round-icon-16.png) [English documentation](docs/en/ReadMe.md)
|
||||
|
||||
![German flag](img/germany-flag-round-icon-16.png) [Deutsche Dokumentation](docs/de/ReadMe.md)
|
||||
|
||||
![France flag](img/france-flag-round-icon-16.png) [Documentation française](docs/fr/ReadMe.md)
|
||||
|
||||
![Italien flag](img/italy-flag-round-icon-16.png) [Documentazione italiana](docs/it/ReadMe.md)
|
||||
|
||||
![Netherlands flag](img/netherlands-flag-round-icon-16.png) [Nederlandse documentatie](docs/nl/ReadMe.md)
|
||||
|
||||
![Poland flag](img/poland-flag-round-icon-16.png) [Polska dokumentacja](docs/pl/ReadMe.md)
|
||||
|
||||
![Portuguese flag](img/portugal-flag-round-icon-16.png) [Documentação portuguesa](docs/pt/ReadMe.md)
|
||||
|
||||
![Russian flag](img/russia-flag-round-icon-16.png) [Российская документация](docs/ru/ReadMe.md)
|
||||
|
||||
![Spanish flag](img/spain-flag-round-icon-16.png) [Documentación española](docs/es/ReadMe.md)
|
||||
|
||||
## Known restrictions
|
||||
|
||||
* The interface is restricted by storing a maximum of 32 scenes in total.
|
||||
* The REST API doesn't provide any feedback of a scene to be finished, therefore each scene is supposed to run at least 30 seconds.
|
||||
* Currently, only single product scenes are supported to control from the product side. Thus, it is always possible to create scenes with several products and control them from the scenes part.
|
||||
* The REST API doesn't let me read the current status of a product. Therefore the current level of a product is always what was set last and defaults to 0% on initialization of the adapter. This also means, that further logic implemented in other adapters won't work if you e.g. open or close your window with the original remote control.
|
||||
|
||||
## Documentation of the data points
|
||||
|
||||
### Devices
|
||||
|
||||
There are two devices: "products" and "scenes". The products device lists all registered products wether they are used in a scene or not. The scenes device lists all scenes that you have created in the interface.
|
||||
|
||||
#### Products
|
||||
|
||||
* productsFound - number of products registered in the interface
|
||||
* 0...n - channel for each registered product
|
||||
* category - name of the category, e.g. Window Opener, Roller Shutter
|
||||
* scenesCount - number of scenes the product is used in
|
||||
* level - This data point is only available when the product can be controlled from a scene.
|
||||
You can set a value to run a scene that will drive the product to that specific value.
|
||||
|
||||
#### Scenes
|
||||
|
||||
* scenesFound - number of scenes found in the interface
|
||||
* 0..n - channel for each scene
|
||||
* productsCount - number of products that are controlled through this scene
|
||||
* silent - true/false if the scene will run in silent mode (only, if the product supports it).
|
||||
Currently, you can only read this information.
|
||||
* run - true/false set to true to run the scene. Will change to false again after the scene has finished
|
||||
|
||||
|
||||
|
||||
## Changelog
|
||||
|
||||
#### 0.9.5
|
||||
* (Michael Schroeder) Bug fixes
|
||||
|
||||
#### 0.9.4
|
||||
* (Michael Schroeder) Compatible to Admin 3, add documentation
|
||||
|
||||
#### 0.9.0
|
||||
* (Michael Schroeder) Initial public beta release
|
||||
|
||||
#### 0.0.1
|
||||
* (Michael Schroeder) Initial developer release
|
||||
|
||||
## License
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Michael Schroeder <klf200@gmx.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.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
VELUX and the VELUX logo are registered trademarks of VKR Holding A/S.
|
9
admin/i18n/de/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Auto",
|
||||
"Manual": "Manual",
|
||||
"My select": "Mein Auswahl",
|
||||
"on save adapter restarts with new config immediately": "Beim Speichern von Einstellungen der Adapter wird sofort neu gestartet.",
|
||||
"template adapter settings": "Beispiel",
|
||||
"test1": "Test 1",
|
||||
"test2": "Test 2"
|
||||
}
|
9
admin/i18n/en/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Auto",
|
||||
"Manual": "Manual",
|
||||
"My select": "My select",
|
||||
"on save adapter restarts with new config immediately": "on save adapter restarts with new config immediately",
|
||||
"template adapter settings": "template adapter settings",
|
||||
"test1": "Test 1",
|
||||
"test2": "Test 2"
|
||||
}
|
9
admin/i18n/es/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Auto",
|
||||
"Manual": "Manual",
|
||||
"My select": "Mi seleccion",
|
||||
"on save adapter restarts with new config immediately": "en el adaptador de guardar se reinicia con nueva configuración de inmediato",
|
||||
"template adapter settings": "configuración del adaptador de plantilla",
|
||||
"test1": "Prueba 1",
|
||||
"test2": "Prueba 2"
|
||||
}
|
9
admin/i18n/fr/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Auto",
|
||||
"Manual": "Manuel",
|
||||
"My select": "Mon choix",
|
||||
"on save adapter restarts with new config immediately": "sur l'adaptateur de sauvegarde redémarre avec la nouvelle config immédiatement",
|
||||
"template adapter settings": "paramètres de l'adaptateur de modèle",
|
||||
"test1": "Test 1",
|
||||
"test2": "Test 2"
|
||||
}
|
9
admin/i18n/it/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Auto",
|
||||
"Manual": "Manuale",
|
||||
"My select": "La mia selezione",
|
||||
"on save adapter restarts with new config immediately": "su save adapter si riavvia immediatamente con la nuova configurazione",
|
||||
"template adapter settings": "impostazioni dell'adattatore del modello",
|
||||
"test1": "Test 1",
|
||||
"test2": "Test 2"
|
||||
}
|
9
admin/i18n/nl/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Auto",
|
||||
"Manual": "Met de hand",
|
||||
"My select": "Mijn select",
|
||||
"on save adapter restarts with new config immediately": "on save-adapter wordt onmiddellijk opnieuw opgestart met nieuwe config",
|
||||
"template adapter settings": "sjabloon-adapterinstellingen",
|
||||
"test1": "Test 1",
|
||||
"test2": "Test 2"
|
||||
}
|
9
admin/i18n/pt/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Auto",
|
||||
"Manual": "Manual",
|
||||
"My select": "Meu selecionado",
|
||||
"on save adapter restarts with new config immediately": "no adaptador de salvar reinicia com nova configuração imediatamente",
|
||||
"template adapter settings": "configurações do adaptador de modelo",
|
||||
"test1": "Teste 1",
|
||||
"test2": "Teste 2"
|
||||
}
|
9
admin/i18n/ru/translations.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Auto": "Автоматически",
|
||||
"Manual": "Вручную",
|
||||
"My select": "Выбор",
|
||||
"on save adapter restarts with new config immediately": "При сохранении настроек адаптера он сразу же перезапускается",
|
||||
"template adapter settings": "Пример",
|
||||
"test1": "Тест 1",
|
||||
"test2": "Тест 2"
|
||||
}
|
90
admin/index.html
Normal file
@ -0,0 +1,90 @@
|
||||
<html>
|
||||
<!-- This file is deprecated!!!!! Please use index_m.html -->
|
||||
<!-- This file is required only for backward compatibility and will be deleted soon -->
|
||||
<!-- these 4 files always have to be included -->
|
||||
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
|
||||
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
|
||||
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
|
||||
|
||||
|
||||
<!-- optional: use jqGrid
|
||||
<link rel="stylesheet" type="text/css" href="../../lib/css/jqGrid/ui.jqgrid-4.5.4.css"/>
|
||||
<script type="text/javascript" src="../../lib/js/jqGrid/jquery.jqGrid-4.5.4.min.js"></script>
|
||||
<script type="text/javascript" src="../../lib/js/jqGrid/i18n/grid.locale-all.js"></script>
|
||||
-->
|
||||
|
||||
<!-- optional: use multiselect
|
||||
<link rel="stylesheet" type="text/css" href="../../lib/css/jquery.multiselect-1.13.css"/>
|
||||
<script type="text/javascript" src="../../lib/js/jquery.multiselect-1.13.min.js"></script>
|
||||
-->
|
||||
|
||||
<!-- these two file always have to be included -->
|
||||
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
|
||||
<script type="text/javascript" src="../../js/translate.js"></script>
|
||||
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
|
||||
<script type="text/javascript" src="words.js"></script>
|
||||
|
||||
|
||||
<!-- you have to define 2 functions in the global scope: -->
|
||||
<script type="text/javascript">
|
||||
|
||||
// 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;
|
||||
$('.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();
|
||||
});
|
||||
}
|
||||
});
|
||||
onChange(false);
|
||||
}
|
||||
|
||||
// ... and the function save has to exist.
|
||||
// you have to make sure the callback is called with the settings object as first param!
|
||||
function save(callback) {
|
||||
// example: select elements with class=value and build settings object
|
||||
var obj = {};
|
||||
$('.value').each(function () {
|
||||
var $this = $(this);
|
||||
if ($this.attr('type') === 'checkbox') {
|
||||
obj[$this.attr('id')] = $this.prop('checked');
|
||||
} else {
|
||||
obj[$this.attr('id')] = $this.val();
|
||||
}
|
||||
});
|
||||
callback(obj);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- you have to put your config page in a div with id adapter-container -->
|
||||
<div id="adapter-container">
|
||||
|
||||
<table><tr>
|
||||
<td><img src="klf200.png"/></td>
|
||||
<td><h3 class="translate">KLF-200 adapter settings</h3></td>
|
||||
</tr></table>
|
||||
<table>
|
||||
<tr><td><span><label for="host" class="translate">host</label></span></td><td><input class="value" id="host"/></td></tr>
|
||||
<tr><td><span><label for="password" class="translate">password</label></span></td><td><input type="password" class="value" id="password"/></td></tr>
|
||||
<tr><td><span><label for="pollInterval" class="translate">pollInterval</label></span></td><td><input class="value" id="pollInterval"/></td></tr>
|
||||
</table>
|
||||
|
||||
<p class="translate">on save adapter restarts with new config immediately</p>
|
||||
|
||||
</div>
|
||||
|
||||
</html>
|
88
admin/index_m.html
Normal file
@ -0,0 +1,88 @@
|
||||
<html>
|
||||
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.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="../../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>
|
||||
|
||||
<!-- you have to define 2 functions in the global scope: -->
|
||||
<script type="text/javascript">
|
||||
|
||||
// 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;
|
||||
$('.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();
|
||||
});
|
||||
}
|
||||
});
|
||||
onChange(false);
|
||||
M.updateTextFields();
|
||||
}
|
||||
|
||||
// ... and the function save has to exist.
|
||||
// you have to make sure the callback is called with the settings object as first param!
|
||||
function save(callback) {
|
||||
// example: select elements with class=value and build settings object
|
||||
var obj = {};
|
||||
$('.value').each(function () {
|
||||
var $this = $(this);
|
||||
if ($this.attr('type') === 'checkbox') {
|
||||
obj[$this.attr('id')] = $this.prop('checked');
|
||||
} else {
|
||||
obj[$this.attr('id')] = $this.val();
|
||||
}
|
||||
});
|
||||
callback(obj);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- you have to put your config page in a div with id adapter-container -->
|
||||
<div class="m adapter-container">
|
||||
|
||||
<div class="row">
|
||||
<div class="col s6"><img src="klf200.png" class="logo"/></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m6">
|
||||
<input class="value" id="host" type="text"/>
|
||||
<label for="host" class="translate">host</label>
|
||||
</div>
|
||||
<div class="input-field col s12 m6">
|
||||
<input class="value" id="password" type="password"/>
|
||||
<label for="password" class="translate">password</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m3">
|
||||
<input class="value" id="pollInterval" type="number" min="0"/>
|
||||
<label for="pollInterval" class="translate">pollInterval</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<p class="translate">on save adapter restarts with new config immediately</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</html>
|
BIN
admin/klf200.png
Normal file
After Width: | Height: | Size: 902 B |
10
admin/words.js
Normal file
@ -0,0 +1,10 @@
|
||||
/*global systemDictionary:true */
|
||||
'use strict';
|
||||
|
||||
systemDictionary = {
|
||||
"KLF-200 adapter settings": { "en": "KLF-200 adapter settings", "de": "Einstellungen des KLF-200 Adapters", "ru": "Настройки адаптера KLF-200", "pt": "Configurações do adaptador KLF-200", "nl": "KLF-200 adapterinstellingen", "fr": "Paramètres de l'adaptateur KLF-200", "it": "Impostazioni dell'adattatore KLF-200", "es": "Configuraciones del adaptador KLF-200", "pl": "Ustawienia adaptera KLF-200"},
|
||||
"host": { "en": "Host", "de": "Host", "ru": "хозяин", "pt": "Hospedeiro", "nl": "Gastheer", "fr": "Hôte", "it": "Ospite", "es": "Anfitrión", "pl": "Gospodarz"},
|
||||
"on save adapter restarts with new config immediately": {"en": "On save the adapter restarts with new configuration immediately", "de": "Beim Speichern von Einstellungen wird der Adapter sofort neu gestartet.", "ru": "При перезагрузке адаптер перезагружается с новой конфигурацией немедленно", "pt": "Ao salvar, o adaptador é reiniciado com a nova configuração imediatamente", "nl": "Bij opslaan wordt de adapter onmiddellijk opnieuw opgestart met een nieuwe configuratie", "fr": "A l'enregistrement, l'adaptateur redémarre immédiatement avec la nouvelle configuration", "it": "Al salvataggio, l'adattatore si riavvia immediatamente con la nuova configurazione", "es": "Al guardar, el adaptador se reinicia con una nueva configuración de inmediato", "pl": "Po zapisaniu adapter natychmiast uruchamia się ponownie z nową konfiguracją"},
|
||||
"password": { "en": "Password", "de": "Passwort", "ru": "пароль", "pt": "Senha", "nl": "Wachtwoord", "fr": "Mot de passe", "it": "Parola d'ordine", "es": "Contraseña", "pl": "Hasło"},
|
||||
"pollInterval": { "en": "Polling interval in minutes", "de": "Abfragehäufigkeit in Minuten", "ru": "Интервал опроса в минутах", "pt": "Intervalo de pesquisa em minutos", "nl": "Polling-interval in minuten", "fr": "Intervalle d'interrogation en minutes", "it": "Intervallo di polling in minuti", "es": "Intervalo de sondeo en minutos", "pl": "Interwał odpytywania w minutach"},
|
||||
};
|
24
appveyor.yml
Normal file
@ -0,0 +1,24 @@
|
||||
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
|
||||
- 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'
|
154
docs/de/ReadMe.md
Normal file
@ -0,0 +1,154 @@
|
||||
# KLF-200 Adapter Dokumentation
|
||||
|
||||
Dieser Adapter dient zur Steuerung einer VELUX® KLF-200-Schnittstelle. Dieser Adapter ist weder ein offizielles VELUX Produktnoch wird er von der Firma unterstützt, die die VELUX-Produkte besitzt.
|
||||
|
||||
Der Hauptzweck dieses Adapters ist die Steuerung von elektrischen Dachfenstern und / oder elektrischen Jalousien oder Rollläden.
|
||||
Die Schnittstelle KLF-200 ist jedoch in der Lage, weitere Geräte wie Lampen, Schalter, Jalousien etc. anzuschließen.
|
||||
Ich habe den Adapter nicht für die Verwendung mit diesen Geräten entwickelt. So könnte es möglich sein,
|
||||
dass diese Geräte auch von diesem Adapter gesteuert werden können.
|
||||
|
||||
Der Adapter arbeitet mit der internen REST-API der KLF-200-Schnittstelle und Sie müssen weder die Eingänge noch die Ausgänge anschließen, obwohl es immer noch möglich ist, diese parallel zu verwenden.
|
||||
|
||||
---
|
||||
|
||||
## Bereiten Sie Ihre KLF-200-Schnittstelle vor
|
||||
|
||||
Um diesen Adapter verwenden zu können, müssen Sie Ihre KLF-200 Box im **Schnittstellenmodus** einrichten. Es funktioniert nicht, wenn Sie Ihre Box als Repeater verwenden.
|
||||
|
||||
> Für eine detaillierte Erklärung der folgenden Aufgaben lesen Sie bitte die mit der Box mitgelieferten Handbücher.
|
||||
>
|
||||
> Es wird davon ausgegangen, dass Sie sich in einem Webbrowser erfolgreich bei Ihrer Box angemeldet haben.
|
||||
|
||||
### Produkte einrichten
|
||||
|
||||
Jedes Produkt, das Sie mit diesem Adapter steuern möchten, muss auf der Seite "Meine Produkte" registriert sein.
|
||||
Sie können neue Produkte registrieren entweder durch
|
||||
|
||||
- Kopieren von einer anderen Fernbedienung
|
||||
- Suche nach Produkten
|
||||
|
||||
Wenn alle Ihre Produkte registriert sind, sollten Sie eine Liste wie die folgende sehen:
|
||||
|
||||
![Screenshot of "My products" of the KLF-200 interface](img/ProductList.PNG)
|
||||
|
||||
### Szenen einrichten
|
||||
|
||||
Um eine Szene aufzunehmen, klicken Sie auf die Schaltfläche
|
||||
|
||||
![Record program button](img/RecordProgramButton.PNG)
|
||||
|
||||
Dies öffnet das Fenster *Programmerstellung in Bearbeitung*. Verwenden Sie jetzt die mit Ihrem Produkt gelieferte Fernbedienung, um etwas zu ändern, z.B. öffne das Fenster zu 40%. Geben Sie dann einen Namen für das Programm ein und klicken Sie auf *Programm speichern*.
|
||||
|
||||
![Screenshot of Recording in progress](img/RecordingInProgress.PNG)
|
||||
|
||||
> TIPP:
|
||||
> - Benennen Sie Ihr Programm nach Produkt und Öffnungsgrad, zum Beispiel Fenster Badezimmer 40%. Der Adapter verwendet allerdings keine Namenskonventionen.
|
||||
> - Wenn Ihr Fenster geschlossen ist, beginnen Sie mit einem Öffnungsgrad von 100% und gehen Sie mit jedem weiteren Programm weiter nach unten bis Sie 0% erreichen.
|
||||
> - Sie haben maximal 32 Programme, die Sie in der Box speichern können.Planen Sie daher Ihre Anzahl an Schritten, da es keinen wirklichen Unterschied zwischen einem zu 30% oder zu 40% geöffnetem Fenster gibt.
|
||||
|
||||
Wenn Sie mit der Aufnahme von Programmen fertig sind, erhalten Sie eine Liste wie folgt:
|
||||
|
||||
![Screenshot of the program list](img/ProgramList.PNG)
|
||||
|
||||
### Verbindungen einrichten
|
||||
|
||||
Dieser letzte Schritt ist optional. Wenn Sie die Eingangs- und Ausgangsleitungen nicht verwenden, haben Sie vielleicht bemerkt, dass die kleine LED an der Box ständig blinkt. Um das lästige Blinken loszuwerden, müssen Sie mindestens eine Verbindung einrichten.
|
||||
|
||||
Sie müssen es nur in der Box einrichten, Sie müssen nichts verkabeln! Wählen Sie einfach irgendetwas aus.
|
||||
|
||||
---
|
||||
|
||||
## Konfigurieren Sie den Adapter
|
||||
|
||||
![Screenshot of the adapter configuration](img/AdapterConfiguration.PNG)
|
||||
|
||||
### Host
|
||||
|
||||
Hostname Ihrer KLF-200-Schnittstelle. Dies ist die gleiche Adresse, die Sie in der Adressleiste Ihres Webbrowsers zum Verbinden mit Ihrer Box eintragen.
|
||||
|
||||
### Passwort
|
||||
|
||||
Das Passwort, das Sie für die Verbindung mit Ihrer KLF-200-Schnittstelle benötigen. Es ist das gleiche, das Sie bei der Verbindung in Ihrem Webbrowser verwenden.
|
||||
|
||||
> Das Standardpasswort des KLF-200 ist `velux123`, aber Sie sollten es trotzdem geändert haben!
|
||||
|
||||
### Abfragehäufigkeit in Minuten
|
||||
|
||||
<span style="color: #ff0000"><strong><em>Diese Option ist für eine zukünftige Version geplant. Wenn Sie die Konfiguration neu laden möchten, müssen Sie den Adapter neu starten.</em></strong></span>
|
||||
|
||||
Die Anzahl der Minuten, nach der der Adapter die Konfiguration erneut von der KLF-200-Schnittstelle lädt.
|
||||
|
||||
---
|
||||
|
||||
## Benutzung des Adapters
|
||||
|
||||
Nachdem der Adapter die Metadaten von der KLF-200-Schnittstelle gelesen hat, finden Sie die folgenden Zustände
|
||||
im Objektbaum:
|
||||
|
||||
Gerät | Kanal | Zustand | Datentyp | Beschreibung
|
||||
--- | --- | --- | --- | ---
|
||||
products | | | | Hat für jedes Produkt in der Produktliste des KLF-200 einen Untereintrag.
|
||||
products | | productsFound | value | Die Anzahl der Produkte in der Liste. Schreibgeschützt.
|
||||
products | 0..n | category | text | Produktkategorie. Schreibgeschützt.
|
||||
products | 0..n | level | level | Aktueller Stand des Produkts Setzen Sie diesen Wert, damit die entsprechende Szene ausgeführt wird. Lesen / Schreiben.
|
||||
products | 0..n | scenesCount | value | Anzahl der Szenen, in denen das Produkt verwendet wird. Schreibgeschützt.
|
||||
scenes | | | | Hat für jedes Produkt in der Produktliste des KLF-200 einen Untereintrag.
|
||||
scenes | | scenesFound | value | Die Anzahl der Szenen in der Liste. Schreibgeschützt.
|
||||
scenes | 0..n | productsCount | value | Anzahl der Produkte in dieser Szene. Schreibgeschützt.
|
||||
scenes | 0..n | run | button.play | Zeigt an, ob die Szene läuft. Setzen Sie diesen Wert, damit die Szene ausgeführt wird. Lesen / Schreiben.
|
||||
scenes | 0..n | silent | indicator.silent | Gibt an, ob die Szene im leisen Modus ausgeführt wird (sofern dies von den Produkten der Szene unterstützt wird). Schreibgeschützt.
|
||||
|
||||
> **WICHTIG:**
|
||||
>
|
||||
> Die IDs, die in den Kanälen verwendet werden, sind die IDs, die von der KLF-200-Schnittstelle kommen. Wenn Sie Änderungen an der Produktliste oder an der Programmliste in Ihrem KLF-200 vornehmen, können sich die IDs ändern.
|
||||
|
||||
Um eine Szene auszuführen, können Sie den Status `run` der Szene auf `true` setzen oder den Status `level` des Produkts auf einen Wert setzen, der einer Szene entspricht, die das Produkt auf dieses Level setzt.
|
||||
|
||||
### Beispiel
|
||||
|
||||
Angenommen, Ihr Badezimmerfenster liegt auf Kanal `0`. Sie haben eine Szene auf Kanal `10`, die das Badezimmerfenster zu 40% öffnet.
|
||||
|
||||
```javascript
|
||||
// Variant 1: Open the bathroom window at 40% using the scenes run state:
|
||||
setState('klf200.0.scenes.10.run', true);
|
||||
/*
|
||||
The following will happen:
|
||||
1. Your window will start to move to 40% opening level.
|
||||
2. After your window has stopped, klf200.0.scenes.10.run will be set to 'false' again.
|
||||
3. klf200.0.products.0.level will be set to 40%.
|
||||
*/
|
||||
|
||||
// Variant 2: Open the bathroom window at 40% using the products level state:
|
||||
setState('klf200.0.products.0.level', 40);
|
||||
/*
|
||||
The following will happen:
|
||||
1. Your window will start to move to 40% opening level.
|
||||
2. klf200.0.scenes.10.run will be set to true.
|
||||
3. After your window has stopped, klf200.0.scenes.10.run will be set to 'false' again.
|
||||
*/
|
||||
|
||||
// What happens, if we don't have a scene for that level?
|
||||
setState('klf200.0.products.0.level', 41);
|
||||
/*
|
||||
The following will happen:
|
||||
1. Your window won't move at all!
|
||||
2. klf200.0.products.0.level will be reset to the previous value, e.g. 40
|
||||
*/
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
Der Adapter steuert das KLF-200 mithilfe der internen REST-API, die von der Webschnittstelle der Box verwendet wird.
|
||||
Obwohl wir nur eine Teilmenge der API verwenden, gibt es einige Einschränkungen:
|
||||
|
||||
- Der Adapter kann den aktuellen Öffnungsgrad eines Fensters nicht lesen. Wenn Sie es mit Ihrer Fernbedienung steuern oder es aufgrund von Regen geschlossen wird, weiß der Adapter nichts davon und es wird immer noch der letzte bekannte Wert angezeigt.
|
||||
- Die KLF-200-Schnittstelle ist auf maximal 32 Szenen beschränkt.
|
||||
- Der Adapter weiß nicht, wann eine Aktion beendet wurde. Der Zustand bleibt für mindestens 30 Sekunden `true`.
|
||||
- Führen Sie Szenen nicht zu schnell hintereinander aus. Der KLF-200 kann dann Fehler melden. (Sie finden die Fehler im Protokoll.)
|
||||
|
||||
---
|
||||
|
||||
VELUX und das VELUX-Logo sind eingetragene Warenzeichen der VKR Holding A/S.
|
BIN
docs/de/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/de/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
docs/de/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
docs/de/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
docs/de/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 25 KiB |
151
docs/en/ReadMe.md
Normal file
@ -0,0 +1,151 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
This adapter is for controlling a VELUX® KLF-200 interface. This adapter is neither an official VELUX product nor is it supported by the company that owns the VELUX products.
|
||||
|
||||
The main intention of this adapter is to control electric roof windows and/or electric blinds or roller shutters. Though the KLF-200 interface is able to connect to further devices like lights, switches, canvas blinds etc. I haven't developed the adapter for use with these kind of devices. Thus, it could be possible, that these devices could be controlled by this adapter, too.
|
||||
|
||||
The adapter works with the internal REST API of the KLF-200 interface and you don't need to wire the inputs and outputs of the box, though it's still possible to use them in parallel.
|
||||
|
||||
----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## Prepare your KLF-200 interface
|
||||
|
||||
To use this adapter you have to setup your KLF-200 box in the **interface mode**. It doesn't work if you use your box as a repeater.
|
||||
|
||||
> For a detailed explanation of how to accomplish the following tasks please read the manuals that came with your box.
|
||||
>
|
||||
> It is assumed that you have successfully logged into your box in a web browser.
|
||||
|
||||
|
||||
### Setup products
|
||||
|
||||
Each product that you want to control by this adapter has to be registered on the "My products" page. You can register new products either by
|
||||
- Copy from another remote control
|
||||
- Search for products
|
||||
|
||||
If all of your products are registered you should see a list like the following:
|
||||
|
||||
![Screenshot of "My products" of the KLF-200 interface](img/ProductList.PNG)
|
||||
|
||||
|
||||
### Setup scenes
|
||||
|
||||
To record a scene you have to click on the button
|
||||
|
||||
![Record program button](img/RecordProgramButton.PNG)
|
||||
|
||||
This will open the *Recording in progress* window. Now, use your remote control that comes with your product to change something, e.g. open the window to 40%. Then type in a name for the program and click on *Save program*.
|
||||
|
||||
![Screenshot of Recording in progress](img/RecordingInProgress.PNG)
|
||||
|
||||
> HINT:
|
||||
> * Name your program after product and opening level, e.g. Window bathroom 40%, though the adapter doesn't use any naming conventions.
|
||||
> * If your window is closed start with an opening level of 100% and go down with each subsequent program until you reach 0%.
|
||||
> * You have a maximum of 32 programs you can save in the box. Therefore, plan your number of steps as there is no real difference in a window opened 30% or 40%.
|
||||
|
||||
If you have finished recording programs you will end with a list like the following:
|
||||
|
||||
![Screenshot of the program list](img/ProgramList.PNG)
|
||||
|
||||
|
||||
### Setup connections
|
||||
|
||||
This last step is optional. If you don't use the input and output wires you may have noticed that the tiny LED on the box is flashing all the time. To get rid of the annoying flashing you have to setup at least one connection.
|
||||
|
||||
You only have to set it up in the box you don't need to wire anything! Just choose anything you like.
|
||||
|
||||
----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## Configure the adapter
|
||||
|
||||
![Screenshot of the adapter configuration](img/AdapterConfiguration.PNG)
|
||||
|
||||
### Host
|
||||
|
||||
Host name of your KLF-200 interface. This is the same you type into the address bar of your web browser to connect to your box.
|
||||
|
||||
### Password
|
||||
|
||||
The password you need to connect to your KLF-200 interface. It's the same you use when connecting to your box in your web browser.
|
||||
|
||||
> The default password of the KLF-200 is `velux123`, but you should have changed it, anyway!
|
||||
|
||||
### Polling interval in minutes
|
||||
|
||||
<span style="color: #ff0000">**_This option is planned for a future release. If you want to reload the configuratio you have to restart the adapter._**</span>
|
||||
|
||||
The number of minutes after which the adapter reloads the configuration from the KLF-200 interface again.
|
||||
|
||||
----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## Use the adapter
|
||||
|
||||
After the adapter has read the meta data from the KLF-200 interface you will find the following states in the object tree:
|
||||
|
||||
Device | Channel | State | Data type | Description
|
||||
---------|---------|---------------|------------------|------------------------------------------------------
|
||||
products | | | | Has a sub-entry for each product found in the product list of the KLF-200.
|
||||
products | | productsFound | value | The number of products in the list. Read-only.
|
||||
products | 0..n | category | text | Category of the product. Read-only.
|
||||
products | 0..n | level | level | Current level of the product. Set to run the corresponding scene. Read/write.
|
||||
products | 0..n | scenesCount | value | Number of scenes in which the product is used. Read-only.
|
||||
scenes | | | | Has a sub-entry for each scene found in the program list of the KLF-200.
|
||||
scenes | | scenesFound | value | The number of scenes in the list. Read-only.
|
||||
scenes | 0..n | productsCount | value | Number of products in this scene. Read-only.
|
||||
scenes | 0..n | run | button.play | Indicates if the scene is running. Set to run the scene. Read/write.
|
||||
scenes | 0..n | silent | indicator.silent | Indicates if the scene is run in silent mode (if supported by the products of the scene). Read-only.
|
||||
|
||||
> **IMPORTANT:**
|
||||
>
|
||||
> The IDs that are used in the channels are the IDs coming from the KLF-200 interface. If you make changes at the products list or at the program list in your KLF-200 the IDs may change.
|
||||
|
||||
To run a scene you can either set the `run` state of the scene to `true` or you can set the `level` state of the product to a value that corresponds to a scene that sets the product to that level.
|
||||
|
||||
### Example
|
||||
|
||||
Assuming your bathroom window is channel `0`. You have a scene on Channel `10` that opens the bathroom window at 40%.
|
||||
|
||||
````javascript
|
||||
// Variant 1: Open the bathroom window at 40% using the scenes run state:
|
||||
setState('klf200.0.scenes.10.run', true);
|
||||
/*
|
||||
The following will happen:
|
||||
1. Your window will start to move to 40% opening level.
|
||||
2. After your window has stopped, klf200.0.scenes.10.run will be set to 'false' again.
|
||||
3. klf200.0.products.0.level will be set to 40%.
|
||||
*/
|
||||
|
||||
// Variant 2: Open the bathroom window at 40% using the products level state:
|
||||
setState('klf200.0.products.0.level', 40);
|
||||
/*
|
||||
The following will happen:
|
||||
1. Your window will start to move to 40% opening level.
|
||||
2. klf200.0.scenes.10.run will be set to true.
|
||||
3. After your window has stopped, klf200.0.scenes.10.run will be set to 'false' again.
|
||||
*/
|
||||
|
||||
// What happens, if we don't have a scene for that level?
|
||||
setState('klf200.0.products.0.level', 41);
|
||||
/*
|
||||
The following will happen:
|
||||
1. Your window won't move at all!
|
||||
2. klf200.0.products.0.level will be reset to the previous value, e.g. 40
|
||||
*/
|
||||
|
||||
````
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## Known limitations
|
||||
|
||||
The adapter controls the KLF-200 using the internal REST API that is used by the web interface of the box. Though we use only a subset of the API there are some restrictions:
|
||||
|
||||
* The adapter can't read the current opening level of a window. If you control it with your remote control or it will be closed due to rain the adapter doesn't know about it and it will still show the last known value.
|
||||
* The KLF-200 interface is limited to a maximum of 32 scenes.
|
||||
* The adapter doesn't know, when an action has finished. The running state will stay `true` for at least 30 seconds.
|
||||
* Don't run scenes to fast after each other. The KLF-200 may throw errors. (You will find the errors in the log.)
|
||||
|
||||
----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
VELUX and the VELUX logo are registered trademarks of VKR Holding A/S.
|
BIN
docs/en/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
docs/en/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/en/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
docs/en/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
docs/en/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 20 KiB |
3
docs/es/ReadMe.md
Normal file
@ -0,0 +1,3 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
Please help me translating the documentation into your language. Details can be found at [issue #9](https://github.com/MiSchroe/yunkong2.klf200/issues/9).
|
BIN
docs/es/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/es/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/es/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
docs/es/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
docs/es/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 21 KiB |
3
docs/fr/ReadMe.md
Normal file
@ -0,0 +1,3 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
Please help me translating the documentation into your language. Details can be found at [issue #9](https://github.com/MiSchroe/yunkong2.klf200/issues/9).
|
BIN
docs/fr/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/fr/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/fr/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
docs/fr/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
docs/fr/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 24 KiB |
3
docs/it/ReadMe.md
Normal file
@ -0,0 +1,3 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
Please help me translating the documentation into your language. Details can be found at [issue #9](https://github.com/MiSchroe/yunkong2.klf200/issues/9).
|
BIN
docs/it/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/it/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/it/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
docs/it/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
docs/it/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 21 KiB |
3
docs/nl/ReadMe.md
Normal file
@ -0,0 +1,3 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
Please help me translating the documentation into your language. Details can be found at [issue #9](https://github.com/MiSchroe/yunkong2.klf200/issues/9).
|
BIN
docs/nl/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/nl/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
docs/nl/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
docs/nl/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
docs/nl/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 24 KiB |
3
docs/pl/ReadMe.md
Normal file
@ -0,0 +1,3 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
Please help me translating the documentation into your language. Details can be found at [issue #9](https://github.com/MiSchroe/yunkong2.klf200/issues/9).
|
BIN
docs/pl/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
docs/pl/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
docs/pl/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/pl/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/pl/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
3
docs/pt/ReadMe.md
Normal file
@ -0,0 +1,3 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
Please help me translating the documentation into your language. Details can be found at [issue #9](https://github.com/MiSchroe/yunkong2.klf200/issues/9).
|
BIN
docs/pt/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/pt/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/pt/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/pt/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
docs/pt/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
3
docs/ru/ReadMe.md
Normal file
@ -0,0 +1,3 @@
|
||||
# KLF-200 adapter documentation
|
||||
|
||||
Please help me translating the documentation into your language. Details can be found at [issue #9](https://github.com/MiSchroe/yunkong2.klf200/issues/9).
|
BIN
docs/ru/img/AdapterConfiguration.PNG
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/ru/img/ProductList.PNG
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
docs/ru/img/ProgramList.PNG
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
docs/ru/img/RecordProgramButton.PNG
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
docs/ru/img/RecordingInProgress.PNG
Normal file
After Width: | Height: | Size: 21 KiB |
489
gulpfile.js
Normal file
@ -0,0 +1,489 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var fs = require('fs');
|
||||
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();
|
||||
}
|
||||
*/
|
||||
const fileName = 'words.js';
|
||||
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) {
|
||||
try {
|
||||
var words;
|
||||
if (fs.existsSync(src + 'js/' + fileName)) {
|
||||
words = fs.readFileSync(src + 'js/' + fileName).toString();
|
||||
} else {
|
||||
words = fs.readFileSync(src + fileName).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 text = '';
|
||||
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/' + fileName)) {
|
||||
fs.writeFileSync(src + 'js/' + fileName, text);
|
||||
} else {
|
||||
fs.writeFileSync(src + '' + fileName, text);
|
||||
}
|
||||
}
|
||||
|
||||
const EMPTY = '';
|
||||
|
||||
function words2languages(src) {
|
||||
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 ' + fileName);
|
||||
}
|
||||
}
|
||||
function words2languagesFlat(src) {
|
||||
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 ' + fileName);
|
||||
}
|
||||
}
|
||||
function languagesFlat2words(src) {
|
||||
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 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('rename', function () {
|
||||
var newname;
|
||||
var author = '@@Author@@';
|
||||
var email = '@@email@@';
|
||||
for (var a = 0; a < process.argv.length; a++) {
|
||||
if (process.argv[a] === '--name') {
|
||||
newname = process.argv[a + 1]
|
||||
} else if (process.argv[a] === '--email') {
|
||||
email = process.argv[a + 1]
|
||||
} else if (process.argv[a] === '--author') {
|
||||
author = process.argv[a + 1]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('Try to rename to "' + newname + '"');
|
||||
if (!newname) {
|
||||
console.log('Please write the new template name, like: "gulp rename --name mywidgetset" --author "Author Name"');
|
||||
process.exit();
|
||||
}
|
||||
if (newname.indexOf(' ') !== -1) {
|
||||
console.log('Name may not have space in it.');
|
||||
process.exit();
|
||||
}
|
||||
if (newname.toLowerCase() !== newname) {
|
||||
console.log('Name must be lower case.');
|
||||
process.exit();
|
||||
}
|
||||
if (fs.existsSync(__dirname + '/admin/template.png')) {
|
||||
fs.renameSync(__dirname + '/admin/template.png', __dirname + '/admin/' + newname + '.png');
|
||||
}
|
||||
if (fs.existsSync(__dirname + '/widgets/template.html')) {
|
||||
fs.renameSync(__dirname + '/widgets/template.html', __dirname + '/widgets/' + newname + '.html');
|
||||
}
|
||||
if (fs.existsSync(__dirname + '/widgets/template/js/template.js')) {
|
||||
fs.renameSync(__dirname + '/widgets/template/js/template.js', __dirname + '/widgets/template/js/' + newname + '.js');
|
||||
}
|
||||
if (fs.existsSync(__dirname + '/widgets/template')) {
|
||||
fs.renameSync(__dirname + '/widgets/template', __dirname + '/widgets/' + newname);
|
||||
}
|
||||
var patterns = [
|
||||
{
|
||||
match: /yunkong2 template Adapter/g,
|
||||
replacement: newname
|
||||
},
|
||||
{
|
||||
match: /template/g,
|
||||
replacement: newname
|
||||
},
|
||||
{
|
||||
match: /Template/g,
|
||||
replacement: newname ? (newname[0].toUpperCase() + newname.substring(1)) : 'Template'
|
||||
},
|
||||
{
|
||||
match: /@@Author@@/g,
|
||||
replacement: author
|
||||
},
|
||||
{
|
||||
match: /@@email@@/g,
|
||||
replacement: email
|
||||
}
|
||||
];
|
||||
var files = [
|
||||
__dirname + '/io-package.json',
|
||||
__dirname + '/LICENSE',
|
||||
__dirname + '/package.json',
|
||||
__dirname + '/README.md',
|
||||
__dirname + '/main.js',
|
||||
__dirname + '/gulpfile.js',
|
||||
__dirname + '/widgets/' + newname +'.html',
|
||||
__dirname + '/www/index.html',
|
||||
__dirname + '/admin/index.html',
|
||||
__dirname + '/admin/index_m.html',
|
||||
__dirname + '/widgets/' + newname + '/js/' + newname +'.js',
|
||||
__dirname + '/widgets/' + newname + '/css/style.css'
|
||||
];
|
||||
files.forEach(function (f) {
|
||||
try {
|
||||
if (fs.existsSync(f)) {
|
||||
var data = fs.readFileSync(f).toString('utf-8');
|
||||
for (var r = 0; r < patterns.length; r++) {
|
||||
data = data.replace(patterns[r].match, patterns[r].replacement);
|
||||
}
|
||||
fs.writeFileSync(f, data);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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']);
|
BIN
img/france-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/france-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
img/germany-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/germany-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
img/italy-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/italy-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
img/netherlands-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/netherlands-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
img/poland-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/poland-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
img/portugal-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
img/portugal-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
img/russia-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
img/russia-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
img/spain-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
img/spain-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
img/united-kingdom-flag-round-icon-16.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
img/united-kingdom-flag-round-icon-32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
49
io-package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"common": {
|
||||
"name": "klf200",
|
||||
"version": "0.9.5",
|
||||
"title": "KLF-200",
|
||||
"titleLang": {
|
||||
"en": "KLF-200",
|
||||
"de": "KLF-200"
|
||||
},
|
||||
"desc": {
|
||||
"en": "Runs scenes on a KLF-200 Interface",
|
||||
"de": "Führt Szenen auf einer KLF-200-Schnittstelle aus"
|
||||
},
|
||||
"authors": [
|
||||
"Michael Schroeder klf200@gmx.de"
|
||||
],
|
||||
"docs": "docs/en/ReadMe.md",
|
||||
"platform": "Javascript/Node.js",
|
||||
"mode": "daemon",
|
||||
"icon": "klf200.png",
|
||||
"materialize": true,
|
||||
"enabled": true,
|
||||
"license": "MIT",
|
||||
"extIcon": "https://git.spacen.net/yunkong2/yunkong2.klf200/raw/master/admin/klf200.png",
|
||||
"keywords": [
|
||||
"KLF-200",
|
||||
"VELUX"
|
||||
],
|
||||
"readme": "https://github.com/MiSchroe/yunkong2.klf200/blob/master/README.md",
|
||||
"loglevel": "info",
|
||||
"type": "hardware"
|
||||
},
|
||||
"native": {
|
||||
"host": "http://velux-klf-",
|
||||
"password": "velux123",
|
||||
"pollInterval": 60
|
||||
},
|
||||
"objects": [
|
||||
{
|
||||
"_id": "_design/klf200",
|
||||
"language": "javascript",
|
||||
"views": {
|
||||
"listSingleProductScenes": {
|
||||
"map": "function(doc) {\n if (doc._id.match(/^klf200\\.[0-9]+\\.scenes\\.[0-9]+$/) && doc.native && doc.native.products.length == 1)\n {\n emit([doc.native.products[0].name, doc.native.products[0].status], {id: doc.native.id, name:doc.native.name});\n }\n }"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
136
lib/klfutils.js
Normal file
@ -0,0 +1,136 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* Converts an array of objects to an object.
|
||||
* The object's properties are fetched from the
|
||||
* id property of the original object of each element
|
||||
* and the value consists of the remaining properties.
|
||||
*
|
||||
* @param {Object[]} klfArray Array with internal KLF interface's result data.
|
||||
* It consists typically of objects with id, name and other properties.
|
||||
* @returns {object} Returns an object with properties for each id and their values set to
|
||||
* a copy of the original object but without the id property.
|
||||
*/
|
||||
exports.convertKlfArrayToDictionary = function convertKlfArrayToDictionary(klfArray)
|
||||
{
|
||||
return klfArray.reduce(
|
||||
function(res, curr)
|
||||
{
|
||||
if (!('id' in curr))
|
||||
{
|
||||
throw new TypeError('Missing property "id"');
|
||||
}
|
||||
res[curr.id] = _.omit(curr, 'id');
|
||||
return res;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the difference between two arrays of KLF products.
|
||||
* Products are considered new or deleted if based on their id
|
||||
* the name, category, typeId or subtype differs.
|
||||
* Products are considered changed if only the scenes differ.
|
||||
*
|
||||
* @param {Object[]} oldProducts Array with internal KLF interface's result data (the old data)
|
||||
* @param {Object[]} newProducts Array with internal KLF interface's result data (the new data)
|
||||
* @returns {object} Returns an object with new, deleted and changed objects.
|
||||
*/
|
||||
exports.getKlfProductDifferences = function getKlfProductDifferences(oldProducts, newProducts)
|
||||
{
|
||||
const compareProperties = ['name', 'category', 'typeId', 'subtype'];
|
||||
|
||||
let result = {
|
||||
"newProducts": [],
|
||||
"deletedProducts": [],
|
||||
"changedProducts": []
|
||||
};
|
||||
|
||||
// Convert the arrays to dictionaries (makes comparision easier)
|
||||
let oldProductsDictionary = this.convertKlfArrayToDictionary(oldProducts);
|
||||
let newProductsDictionary = this.convertKlfArrayToDictionary(newProducts);
|
||||
|
||||
// Check for new or changed products:
|
||||
_.forEach(newProducts, function (value) {
|
||||
let id = value.id.toString();
|
||||
let oldProduct = oldProductsDictionary[id];
|
||||
if (undefined === oldProduct
|
||||
|| !_.isEqual(_.pick(oldProduct, compareProperties), _.pick(value, compareProperties)))
|
||||
{
|
||||
result.newProducts.push(value);
|
||||
}
|
||||
else if (!_.isEqual(oldProduct.scenes, value.scenes))
|
||||
{
|
||||
result.changedProducts.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for deleted products:
|
||||
_.forEach(oldProducts, function (value) {
|
||||
let id = value.id.toString();
|
||||
let newProduct = newProductsDictionary[id];
|
||||
if (undefined === newProduct
|
||||
|| !_.isEqual(_.pick(newProduct, compareProperties), _.pick(value, compareProperties)))
|
||||
{
|
||||
result.deletedProducts.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the difference between two arrays of KLF scenes.
|
||||
* Scenes are considered new or deleted if based on their id
|
||||
* the name or products differs.
|
||||
* Scenes are considered changed if the silent property differs.
|
||||
*
|
||||
* @param {Object[]} oldScenes Array with internal KLF interface's result data (the old data)
|
||||
* @param {Object[]} newScenes Array with internal KLF interface's result data (the new data)
|
||||
* @returns {object} Returns an object with new, deleted and changed objects.
|
||||
*/
|
||||
exports.getKlfSceneDifferences = function getKlfSceneDifferences(oldScenes, newScenes)
|
||||
{
|
||||
const compareProperties = ['name', 'products'];
|
||||
|
||||
let result = {
|
||||
"newScenes": [],
|
||||
"deletedScenes": [],
|
||||
"changedScenes": []
|
||||
};
|
||||
|
||||
// Convert the arrays to dictionaries (makes comparision easier)
|
||||
let oldScenesDictionary = this.convertKlfArrayToDictionary(oldScenes);
|
||||
let newScenesDictionary = this.convertKlfArrayToDictionary(newScenes);
|
||||
|
||||
// Check for new or changed scenes:
|
||||
_.forEach(newScenes, function (value) {
|
||||
let id = value.id.toString();
|
||||
let oldScene = oldScenesDictionary[id];
|
||||
if (undefined === oldScene
|
||||
|| !_.isEqual(_.pick(oldScene, compareProperties), _.pick(value, compareProperties)))
|
||||
{
|
||||
result.newScenes.push(value);
|
||||
}
|
||||
else if (!_.isEqual(oldScene.silent, value.silent))
|
||||
{
|
||||
result.changedScenes.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for deleted scenes:
|
||||
_.forEach(oldScenes, function (value) {
|
||||
let id = value.id.toString();
|
||||
let newScene = newScenesDictionary[id];
|
||||
if (undefined === newScene
|
||||
|| !_.isEqual(_.pick(newScene, compareProperties), _.pick(value, compareProperties)))
|
||||
{
|
||||
result.deletedScenes.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
55
lib/mapTypeId.js
Normal file
@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Converts the interface's typeId value to the corresponding channel role.
|
||||
*
|
||||
* @param {number} typeId
|
||||
* @returns {string} Returns the channel role.
|
||||
*/
|
||||
exports.getRole = function getRole(typeId) {
|
||||
const mapping = {
|
||||
"1": "blind",
|
||||
"2": "shutter.roller",
|
||||
"3": "blind.awning",
|
||||
"4": "window",
|
||||
"5": "opener.garage",
|
||||
"6": "light",
|
||||
"7": "opener.gate",
|
||||
"8": "opener.door.rolling",
|
||||
"9": "lock",
|
||||
"10": "blind",
|
||||
"11": "secureconfigurationdevice",
|
||||
"12": "repeater",
|
||||
"13": "shutter.dual",
|
||||
"14": "thermo",
|
||||
"15": "switch",
|
||||
"16": "awning.horizontal",
|
||||
"17": "blind.venetion",
|
||||
"18": "blind.louvre",
|
||||
"19": "track.curtain",
|
||||
"20": "thermo.ventilation",
|
||||
"21": "thermo.heating.outdoor",
|
||||
"22": "thermo.heating.pump",
|
||||
"23": "alarm.intrusion",
|
||||
"24": "shutter.swinging"
|
||||
};
|
||||
let result = mapping[typeId.toString()] || '';
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.getLevelType = function getLevelType(typeId) {
|
||||
const mapping = {
|
||||
"1": "level.blind",
|
||||
"2": "level.blind",
|
||||
"3": "level.blind",
|
||||
"4": "level.blind",
|
||||
"10": "level.blind",
|
||||
"13": "level.blind",
|
||||
"16": "level.blind",
|
||||
"17": "level.blind",
|
||||
"18": "level.blind",
|
||||
"24": "level.blind"
|
||||
};
|
||||
let result = mapping[typeId.toString()] || 'level.blind';
|
||||
return result;
|
||||
}
|
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 == null) {
|
||||
if (!isInstall) {
|
||||
console.log('Cannot find js-controller');
|
||||
process.exit(10);
|
||||
} else {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
// we found the controller
|
||||
return path.dirname(controllerPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* reads controller base settings
|
||||
*
|
||||
* @alias getConfig
|
||||
* @returns {object}
|
||||
*/
|
||||
function getConfig() {
|
||||
let configPath;
|
||||
if (fs.existsSync(
|
||||
configPath = path.join(controllerDir, 'conf', appName + '.json')
|
||||
)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} else if (fs.existsSync(
|
||||
configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json')
|
||||
)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} else {
|
||||
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
|
||||
}
|
||||
}
|
||||
appName = getAppName();
|
||||
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
|
||||
const adapter = require(path.join(controllerDir, 'lib/adapter.js'));
|
||||
|
||||
exports.controllerDir = controllerDir;
|
||||
exports.getConfig = getConfig;
|
||||
exports.Adapter = adapter;
|
||||
exports.appName = appName;
|
593
main.js
Normal file
@ -0,0 +1,593 @@
|
||||
/**
|
||||
*
|
||||
* klf200 adapter
|
||||
*
|
||||
*
|
||||
* file io-package.json comments:
|
||||
*
|
||||
* {
|
||||
* "common": {
|
||||
* "name": "klf200", // name has to be set and has to be equal to adapters folder name and main file name excluding extension
|
||||
* "version": "0.0.0", // use "Semantic Versioning"! see http://semver.org/
|
||||
* "title": "Node.js klf200 Adapter", // Adapter title shown in User Interfaces
|
||||
* "authors": [ // Array of authord
|
||||
* "name <mail@klf200.com>"
|
||||
* ]
|
||||
* "desc": "klf200 adapter", // Adapter description shown in User Interfaces. Can be a language object {de:"...",ru:"..."} or a string
|
||||
* "platform": "Javascript/Node.js", // possible values "javascript", "javascript/Node.js" - more coming
|
||||
* "mode": "daemon", // possible values "daemon", "schedule", "subscribe"
|
||||
* "materialize": true, // support of admin3
|
||||
* "schedule": "0 0 * * *" // cron-style schedule. Only needed if mode=schedule
|
||||
* "loglevel": "info" // Adapters Log Level
|
||||
* },
|
||||
* "native": { // the native object is available via adapter.config in your adapters code - use it for configuration
|
||||
* "test1": true,
|
||||
* "test2": 42,
|
||||
* "mySelect": "auto"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
/* jshint -W097 */// jshint strict:false
|
||||
/*jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// you have to require the utils module and call adapter function
|
||||
const utils = require(__dirname + '/lib/utils'); // Get common adapter utils
|
||||
const Promise = require('bluebird');
|
||||
const klf200api = require('klf-200-api');
|
||||
const mapTypeId = require(__dirname + '/lib/mapTypeId'); // Mapping of typeId values to channel role names
|
||||
const klfutils = require(__dirname + '/lib/klfutils');
|
||||
|
||||
// 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.klf200.0
|
||||
let adapter = utils.Adapter('klf200');
|
||||
|
||||
// Define some constant values
|
||||
const deviceScenes = 'scenes';
|
||||
const deviceProducts = 'products';
|
||||
const run = 'run';
|
||||
const category = 'category';
|
||||
const scenesCount = 'scenesCount';
|
||||
const silent = 'silent';
|
||||
const productsCount = 'productsCount';
|
||||
const level = 'level';
|
||||
const levelTypes = [1, 2, 3, 4, 10, 13, 16, 17, 18, 24];
|
||||
const delayBetweenSceneRunsInMS = 30000;
|
||||
|
||||
// Cash for previous states
|
||||
let previousStates = {};
|
||||
let sceneIsRunning = {};
|
||||
|
||||
// Trace unhandled errors
|
||||
process.on('unhandledRejection', r => {
|
||||
adapter.log.error(`Unhandled promise rejection: ${r}`);
|
||||
});
|
||||
|
||||
// is called when adapter shuts down - callback has to be called under any circumstances!
|
||||
adapter.on('unload', function (callback) {
|
||||
try {
|
||||
adapter.log.info('cleaned everything up...');
|
||||
callback();
|
||||
} catch (e) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// // is called if a subscribed object changes
|
||||
// adapter.on('objectChange', function (id, obj) {
|
||||
// // Warning, obj can be null if it was deleted
|
||||
// adapter.log.info('objectChange ' + id + ' ' + JSON.stringify(obj));
|
||||
// });
|
||||
|
||||
// is called if a subscribed state changes
|
||||
adapter.on('stateChange', function (id, state) {
|
||||
// // Warning, state can be null if it was deleted
|
||||
|
||||
// you can use the ack flag to detect if it is status (true) or command (false)
|
||||
if (state && !state.ack) {
|
||||
// Get previous state (for rollback if state change isn't possible)
|
||||
let oldState = null;
|
||||
if (id.match(/^klf200\.[0-9]+\.products\.[0-9]+\.level$/)) {
|
||||
// Set old state to 0% if no previous state was found
|
||||
oldState = 0;
|
||||
} else if (id.match(/^klf200\.[0-9]+\.scenes\.[0-9]+\.run$/) && state.val === true)
|
||||
{
|
||||
// Set old state to false if no previous state was found
|
||||
oldState = false;
|
||||
}
|
||||
if (previousStates[id]) {
|
||||
oldState = previousStates[id].state.val;
|
||||
}
|
||||
|
||||
// If scene is still running => abort state change
|
||||
if (sceneIsRunning[adapter.instance] === true)
|
||||
{
|
||||
adapter.log.warn('Adapter is still running a scene, please wait until finished.');
|
||||
adapter.setStateAsync(id, oldState, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.coroutine(function* () {
|
||||
try {
|
||||
|
||||
// Check for product, e.g. id = klf200.0.products.0.level
|
||||
if (id.match(/^klf200\.[0-9]+\.products\.[0-9]+\.level$/)) {
|
||||
let sceneId = yield getSceneForProductLevel(id, state.val);
|
||||
let stateIdForScene = `${deviceScenes}.${sceneId.sceneId}.${run}`;
|
||||
// Set scene to running
|
||||
yield adapter.setStateAsync(stateIdForScene, true, true);
|
||||
yield runScene(sceneId.sceneId);
|
||||
// Set scene to not running
|
||||
yield adapter.setStateAsync(stateIdForScene, false, true);
|
||||
yield adapter.setStateAsync(id, state.val, true);
|
||||
} else if (id.match(/^klf200\.[0-9]+\.scenes\.[0-9]+\.run$/) && state.val === true)
|
||||
{
|
||||
// Check for scene, e.g. id = klf200.0.scenes.0.run and for set to running
|
||||
let idDCS = adapter.idToDCS(id);
|
||||
let sceneId = parseInt(idDCS.channel);
|
||||
// Get corresponding channel for native object with related products
|
||||
let channel = yield adapter.getObjectAsync(`${deviceScenes}.${sceneId}`);
|
||||
channel.native = channel.native || {};
|
||||
channel.native.products = channel.native.products || [];
|
||||
let products = yield adapter.getChannelsOfAsync(deviceProducts);
|
||||
// Set scene to running
|
||||
yield adapter.setStateAsync(id, true, true);
|
||||
yield runScene(sceneId);
|
||||
// Set scene to not running
|
||||
yield adapter.setStateAsync(id, false, true);
|
||||
// Set corresponding products to their correct level
|
||||
yield Promise.map(channel.native.products, function(product) {
|
||||
let productId = products.reduce(function(prev, prod) {
|
||||
if (prod.native.name === product.name) return prod.native.id;
|
||||
});
|
||||
return adapter.setStateAsync(`${deviceProducts}.${productId}.level`, product.status, true);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
adapter.log.error(`Error during state change for ${id}: ${err}`);
|
||||
yield adapter.setStateAsync(id, oldState, true);
|
||||
}
|
||||
})();
|
||||
} else if (state && state.ack) {
|
||||
previousStates[id] = {state: state};
|
||||
}
|
||||
});
|
||||
|
||||
// Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ...
|
||||
// adapter.on('message', function (obj) {
|
||||
// if (typeof obj == 'object' && obj.message) {
|
||||
// if (obj.command == 'send') {
|
||||
// // e.g. send email or pushover or whatever
|
||||
// console.log('send command');
|
||||
|
||||
// // Send response in callback if required
|
||||
// if (obj.callback) adapter.sendTo(obj.from, obj.command, 'Message received', obj.callback);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// is called when databases are connected and adapter received configuration.
|
||||
// start here!
|
||||
adapter.on('ready', function () {
|
||||
main();
|
||||
});
|
||||
|
||||
function initStates () {
|
||||
// Connect to KLF interface and read data
|
||||
let connection = new klf200api.connection(adapter.config.host);
|
||||
Promise.coroutine(function* () {
|
||||
try {
|
||||
yield connection.loginAsync(adapter.config.password);
|
||||
adapter.log.info('Connected to interface.');
|
||||
|
||||
yield adapter.createDeviceAsync(deviceProducts, { name: deviceProducts, desc: "Product list" });
|
||||
yield adapter.setObjectAsync(`${deviceProducts}.productsFound`, {
|
||||
type: 'state',
|
||||
common: {
|
||||
name: 'Number of products found',
|
||||
role: 'value',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
def: 0,
|
||||
read: true,
|
||||
write: false,
|
||||
desc: 'Number of products connected to the interface'
|
||||
},
|
||||
native: []
|
||||
});
|
||||
yield adapter.setStateAsync(`${deviceProducts}.productsFound`, { val: 0, ack: true, q: 0x42 });
|
||||
|
||||
yield adapter.createDeviceAsync(deviceScenes, { name: deviceScenes, desc: "Scene list" });
|
||||
yield adapter.setObjectAsync(`${deviceScenes}.scenesFound`, {
|
||||
type: 'state',
|
||||
common: {
|
||||
name: 'Number of scenes found',
|
||||
role: 'value',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
def: 0,
|
||||
read: true,
|
||||
write: false,
|
||||
desc: 'Number of scenes defined in the interface'
|
||||
},
|
||||
native: []
|
||||
});
|
||||
yield adapter.setStateAsync(`${deviceScenes}.scenesFound`, { val: 0, ack: true, q: 0x42 });
|
||||
|
||||
adapter.log.info('Getting installed products...');
|
||||
let products = yield new klf200api.products(connection).getAsync();
|
||||
adapter.log.info(`Found ${products.length} product(s).`);
|
||||
yield adapter.setStateAsync(`${deviceProducts}.productsFound`, { val: products.length, ack: true });
|
||||
let productsFoundObject = yield adapter.getObjectAsync(`${deviceProducts}.productsFound`);
|
||||
|
||||
let oldProducts = productsFoundObject.native || [];
|
||||
productsFoundObject.native = products || {};
|
||||
|
||||
// Get differences of products
|
||||
let productDifferences = klfutils.getKlfProductDifferences(oldProducts, products);
|
||||
|
||||
// Remove deleted products
|
||||
yield Promise.mapSeries(productDifferences.deletedProducts, function (product) {
|
||||
adapter.log.debug(`Removing product ${product.name}`);
|
||||
return adapter.deleteChannelAsync(deviceProducts, product.id.toString());
|
||||
});
|
||||
|
||||
// Add new products
|
||||
yield Promise.mapSeries(productDifferences.newProducts, function (product) {
|
||||
adapter.log.debug(`Found new product ${product.name}`);
|
||||
return createProductStateAsync(product);
|
||||
});
|
||||
|
||||
// Change products
|
||||
yield Promise.mapSeries(productDifferences.changedProducts, function (product) {
|
||||
adapter.log.debug(`Found changed product ${product.name}`);
|
||||
const productId = product.id.toString();
|
||||
return adapter.setStateAsync(`${deviceProducts}.${productId}.${scenesCount}`, { val: product.scenes.length || 0, ack: true })
|
||||
.then(
|
||||
function () {
|
||||
if (levelTypes.find((val) => { return val === product.typeId; }))
|
||||
{
|
||||
return adapter.setStateAsync(`${deviceProducts}.${productId}.${level}`, { val: 0, ack: true, q: 0x42 }); // Quality issue, because we can only assume the opening level
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
adapter.log.info('Getting scenes...');
|
||||
let scenes = yield new klf200api.scenes(connection).getAsync();
|
||||
adapter.log.info(`Found ${scenes.length} scene(s).`);
|
||||
yield adapter.setStateAsync(`${deviceScenes}.scenesFound`, { val: scenes.length, ack: true });
|
||||
let scenesFoundObject = yield adapter.getObjectAsync(`${deviceScenes}.scenesFound`);
|
||||
|
||||
let oldScenes = scenesFoundObject.native || [];
|
||||
scenesFoundObject.native = scenes || {};
|
||||
|
||||
// Get differences of scenes
|
||||
let sceneDifferences = klfutils.getKlfSceneDifferences(oldScenes, scenes);
|
||||
|
||||
// Remove delete products
|
||||
yield Promise.mapSeries(sceneDifferences.deletedScenes, function (scene) {
|
||||
adapter.log.debug(`Removing scene ${scene.name}`);
|
||||
return adapter.deleteChannelAsync(deviceScenes, scene.id.toString());
|
||||
});
|
||||
|
||||
// Add new scenes
|
||||
yield Promise.mapSeries(sceneDifferences.newScenes, function (scene) {
|
||||
adapter.log.debug(`Found new scene ${scene.name}`);
|
||||
return createSceneStateAsync(scene);
|
||||
});
|
||||
|
||||
// Change scenes
|
||||
yield Promise.mapSeries(sceneDifferences.changedScenes, function (scene) {
|
||||
adapter.log.debug(`Found changed scene ${scene.name}`);
|
||||
const sceneId = scene.id.toString();
|
||||
return adapter.setStateAsync(`${deviceScenes}.${sceneId}.${silent}`, { val: scene.silent, ack: true });
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
adapter.log.error(`Error during initialization occured: ${err}`);
|
||||
}
|
||||
finally {
|
||||
if (connection.token) {
|
||||
adapter.log.info('Disconnected from interface.');
|
||||
yield connection.logoutAsync();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
}
|
||||
|
||||
function main() {
|
||||
// The adapters config (in the instance object everything under the attribute "native") is accessible via
|
||||
// adapter.config:
|
||||
|
||||
// Promisifying has to be done at this time, because some methods are generated during initialization only
|
||||
// (e.g. setState)
|
||||
if (undefined === adapter.objects.getObjectViewAsync)
|
||||
{
|
||||
adapter.objects.getObjectViewAsync = Promise.promisify(adapter.objects.getObjectView);
|
||||
}
|
||||
|
||||
adapter.log.info('Host: ' + adapter.config.host);
|
||||
adapter.log.info('Polling interval (minutes): ' + adapter.config.pollInterval);
|
||||
|
||||
// Set internal adapter running state to false
|
||||
sceneIsRunning[adapter.instance] = false;
|
||||
|
||||
initStates();
|
||||
|
||||
// Subscribe to all level states
|
||||
adapter.subscribeStates('*level');
|
||||
// Subscribe to all silent states
|
||||
adapter.subscribeStates('*silent');
|
||||
// Subscribe to all run states
|
||||
adapter.subscribeStates('*run');
|
||||
|
||||
|
||||
// /**
|
||||
// *
|
||||
// * For every state in the system there has to be also an object of type state
|
||||
// *
|
||||
// * Here a simple klf200 for a boolean variable named "testVariable"
|
||||
// *
|
||||
// * Because every adapter instance uses its own unique namespace variable names can't collide with other adapters variables
|
||||
// *
|
||||
// */
|
||||
|
||||
// adapter.setObject('testVariable', {
|
||||
// type: 'state',
|
||||
// common: {
|
||||
// name: 'testVariable',
|
||||
// type: 'boolean',
|
||||
// role: 'indicator'
|
||||
// },
|
||||
// native: {}
|
||||
// });
|
||||
|
||||
// // in this klf200 all states changes inside the adapters namespace are subscribed
|
||||
// adapter.subscribeStates('*');
|
||||
|
||||
|
||||
// /**
|
||||
// * setState examples
|
||||
// *
|
||||
// * you will notice that each setState will cause the stateChange event to fire (because of above subscribeStates cmd)
|
||||
// *
|
||||
// */
|
||||
|
||||
// // the variable testVariable is set to true as command (ack=false)
|
||||
// adapter.setState('testVariable', true);
|
||||
|
||||
// same thing, but the value is flagged "ack"
|
||||
// ack should be always set to true if the value is received from or acknowledged from the target system
|
||||
// adapter.setState('testVariable', {val: true, ack: true});
|
||||
|
||||
// // same thing, but the state is deleted after 30s (getState will return null afterwards)
|
||||
// adapter.setState('testVariable', {val: true, ack: true, expire: 30});
|
||||
|
||||
|
||||
|
||||
// // examples for the checkPassword/checkGroup functions
|
||||
// adapter.checkPassword('admin', 'yunkong2', function (res) {
|
||||
// console.log('check user admin pw ioboker: ' + res);
|
||||
// });
|
||||
|
||||
// adapter.checkGroup('admin', 'admin', function (res) {
|
||||
// console.log('check group user admin group admin: ' + res);
|
||||
// });
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the product channels and states for the given product
|
||||
*
|
||||
* @param {product} product
|
||||
* @returns {Promise} Returns a promise that will fulfill after all steps are finished.
|
||||
*/
|
||||
function createProductStateAsync(product) {
|
||||
if (!product)
|
||||
return Promise.reject(new Error('Can\'t create states for empty product.'));
|
||||
|
||||
const productId = product.id.toString();
|
||||
|
||||
return adapter.createChannelAsync(deviceProducts, productId, { name: product.name, role: mapTypeId.getRole(product.typeId) }, product)
|
||||
.then(function () {
|
||||
return adapter.createStateAsync(deviceProducts, productId, category, {
|
||||
name: category,
|
||||
role: 'text',
|
||||
type: 'string',
|
||||
read: true,
|
||||
write: false,
|
||||
desc: 'Category of the registered product'
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.setStateAsync(`${deviceProducts}.${productId}.${category}`, { val: product.category, ack: true });
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.createStateAsync(deviceProducts, productId, scenesCount, {
|
||||
name: scenesCount,
|
||||
role: 'value',
|
||||
type: 'number',
|
||||
read: true,
|
||||
write: false,
|
||||
desc: 'Number of scenes the product is used in'
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.setStateAsync(`${deviceProducts}.${productId}.${scenesCount}`, { val: product.scenes.length || 0, ack: true });
|
||||
})
|
||||
.then(function () {
|
||||
if (levelTypes.find((val) => { return val === product.typeId; }))
|
||||
return adapter.createStateAsync(deviceProducts, productId, level, {
|
||||
name: level,
|
||||
role: mapTypeId.getLevelType(product.typeId),
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit: '%',
|
||||
read: true,
|
||||
write: true,
|
||||
desc: 'Opening level in percent'
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.setStateAsync(`${deviceProducts}.${productId}.${level}`, { val: 0, ack: true, q: 0x42 }); // Quality issue, because we can only assume the opening level
|
||||
});
|
||||
else
|
||||
return Promise.resolve();
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the scene channels and states for the given scene
|
||||
*
|
||||
* @param {scene} scene
|
||||
* @returns {Promise} Returns a promise that will fulfill after all steps are finished.
|
||||
*/
|
||||
function createSceneStateAsync(scene) {
|
||||
if (!scene)
|
||||
return Promise.reject(new Error('Can\'t create states for empty scene.'));
|
||||
|
||||
const sceneId = scene.id.toString();
|
||||
|
||||
return adapter.createChannelAsync(deviceScenes, sceneId, { name: scene.name, role: 'scene' }, scene)
|
||||
.then(function () {
|
||||
return adapter.createStateAsync(deviceScenes, sceneId, silent, {
|
||||
name: silent,
|
||||
role: 'switch',
|
||||
type: 'boolean',
|
||||
read: true,
|
||||
write: true,
|
||||
desc: 'Silent mode of the scene'
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.setStateAsync(`${deviceScenes}.${sceneId}.${silent}`, { val: scene.silent, ack: true });
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.createStateAsync(deviceScenes, sceneId, productsCount, {
|
||||
name: productsCount,
|
||||
role: 'value',
|
||||
type: 'number',
|
||||
read: true,
|
||||
write: false,
|
||||
desc: 'Number of products in the scene'
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.setStateAsync(`${deviceScenes}.${sceneId}.${productsCount}`, { val: scene.products.length || 0, ack: true });
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.createStateAsync(deviceScenes, sceneId, run, {
|
||||
name: run,
|
||||
role: 'button.play',
|
||||
type: 'boolean',
|
||||
def: false,
|
||||
read: false,
|
||||
write: true,
|
||||
desc: 'Shows the running state of a scene. Set to true to run a scene.'
|
||||
})
|
||||
.then(function () {
|
||||
return adapter.setStateAsync(`${deviceScenes}.${sceneId}.${run}`, { val: false, ack: true, q: 0x42 }); // Quality issue, because we can only assume the running state
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the corresponding scene to run a product to the specified level
|
||||
*
|
||||
* @param {string} id The id of the object representing the product you want to use.
|
||||
* @param {number} level The level to which the product should be driven, usually between 0 and 100.
|
||||
* E.g. use 50 to open a window (specified in the id parameter) to 50%.
|
||||
* @returns {Promise} Returns a promise that will fulfill after all steps are finished.
|
||||
* The resulting object of the fulfilled promise looks like this:
|
||||
* <pre>
|
||||
* <code language="javascript">
|
||||
* {
|
||||
* sceneId: 0,
|
||||
* sceneName: 'Bath room window 50%'
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
* If no corresponding scene is found the promise will be rejected.
|
||||
*/
|
||||
function getSceneForProductLevel(id, level) {
|
||||
let productName;
|
||||
return Promise.cast(Promise.coroutine(function* () {
|
||||
try {
|
||||
let idDCS = adapter.idToDCS(id);
|
||||
let productChannelId = [idDCS.device, idDCS.channel].join('.');
|
||||
|
||||
let productChannel = yield adapter.getObjectAsync(productChannelId);
|
||||
productName = productChannel.common.name;
|
||||
let scenesKey = ['klf200', adapter.instance, 'scenes', ''].join('.');
|
||||
|
||||
// This will get a list of all scenes with only a single product in it.
|
||||
let scenes = yield adapter.objects.getObjectViewAsync(
|
||||
'klf200', 'listSingleProductScenes',
|
||||
{startkey: scenesKey, endkey: scenesKey + '\u9999'}
|
||||
);
|
||||
|
||||
if (!scenes || !scenes.rows || !scenes.rows.length) return Promise.reject(new Error(`No matching scene for product ${productName} and level ${level}.`));
|
||||
|
||||
// Reduce array of products with corresponding names
|
||||
let scenesReduced = scenes.rows.reduce(
|
||||
function (currentResult, currentValue) {
|
||||
// Add product name
|
||||
currentResult[currentValue.id[0]] = currentResult[currentValue.id[0]] || { levels: {}};
|
||||
|
||||
// Add level name and scene id and name
|
||||
currentResult[currentValue.id[0]].levels[currentValue.id[1]] = currentResult[currentValue.id[0]].levels[currentValue.id[1]] || { sceneId: currentValue.value.id, sceneName: currentValue.value.name };
|
||||
|
||||
return currentResult;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (!scenesReduced[productName] || !scenesReduced[productName].levels[level]) return Promise.reject(new Error(`No matching scene for product ${productName} and level ${level}.`));
|
||||
|
||||
return scenesReduced[productName].levels[level];
|
||||
|
||||
} catch (err) {
|
||||
let errHelper = err;
|
||||
if (!(err instanceof Error)) {
|
||||
errHelper = JSON.stringify(err);
|
||||
}
|
||||
adapter.log.error(`Error during lookup scene for product ${productName} and level ${level}: ${errHelper}`);
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given scene by name or by id
|
||||
*
|
||||
* @param {string|number} sceneNameOrId
|
||||
* @returns {Promise} Returns a promise that will fulfill after the scene has run.
|
||||
*/
|
||||
function runScene(sceneNameOrId) {
|
||||
return Promise.cast(Promise.coroutine(function* () {
|
||||
let connection = new klf200api.connection(adapter.config.host);
|
||||
try {
|
||||
sceneIsRunning[adapter.instance] = true;
|
||||
yield connection.loginAsync(adapter.config.password);
|
||||
adapter.log.info('Connected to interface.');
|
||||
|
||||
yield Promise.all([new klf200api.scenes(connection).runAsync(sceneNameOrId), Promise.delay(delayBetweenSceneRunsInMS)]);
|
||||
} catch (err) {
|
||||
adapter.log.error(`Error during running scene ${sceneNameOrId} occured: ${err}`);
|
||||
}
|
||||
finally {
|
||||
sceneIsRunning[adapter.instance] = false;
|
||||
if (connection.token) {
|
||||
adapter.log.info('Disconnected from interface.');
|
||||
yield connection.logoutAsync();
|
||||
}
|
||||
}
|
||||
})());
|
||||
}
|