Initial commit

This commit is contained in:
zhongjin 2019-01-16 11:42:54 +08:00
commit 44ca0897d7
82 changed files with 4809 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules
.idea
tmp
/.vs
admin/i18n/flat.txt
admin/i18n/*/flat.txt
iob_npm.done
package-lock.json

11
.npmignore Normal file
View File

@ -0,0 +1,11 @@
gulpfile.js
tasks
.git
.idea
node_modules
test
.travis.yml
appveyor.yml
admin/i18n
iob_npm.done
package-lock.json

26
.travis.yml Normal file
View File

@ -0,0 +1,26 @@
os:
- linux
- osx
sudo: required
language: node_js
node_js:
- '4'
- '6'
- '8'
- '10'
before_install:
- 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export CC=clang++; export CXX=clang++; export CXXFLAGS=-std=c++11; fi'
- 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew unlink pkg-config; fi'
- 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8; fi'
before_script:
- export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1)
- 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi'
- npm -v
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

22
LICENSE Normal file
View File

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

149
README.md Normal file
View File

@ -0,0 +1,149 @@
![Logo](admin/mihome.png)
# yunkong2 mihome Adapter
==============
[![NPM version](http://img.shields.io/npm/v/yunkong2.mihome.svg)](https://www.npmjs.com/package/yunkong2.mihome)
[![Downloads](https://img.shields.io/npm/dm/yunkong2.mihome.svg)](https://www.npmjs.com/package/yunkong2.mihome)
[![NPM](https://nodei.co/npm/yunkong2.mihome.png?downloads=true)](https://nodei.co/npm/yunkong2.mihome/)
## Requirements
### Android (copied from [here](http://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)) )
You first need to enable local network functions by using the Android Mi Home
App https://play.google.com/store/apps/details?id=com.xiaomi.smarthome :
- Install the App on a Android device
- Make sure you set your region to: Mainland China under settings -> Locale - at time of writing this seems to be required.
- Mainland China and language can set on English
- Select your Gateway in Mi Home
- Then the 3 dots at the top right of the screen
- Then click on about
- Tap the version (2.27 is the current Android version as of 2 June 2017) number at the bottom of the screen repeatedly
- You should see now 2 extra options listed in English (was Chinese in earlier versions)until you did now enable the developer mode. \[ if not try all steps again! \]
- Choose the first new option
- Then tap the first toggle switch to enable LAN functions. Note down the password (29p9i40jeypwck38 in the screenshot). Make sure you hit the OK button (to the right of the cancel button) to save your changes.
- If you change here something, you lose your password!
![android](img/mihome-settings.png)
### iOS
You first need to enable local network functions by using the [iOS Mi Home App iosApp Mi](https://itunes.apple.com/fr/app/%E7%B1%B3%E5%AE%B6-%E7%B2%BE%E5%93%81%E5%95%86%E5%9F%8E-%E6%99%BA%E8%83%BD%E7%94%9F%E6%B4%BB/id957323480?mt=8)
Install the App on a iOS device:
- Make sure you set your region to: Mainland China under settings -> Locale - required for the moment.
- Mainland China and language can set on English
- Select your Gateway in Mi Home
- Then the 3 dots at the top right of the screen
- Then click on about
- Tap under Tutorial menu(on the blank part) repeatedly
- You should see now 3 extra options listed in Chinese until you did now enable the developer mode. \[ if not try all steps again! \]
- Choose the second new option
- Then tap the first toggle switch to enable LAN functions. Note down the password (29p9i40jeypwck38 in the screenshot). Make sure you hit the OK button (to the right of the cancel button) to save your changes.
- If you change here something, you lose your password!
## Usage
You can use small button on temperature sensor to trigger "double Press" event. Just press twice within 5 seconds. You can set this interval in settings, but do not set it over 10 seconds.
### Supported devices
- gateway - Xiaomi RGB Gateway
- sensor_ht - Xiaomi Temperature/Humidity
- weather.v1 - Xiaomi Temperature/Humidity/Pressure
- switch - Xiaomi Wireless Switch
- sensor_switch.aq2 - Xiaomi Aqara Wireless Switch Sensor
- sensor_switch.aq3 - Xiaomi Aqara Wireless Switch Sensor
- plug - Xiaomi Smart Plug
- 86plug - Xiaomi Smart Wall Plug
- 86sw2 - Xiaomi Wireless Dual Wall Switch
- 86sw1 - Xiaomi Wireless Single Wall Switch
- natgas - Xiaomi Mijia Honeywell Gas Alarm Detector
- smoke - Xiaomi Mijia Honeywell Fire Alarm Detector
- ctrl_ln1 - Xiaomi Aqara 86 Fire Wall Switch One Button
- ctrl_ln1.aq1 - Xiaomi Aqara Wall Switch LN
- ctrl_ln2 - Xiaomi 86 zero fire wall switch double key
- ctrl_ln2.aq1 - Xiaomi Aqara Wall Switch LN double key
- ctrl_neutral2 - Xiaomi Wired Dual Wall Switch
- ctrl_neutral1 - Xiaomi Wired Single Wall Switch
- cube - Xiaomi Cube
- sensor_cube.aqgl01 - Xiaomi Cube
- magnet - Xiaomi Door Sensor
- sensor_magnet.aq2 - Xiaomi Aqara Door Sensor
- curtain - Xiaomi Aqara Smart Curtain
- motion - Xiaomi Motion Sensor
- sensor_motion.aq2 - Xiaomi Aqara Motion Sensor
- sensor_wleak.aq1 - Xiaomi Aqara water sensor
- ctrl_ln2.aq1 - Xiaomi Aqara Wall Switch LN (Double)
- remote.b286acn01 - Xiaomi Aqara Wireless Remote Switch (Double Rocker)
- remote.b1acn01 - Xiaomi Aqara Wireless Remote Switch
- vibration - Xiaomi vibration Sensor
- wleak1 - Xiaomi Aqara Water Sensor
- lock_aq1 - Xiaomi Lock
## Changelog
### 1.2.3 (2018-10-23)
- (goohnie) New wall switch was added
### 1.2.0 (2018-10-12)
- (bluefox) refactoring
### 1.1.2 (2018-10-08)
- (bluefox) New button switch was added
### 1.1.1 (2018-09-23)
- (bluefox) Fixed the creation of new devices
### 1.1.0 (2018-09-13)
- (bluefox) New devices added: sensor_switch.aq3, ctrl_ln1.aq1, ctrl_ln2.aq1, sensor_cube.aqgl01, remote.b286acn01, vibration, wleak1, lock_aq1
- (bluefox) Names will be taken from gateway
### 1.0.7 (2018-06-25)
- (bluefox) The heartbeat timeout and the re-connection interval settings were added
### 1.0.6 (2018-05-26)
- (bluefox) Added new Aqara cube sensor
### 1.0.5 (2018-03-05)
- (bluefox) Xiaomi Aqara Wall Switch LN Double was added
### 1.0.4 (2018-01-21)
- (bluefox) The alarm state was fixed.
### 1.0.3 (2018-01-21)
- (bluefox) Invalid temperature values will be ignored
### 1.0.2 (2018-01-14)
- (bluefox) Ignore unknown state of sensors
### 1.0.0 (2018-01-05)
- (bluefox) Do not overwrite the names
- (bluefox) Ready for Admin3
### 0.3.3 (2017-11-26)
- (bluefox) Allow multiple mihome gateways
### 0.2.4 (2017-11-04)
- (bluefox) Add aqara water sensor
### 0.2.3 (2017-09-22)
- (bluefox) Remove "." from id of the device
### 0.2.2 (2017-08-01)
- (bluefox) Set after 300ms doublePress to false by Temperature Sensor\nAllow control of Plug
### 0.2.1 (2017-07-29)
- (bluefox) Implement double click on temperature sensor
### 0.2.0 (2017-07-18)
- (bluefox) fix battery level
### 0.1.4 (2017-06-09)
- (bluefox) add cube
- (bluefox) remove voltage by gateway
### 0.1.1 (2017-06-06)
- (bluefox) Initial commit
## License
MIT
Copyright (c) 2017-2018 bluefox <dogafox@gmail.com>

BIN
admin/icons/86plug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
admin/icons/86sw1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
admin/icons/86sw2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
admin/icons/battery_p.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
admin/icons/battery_v.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
admin/icons/ctrl_ln1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
admin/icons/ctrl_ln2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
admin/icons/cube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
admin/icons/curtain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
admin/icons/gateway.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
admin/icons/lock_aq1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
admin/icons/lock_v1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
admin/icons/magnet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
admin/icons/motion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
admin/icons/natgas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
admin/icons/plug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
admin/icons/sensor_ht.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
admin/icons/smoke.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
admin/icons/switch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
admin/icons/vibration.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
admin/icons/weather_v1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

163
admin/index.html Normal file
View File

@ -0,0 +1,163 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<link rel="stylesheet" type="text/css" href="../../css/adapter.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.min.js"></script>
<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>
<style>
table {
border-collapse: collapse;
}
td.line{
border-top:1px solid black;
}
</style>
<script type="text/javascript">
'use strict';
var keys = [];
function load(settings, onChange) {
if (!settings) return;
if (!settings.keys) {
settings.keys = [];
}
fillSelectIPs('#bind', settings.bind, false, true);
$('.value').each(function () {
var key = $(this).attr('id');
var $value = $('#' + key + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$value.val(settings[key]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
$('#search').button({
label: _('Search'),
icons: {
primary: ' ui-icon-refresh'
}
}).click(function () {
$('#search').button('disable');
sendTo(null, 'browse', {port: parseInt($('#port').val(), 10), bind: $('#bind').val()}, function (list) {
$('#search').button('enable');
keys = table2values('values');
if (list && list.length) {
var changed = false;
for (var i = 0; i < list.length; list++) {
var found = false;
for (var k = 0; k < keys.length; k++) {
if (keys[k].ip.trim() === list[i]) {
found = true;
break;
}
}
if (!found) {
keys.push({ip: list[i], key: ''});
changed = true;
}
}
if (keys.length > 1) {
for (var kk = keys.length - 1; kk >= 0; kk--) {
if (!keys[kk].ip && !keys[kk].key) {
keys.splice(kk, 1);
changed = true;
}
}
}
if (changed) {
onChange();
values2table('values', keys, onChange);
}
}
});
});
keys = settings.keys || [];
values2table('values', keys, onChange);
getIsAdapterAlive(function (isAlive) {
if (isAlive || common.enabled) {
$('#search').button('enable');
} else {
$('#search').button('disable');
}
});
onChange(false);
}
function save(callback) {
var obj = {};
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
obj.interval = parseInt(obj.interval, 10) || 0;
if (obj.interval > 9000) obj.interval = 9000;
// Get edited table
obj.keys = table2values('values');
callback(obj);
}
</script>
</head>
<body>
<div id="adapter-container">
<table><tr>
<td><img src="mihome.png" width="128"/></td>
<td><h3 class="translate">MiHome Smarthome settings</h3></td>
</tr></table>
<table>
<tr><td><label class="translate" for="bind">yunkong2 IP:</label></td><td class="admin-icon"></td><td><select id="bind" class="value"></select></td></tr>
<tr><td><label class="translate" for="port">yunkong2 Port:</label></td><td class="admin-icon"></td><td><input class="value" id="port" size="5" maxlength="5"/></td></tr>
<tr><td><label class="translate" for="key">Default Gateway Key:</label></td><td class="admin-icon"></td><td><input class="value" id="key" /></td></tr>
<tr><td><label class="translate" for="interval">Double key interval (ms):</label></td><td class="admin-icon"></td><td><input class="value" type="number" max="9000" min="0" id="interval" /></td></tr>
</table>
<h4 class="translate">Gateway keys</h4>
<div id="values" style="width: 100%; height: calc(100% - 280px)">
<button class="table-button-add" style="margin-left: 10px; height: 2em; margin-right: 32px; margin-bottom: 3px; display: inline"></button><button id="search" style="display: inline;margin-bottom: 3px;"></button>
<div style="width: 100%; height: calc(100% - 30px); overflow: auto;">
<table class="table-values" style="width: 100%;">
<thead>
<tr>
<th data-name="_index" style="width: 40px" class="translate"></th>
<th data-name="ip" style="width: 120px" data-style="width: 120px" class="translate">IP Address</th>
<th data-name="key" style="width: 100%; text-align: left;" data-style="width: 100%" class="translate">Key</th>
<th data-buttons="delete" style="width: 40px"></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</body>
</html>

214
admin/index_m.html Normal file
View File

@ -0,0 +1,214 @@
<html>
<head>
<!-- these 4 files always have to be included -->
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<!-- these files always have to be included -->
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../lib/js/materialize.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="../../js/tableEditor.js"></script>
<script type="text/javascript" src="words.js"></script>
<style>
.m th {
border-radius: 0 !important;
}
</style>
<script type="text/javascript">
'use strict';
var keys = [];
function load(settings, onChange) {
if (!settings) return;
if (!settings.keys) {
settings.keys = [];
}
if (settings.heartbeatTimeout === undefined) {
settings.heartbeatTimeout = 20000;
}
if (settings.restartInterval === undefined) {
settings.restartInterval = 30000;
}
fillSelectIPs('#bind', settings.bind, false, true, function () {
$('#bind').select();
});
$('.value').each(function () {
var key = $(this).attr('id');
var $value = $('#' + key + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', settings[key]).on('change', function() {
onChange();
});
} else {
$value.val(settings[key]).on('change', function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
$('#search')/*.button({
label: _('Search'),
icons: {
primary: ' ui-icon-refresh'
}
})*/.click(function () {
$('#search').addClass('disabled');
sendTo(null, 'browse', {port: parseInt($('#port').val(), 10), bind: $('#bind').val()}, function (list) {
$('#search').removeClass('disabled');
keys = table2values('values');
if (list && list.length) {
var changed = false;
for (var i = 0; i < list.length; list++) {
var found = false;
for (var k = 0; k < keys.length; k++) {
if (keys[k].ip.trim() === list[i]) {
found = true;
break;
}
}
if (!found) {
keys.push({ip: list[i], key: ''});
changed = true;
}
}
if (keys.length > 1) {
for (var kk = keys.length - 1; kk >= 0; kk--) {
if (!keys[kk].ip && !keys[kk].key) {
keys.splice(kk, 1);
changed = true;
}
}
}
if (changed) {
onChange();
values2table('values', keys, {onChange: onChange});
}
}
});
});
keys = settings.keys || [];
values2table('values', keys, {onChange: onChange});
getIsAdapterAlive(function (isAlive) {
if (isAlive || common.enabled) {
$('#search').removeClass('disabled');
} else {
$('#search').addClass('disabled');
}
});
onChange(false);
}
function save(callback) {
var obj = {};
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
obj.interval = parseInt(obj.interval, 10) || 0;
if (obj.interval > 9000) obj.interval = 9000;
// Get edited table
obj.keys = table2values('values');
callback(obj);
}
</script>
</head>
<body>
<div class="adapter-container m">
<div class="row">
<div class="col s12">
<ul class="tabs">
<li class="tab col s2"><a href="#tab-main" class="translate active">Main settings</a></li>
<li class="tab col s2"><a href="#tab-keys" class="translate">Gateway keys</a></li>
</ul>
</div>
<div id="tab-main" class="col s12 page">
<div class="row">
<div class="col s12 m4 l2">
<img src="mihome.png" class="logo">
</div>
</div>
<div class="row">
<div class="col s12 m8 l5">
<select class="value" id="bind"></select>
<label class="translate" for="bind">yunkong2 IP:</label>
</div>
<div class="col s12 m4 l1">
<input class="value" id="port" min="0" max="65565" maxlength="5" type="number"/>
<label class="translate" for="port">yunkong2 Port:</label>
</div>
</div>
<div class="row">
<div class="col s12 m8 l5">
<input class="value" id="heartbeatTimeout" min="0" max="120000" type="number"/>
<label class="translate" for="heartbeatTimeout">heartbeatTimeout</label>
</div>
<div class="col s12 m4 l1">
<input class="value" id="restartInterval" min="1000" max="120000" type="number"/>
<label class="translate" for="restartInterval">restartInterval</label>
</div>
</div>
<div class="row">
<div class="col s12 m8 l5">
<input class="value" id="key" />
<label class="translate" for="key">Default Gateway Key:</label>
</div>
<div class="col s12 m4 l1">
<input class="value" id="interval" min="0" max="9000" maxlength="4" type="number"/>
<label class="translate" for="interval">Double key interval (ms):</label>
</div>
</div>
</div>
<div id="tab-keys" class="col s12 page">
<div id="values">
<div class="row">
<div class="col s12">
<a class="btn-floating waves-effect waves-light table-button-add"><i class="material-icons">add</i></a>
<a class="waves-effect waves-light btn" id="search"><i class="material-icons">search</i><span class="translate">Search</span></a>
</div>
</div>
<div class="row">
<div class="col s12">
<table class="table-values" style="width: 100%;">
<thead>
<tr>
<th data-name="_index" style="width: 40px" class="translate"></th>
<th data-name="ip" style="width: 120px" data-style="width: 120px" class="translate">IP Address</th>
<th data-name="key" style="width: 100%; text-align: left;" data-style="width: 100%" class="translate">Key</th>
<th data-buttons="delete" style="width: 40px"></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

BIN
admin/mihome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

32
admin/words.js Normal file
View File

@ -0,0 +1,32 @@
/*global systemDictionary:true */
'use strict';
systemDictionary = {
"Gateway Key:": { "en": "Gateway Key:", "de": "Gateway Schlussel:", "ru": "Gateway ключ:", "pt": "Chave do Gateway:", "nl": "Gateway-sleutel:", "fr": "Clé de la passerelle:", "it": "Chiave di accesso:", "es": "Clave de acceso:", "pl": "Klucz bramki:"},
"Listen on all IPs": { "en": "Listen on all IPs", "de": "Auf allen IP Adressen hören", "ru": "Слушать на всех адресах", "pt": "Ouça todos os IPs", "nl": "Luister op alle IP's", "fr": "Écoutez sur toutes les adresses IP", "it": "Ascolta su tutti gli IP", "es": "Escuchar en todas las direcciones IP", "pl": "Posłuchaj na wszystkich IP"},
"MiHome Smarthome settings": { "en": "MiHome Smarthome settings", "de": "MiHome Smarthome settings", "ru": "MiHome Smarthome settings", "pt": "Configurações de MiHome Smarthome", "nl": "MiHome Smarthome-instellingen", "fr": "MiHome Smarthome paramètres", "it": "Impostazioni MiHome Smarthome", "es": "Ajustes MiHome Smarthome", "pl": "Ustawienia MiHome Smarthome"},
"yunkong2 IP:": { "en": "yunkong2 IP:", "de": "yunkong2 IP:", "ru": "yunkong2 IP:", "pt": "yunkong2 IP:", "nl": "yunkong2 IP:", "fr": "yunkong2 IP:", "it": "IP yunkong2:", "es": "yunkong2 IP:", "pl": "IP yunkong2:"},
"yunkong2 Port:": { "en": "yunkong2 Port:", "de": "yunkong2 Port:", "ru": "yunkong2 порт:", "pt": "yunkong2 Port:", "nl": "yunkong2 Port:", "fr": "yunkong2 Port:", "it": "porta yunkong2:", "es": "Puerto yunkong2:", "pl": "Port yunkong2:"},
"heartbeatTimeout": {
"en": "Heartbeat timeout (ms)",
"de": "Heartbeat-Timeout (ms)",
"ru": "Тайм-аут сердечного ритма (мс)",
"pt": "Tempo limite de pulsação (ms)",
"nl": "Heartbeat time-out (ms)",
"fr": "Délai d'attente de pulsation (ms)",
"it": "Timeout heartbeat (ms)",
"es": "Tiempo de latido del corazón (ms)",
"pl": "Limit czasu bicia serca (ms)"
},
"restartInterval": {
"en": "Re-connection interval (ms)",
"de": "Re-Verbindungsintervall (ms)",
"ru": "Интервал повторного подключения (мс)",
"pt": "Intervalo de reconexão (ms)",
"nl": "Re-connection interval (ms)",
"fr": "Intervalle de reconnexion (ms)",
"it": "Intervallo di ricollegamento (ms)",
"es": "Intervalo de reconexión (ms)",
"pl": "Interwał ponownego połączenia (ms)"
}
};

25
appveyor.yml Normal file
View File

@ -0,0 +1,25 @@
version: 'test-{build}'
environment:
matrix:
- nodejs_version: '4'
- nodejs_version: '6'
- nodejs_version: '8'
- nodejs_version: '10'
platform:
- x86
- x64
clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%'
install:
- ps: 'Install-Product node $env:nodejs_version $env:platform'
- ps: '$NpmVersion = (npm -v).Substring(0,1)'
- ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }'
- ps: npm --version
- npm install
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
test_script:
- echo %cd%
- node --version
- npm --version
- npm test
build: 'off'

125
doc/de/README.md Normal file
View File

@ -0,0 +1,125 @@
![Logo](media/mihome.png)
# yunkong2 Mi Home Adapter
Mit dem Mi Home Adapter wird ein Mi Control Hub (Gateway) in ein yunkong2 System
eingebunden und ermöglicht so die Kommunikation verschiedener Xiaomi Sensoren,
Schalter etc. mit yunkong2.
Über yunkong2 kann z.B. die Beleuchtung und der Lautsprecher des Gateways gesteuert
werden.
## Voraussetzungen
* Mi Home App auf Android oder iOS Gerät und frei geschaltete lokale Netzwerk Funktion
* Angeschlossenes Mi Home Gateway
* Betriebsbereites yunkong2 System
### Installation der Mi Home App und freischalten der lokalen Netzwerk Funktion
#### Android
* [Android Mi Home App][Android App] auf einem Android Gerät herunterladen, installieren, öffnen und
den Geschäftsbedingungen zustimmen.
* Als Land `Festland-China` auswählen
* Über `Anmelden` ein Konto erstellen
* Nach der erfolgreichen Anmeldung über `+` ein Gerät hinzufügen
* Unter `Haushaltssicherheit` den `MI Control Hub` auswählen und den Anweisungen
folgen
* Nach erfolgreichem einbinden des Gateways die 3 Punkte am oberen rechten Bildschirm
und danach `About` betätigen
* Den Text `Plug-in version` unten 10mal tippen
* Nun ist der Entwickler Modus eingeschaltet und es sollten nach einer gewissen Zeit
2 weitere Menüpunkte erscheinen
>Falls nicht, wiederholt versuchen
* Den Menüpunkt `Wireless communication protocol` auswählen
* Den Schiebeschalter oben einschalten, das Passwort notieren und mit `OK` bestätigen.
>Das Passwort wird später bei der yunkong2 Installation benötigt.
Nun können weitere Geräte über das `+` Zeichen angelernt werden.
#### iOS
* [iOS Mi Home App][ios App] auf einem iOS Gerät herunterladen, installieren, öffnen und der
Datenschutzerklärung zustimmen
* Über Profil/Einstellungen/Ländereinstellungen das Land `Festland` auswählen.
* Über `Anmelden` ein Konto erstellen
* Nach der erfolgreichen Anmeldung über `+` ein Gerät hinzufügen
* Unter `Haushalt Sicherheit` den `MI Control Hub` auswählen und den Anweisungen
folgen
* Nach erfolgreichem einbinden des Gateways die 3 Punkte am oberen rechten Bildschirm
betätigen und `About` betätigen
* Wiederholt im leeren unteren Bereich tippen
* Nun ist der Entwickler Modus eingeschaltet und es sollten nach einer gewissen Zeit
weitere Menüpunkte erscheinen
> Falls es nicht gleich klappt, die Schritte wiederholen
* Den 4. Menüpunkt auswählen
* Den Schiebeschalter oben einschalten, das Passwort notieren und mit `OK` bestätigen.
>Das Passwort wird später bei der yunkong2 Installation benötigt.
Nun können weitere Geräte über das `+` Zeichen angelernt werden.
### Einstellung am Router
Unter About/Hub info kann im Text nach _localip_ die vom Gateway verwendete IP Adresse
des Gateways ermittelt werden. Im verwendeten Router sollte diese IP dem Gateway fest
zugewiesen werden.
Falls die Bedienung der angelernten Geräte über die App nicht mehr gewollt ist, kann nach
dem anlernen aller Geräte im Router auch der Internet Zugriff des Gateways abgeschaltet
werden.
### Unterstützte Geräte
Die folgende Aufstellung erhebt keinen Anspruch auf Vollständigkeit:
- gateway - Xiaomi RGB Gateway
- sensor_ht - Xiaomi Temperature/Humidity
- weather.v1 - Xiaomi Temperature/Humidity/Pressure
- switch - Xiaomi Wireless Switch
- sensor_switch.aq2 - Xiaomi Aqara Wireless Switch Sensor
- sensor_switch.aq3 - Xiaomi Aqara Wireless Switch Sensor
- plug - Xiaomi Smart Plug
- 86plug - Xiaomi Smart Wall Plug
- 86sw2 - Xiaomi Wireless Dual Wall Switch
- 86sw1 - Xiaomi Wireless Single Wall Switch
- natgas - Xiaomi Mijia Honeywell Gas Alarm Detector
- smoke - Xiaomi Mijia Honeywell Fire Alarm Detector
- ctrl_ln1 - Xiaomi Aqara 86 Fire Wall Switch One Button
- ctrl_ln1.aq1 - Xiaomi Aqara Wall Switch LN
- ctrl_ln2 - Xiaomi 86 zero fire wall switch double key
- ctrl_ln2.aq1 - Xiaomi Aqara Wall Switch LN double key
- ctrl_neutral2 - Xiaomi Wired Dual Wall Switch
- ctrl_neutral1 - Xiaomi Wired Single Wall Switch
- cube - Xiaomi Cube
- sensor_cube.aqgl01 - Xiaomi Cube
- magnet - Xiaomi Door Sensor
- sensor_magnet.aq2 - Xiaomi Aqara Door Sensor
- curtain - Xiaomi Aqara Smart Curtain
- motion - Xiaomi Motion Sensor
- sensor_motion.aq2 - Xiaomi Aqara Motion Sensor
- sensor_wleak.aq1 - Xiaomi Aqara water sensor
- ctrl_ln2.aq1 - Xiaomi Aqara Wall Switch LN (Double)
- remote.b286acn01 - Xiaomi Aqara Wireless Remote Switch (Double Rocker)
- remote.b1acn01 - Xiaomi Aqara Wireless Remote Switch
- vibration - Xiaomi vibration Sensor
- wleak1 - Xiaomi Aqara Water Sensor
- lock_aq1 - Xiaomi Lock
## yunkong2 Mi Home Adapter Installation
Weitere Einstellungen erfolgen nur noch über die yunkong2 Admin-Oberfläche.
Den Adapter im Bereich `Adapter` suchen und über das `+` Zeichen installieren.
![Logo](media/Adapter.png)
Es öffnet sich dann folgendes Konfigurationsfenster:
![Logo](media/Adapterconfig1.PNG)
Unter `Default Gateway Key` das oben ermittelte Passwort eintragen und mit `speichern`
`und schließen` das Fenster schließen. Der laufende Adapter sollte danach unter
`Instanzen` grün angezeigt werden:
![Logo](media/Instanz.PNG)
Unter `Objekte` wird nun das Gateway und seine angelernten Geräte angezeigt:
![Logo](media/Objekte.PNG)
Die Anleitung wurde nach besten Wissen und Gewissen erstellt.
[Android App]:(https://play.google.com/store/apps/details?id=com.xiaomi.smarthome)
[iOS App]:(https://itunes.apple.com/de/app/mi-home-xiaomi-smarthome/id957323480?mt=8)

BIN
doc/de/media/Adapter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
doc/de/media/Instanz.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
doc/de/media/Objekte.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
doc/de/media/mihome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

114
doc/en/Readme.md Normal file
View File

@ -0,0 +1,114 @@
![Logo](media/mihome.png)
# yunkong2 Mi Home Adapter
The Mi Home Adapter integrates a Mi Control Hub (Gateway) into an yunkong2 system
and enables the communication of various Xiaomi sensors, switches etc. with yunkong2.
Via yunkong2, e.g. the lighting and the speaker of the gateway are controlled.
## Requirements
* Mi Home App on Android or iOS device and free local network function
* Connected Mi Home Gateway
* Ready to use yunkong2 system
### Mi Home App installation and local network feature unlocking
#### Android
* Download, install and open [Android Mi Home App][Android App] on an Android device
* Choose `Mainland China` and set language `English`
* Setup an Account with `Sign in`
* After successfully logging in add a device via `+`
* Choose `MI Control Hub` under `Household security` and follow the instructions
* After successfully integration of the Gateway in Mi Home tip the 3 dots at the top
right of the screen and click `About`
* Tap the text `Plug*in version` number at the bottom of the screen 10 times
* There should be now 2 extra options listed until the developer mode is enabled.
[ if not try all steps again!]
* Choose the option `Wireless communication protocol`
* Then tap the first toggle switch to enable LAN functions. Note down the password.
> Password is required later in the yunkong2 installation.
* Make sure to hit the OK button to save changes.
* If you change here something, you lose your password!
Now further devices can be trained via the `+` sign.
#### iOS
* Download, install and open [iOS Mi Home App][ios App] on a iOS device
* Select the country setting `mainland China` via Profile/Settings/Country .
* Setup an Account with `Sign in`
* After successfully logging in add a device via `+`
* Choose `MI Control Hub` under `Household security` and follow the instructions
* After successfully integration of the Gateway in Mi Home tip the 3 dots at the top
right of the screen and click `About`*
* There should be now more extra options listed until the developer mode is enabled.
[ if not try all steps again!]
* Choose the option No. 4
* Then tap the first toggle switch to enable LAN functions. Note down the password.
> Password is required later in the yunkong2 installation.
* Make sure to hit the OK button to save changes.
* If you change here something, you lose your password!
Now further devices can be trained via the `+` sign.
## Router Setup
The Gateway IP address can be determined within the text after _localip_ under About/Hub
info. In the used router used, this IP should be assigned fix to the gateway.
If the operation of the learned devices via the App is no longer wanted, after learning
all devices the Internet access of the gateway can be switched off in the router.
### Supported devices
The following list does not claim to be complete:
* gateway * Xiaomi RGB Gateway
* sensor_ht * Xiaomi Temperature/Humidity
* weather.v1 * Xiaomi Temperature/Humidity/Pressure
* switch * Xiaomi Wireless Switch
* sensor_switch.aq2 * Xiaomi Aqara Wireless Switch Sensor
* sensor_switch.aq3 * Xiaomi Aqara Wireless Switch Sensor
* plug * Xiaomi Smart Plug
* 86plug * Xiaomi Smart Wall Plug
* 86sw2 * Xiaomi Wireless Dual Wall Switch
* 86sw1 * Xiaomi Wireless Single Wall Switch
* natgas * Xiaomi Mijia Honeywell Gas Alarm Detector
* smoke * Xiaomi Mijia Honeywell Fire Alarm Detector
* ctrl_ln1 * Xiaomi Aqara 86 Fire Wall Switch One Button
* ctrl_ln1.aq1 * Xiaomi Aqara Wall Switch LN
* ctrl_ln2 * Xiaomi 86 zero fire wall switch double key
* ctrl_ln2.aq1 * Xiaomi Aqara Wall Switch LN double key
* ctrl_neutral2 * Xiaomi Wired Dual Wall Switch
* ctrl_neutral1 * Xiaomi Wired Single Wall Switch
* cube * Xiaomi Cube
* sensor_cube.aqgl01 * Xiaomi Cube
* magnet * Xiaomi Door Sensor
* sensor_magnet.aq2 * Xiaomi Aqara Door Sensor
* curtain * Xiaomi Aqara Smart Curtain
* motion * Xiaomi Motion Sensor
* sensor_motion.aq2 * Xiaomi Aqara Motion Sensor
* sensor_wleak.aq1 * Xiaomi Aqara water sensor
* ctrl_ln2.aq1 * Xiaomi Aqara Wall Switch LN (Double)
* remote.b286acn01 * Xiaomi Aqara Wireless Remote Switch (Double Rocker)
* remote.b1acn01 * Xiaomi Aqara Wireless Remote Switch
* vibration * Xiaomi vibration Sensor
* wleak1 * Xiaomi Aqara Water Sensor
* lock_aq1 * Xiaomi Lock
## yunkong2 Mi Home Adapter installation
Further settings are only made via the yunkong2 Admin * interface.
Find the adapter in the `Adapter` area and install it using the `+` sign.
![Logo](media/Adapter.png)
The following configuration window then opens:
![Logo](media/Adapterconfig1.PNG)
Enter the password determined above under `Default Gateway Key` and close the window
with `save and close`.
The current adapter should then be displayed green under `Instances`:
![Logo](media/Instanz.PNG)
Under `Objects` now the gateway and its learned devices are displayed:
![Logo](media/Objekte.PNG)
The manual was created to the best of my knowledge and belief.
[Android App]:(https://play.google.com/store/apps/details?id=com.xiaomi.smarthome)
[iOS App]:(https://itunes.apple.com/de/app/mi*home*xiaomi*smarthome/id957323480?mt=8)

BIN
doc/en/media/Adapter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
doc/en/media/Instanz.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
doc/en/media/Objekte.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
doc/en/media/mihome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

398
gulpfile.js Normal file
View File

@ -0,0 +1,398 @@
'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('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/mihome-settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

61
io-package.json Normal file
View File

@ -0,0 +1,61 @@
{
"common": {
"name": "mihome",
"title": "Xiaomi MiHome Gateway",
"desc": {
"en": "Xiaomi MiHome gateway support",
"cn": "小米网关"
},
"version": "1.2.3",
"mode": "daemon",
"platform": "javascript/Node.js",
"loglevel": "info",
"keywords": [
"mihome",
"xiaomi"
],
"messagebox": true,
"materialize": true,
"main": "main.js",
"license": "MIT",
"readme": "https://git.spacen.net/yunkong2/yunkong2.mihome/blob/master/README.md",
"icon": "mihome.png",
"extIcon": "https://git.spacen.net/yunkong2/yunkong2.mihome/master/admin/mihome.png",
"type": "iot-systems",
"enabled": true
},
"native": {
"bind": "0.0.0.0",
"port": 9898,
"keys": [
{"ip": "", "key": ""}
],
"interval": 5000,
"heartbeatTimeout": 20000,
"restartInterval": 30000
},
"objects": [],
"instanceObjects": [
{
"_id": "info",
"type": "channel",
"common": {
"name": "Information"
},
"native": {}
},
{
"_id": "info.connection",
"type": "state",
"common": {
"role": "indicator.connected",
"name": "If connected to MiHome gateway",
"type": "boolean",
"read": true,
"write": false,
"def": false
},
"native": {}
}
]
}

183
lib/Hub.js Normal file
View File

@ -0,0 +1,183 @@
'use strict';
const dgram = require('dgram');
const util = require('util');
const EventEmitter = require('events').EventEmitter;
const crypto = require('crypto');
const Devices = require('./devices');
function Hub(options) {
if (!(this instanceof Hub)) return new Hub(options);
options = options || {};
options.port = parseInt(options.port, 10) || 9898;
this.sensors = {};
this.key = options.key;
this.keys = {};
this.token = {};
this.iv = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
if (options.keys) {
for (let i = 0; i < options.keys.length; i++) {
this.keys[options.keys[i].ip] = options.keys[i].key;
}
}
/* this.clickTypes = {
click: 'click',
double_click: 'double_click',
long_click_press: 'long_click_press',
long_click_release: 'long_click_release'
}; */
this.listen = function () {
this.socket = dgram.createSocket('udp4');
this.socket.on('message', this.onMessage.bind(this));
this.socket.on('error', this.onError.bind(this));
this.socket.on('listening', this.onListening.bind(this));
this.socket.bind(options.port);
};
this.stop = function (cb) {
if (this._state === 'CLOSED') return false;
this._state = 'CLOSED';
if (this.socket) {
try {
this.socket.removeAllListeners();
this.socket.close(cb);
this.socket = null;
} catch (e) {
cb && cb();
}
} else {
cb && cb();
}
};
this.onListening = function () {
this._state = 'CONNECTED';
this.socket.setBroadcast(true);
this.socket.setMulticastTTL(128);
if (options.bind && options.bind !== '0.0.0.0') {
this.socket.addMembership('224.0.0.50', options.bind);
} else {
this.socket.addMembership('224.0.0.50');
}
const whoIsCommand = '{"cmd": "whois"}';
this.socket.send(whoIsCommand, 0, whoIsCommand.length, 4321, '224.0.0.50');
};
this.onError = function (err) {
if (this._state === 'CLOSED') return false;
this.emit('error', err);
};
this.onMessage = function (msgBuffer, rinfo) {
if (this._state === 'CLOSED') return false;
let msg;
try {
msg = JSON.parse(msgBuffer.toString());
}
catch (e) {
return;
}
let sensor = this.getSensor(msg.sid);
if (!sensor) {
// {"model":"lumi.lock.v1","did":"lumi.1xxxxxxxxxxxxx8","name":"Front door lock"}
if (!msg.model) {
return;
}
try {
if (options.browse) {
if (msg.model === 'gateway') {
this.emit('browse', {ip: rinfo.address});
}
return;
} else {
sensor = this.sensorFactory(msg.sid, msg.model, rinfo.address, msg.name);
}
}
catch (e) {
this.emit('warning', 'Could not add new sensor: ' + e.message);
return;
}
}
if (sensor) {
if (msg.data && typeof msg.data === 'string') {
try {
msg.data = JSON.parse(msg.data);
} catch (e) {
this.emit('warning', 'Could not parse: ' + msg.data);
msg.data = null;
}
}
if (msg.token) {
this.token[rinfo.address] = msg.token;
}
if (msg.cmd === 'heartbeat') {
sensor.heartBeat(msg.token, msg.data);
} else {
sensor.heartBeat();
}
if (msg.data && (msg.cmd === 'report' || msg.cmd.indexOf('_ack') !== -1)) {
sensor.onMessage(msg);
}
}
this.emit('message', msg);
};
this.getKey = function (ip) {
if (!this.token[ip]) return null;
const key = this.keys[ip] || this.key;
const cipher = crypto.createCipheriv('aes-128-cbc', key, this.iv);
const crypted = cipher.update(this.token[ip], 'ascii', 'hex');
cipher.final('hex'); // Useless data, don't know why yet.
return crypted;
};
this.sendMessage = function (message, ip) {
if (this._state === 'CLOSED') return false;
const json = JSON.stringify(message);
this.socket.send(json, 0, json.length, options.port, ip || '224.0.0.50');
};
this.sensorFactory = function (sid, model, ip, name) {
if (this._state === 'CLOSED') return false;
let sensor = null;
const dev = Object.keys(Devices).find(id => Devices[id].type === model);
if (Devices[dev] && Devices[dev].ClassName) {
sensor = new Devices[dev].ClassName(sid, ip, this, model, options);
} else {
throw new Error('Type "' + model + '" is not valid, use one of Hub::sensorTypes');
}
this.registerSensor(sensor, name);
return sensor;
};
this.getSensor = function (sid) {
return this.sensors[sid] || null;
};
this.registerSensor = function (sensor, name) {
if (this._state === 'CLOSED') return false;
this.emit('device', sensor, name);
this.sensors[sensor.sid] = sensor;
};
return this;
}
// extend the EventEmitter class using our Radio class
util.inherits(Hub, EventEmitter);
module.exports = {
Hub,
Devices
};

81
lib/Sensors/Alarm.js Normal file
View File

@ -0,0 +1,81 @@
'use strict';
const texts = {
0: 'Release alarm',
1: 'Gas alarm',
2: 'Analog alarm',
64: 'Sensitivity fault alarm',
32768: 'I2C communication failure'
};
function Alarm(sid, ip, hub, model) {
this.type = model;
this.sid = sid;
this.hub = hub;
this.ip = ip;
this.state = null;
this.description = null;
this.voltage = null;
this.percent = null;
}
Alarm.prototype.getData = function (data, isHeartbeat) {
let newData = false;
let obj = {};
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.alarm !== undefined && !isHeartbeat) {
if (data.alarm !== true && data.alarm !== false) {
data.alarm = parseInt(data.alarm, 10) || 0;
if (data.alarm === 1) {
if (this.type === 'smoke') {
data.description = 'Smoke alarm';
} else {
data.description = 'Gas alarm';
}
} else {
data.description = texts[data.alarm] || '';
}
data.alarm = (data.alarm === 1 || data.alarm === 2);
}
this.state = data.alarm;
obj.state = data.alarm;
obj.description = data.description;
newData = true;
}
return newData ? obj : null;
};
Alarm.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data, true);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Alarm.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = Alarm;

71
lib/Sensors/Button.js Normal file
View File

@ -0,0 +1,71 @@
'use strict';
function Button(sid, ip, hub) {
this.type = 'switch';
this.sid = sid;
this.hub = hub;
this.voltage = null;
this.percent = null;
this.ip = ip;
return this;
}
Button.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.voltage) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.status) {
if (data.status === 'click') {
obj.click = true;
setTimeout(() => this.hub.emit('data', this.sid, this.type, {click: false}), 300);
}
if (data.status === 'double_click') {
obj.double = true;
setTimeout(() => this.hub.emit('data', this.sid, this.type, {double: false}), 300);
}
if (data.status === 'long_click_press') {
obj.long = true;
}
if (data.status === 'long_click_release') {
obj.long = false;
}
newData = true;
}
return newData ? obj : null;
};
Button.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Button.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = Button;

97
lib/Sensors/Cube.js Normal file
View File

@ -0,0 +1,97 @@
'use strict';
function Cube(sid, ip, hub, rotatePosition) {
this.type = 'cube';
this.sid = sid;
this.ip = ip;
this.hub = hub;
this.voltage = null;
this.percent = null;
this.rotate_position = parseFloat(rotatePosition || 0) || 0;
return this;
}
Cube.prototype.trigger = function (obj, name) {
obj[name] = true;
setTimeout(() => {
const _obj = {};
_obj[name] = false;
this.hub.emit('data', this.sid, this.type, _obj);
}, 300);
};
Cube.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.voltage) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.status) {
// flip90, flip180, move, tap_twice, shake_air, swing, alert, free_fall, rotate_left, rotate_right
this.trigger(obj, data.status);
newData = true;
}
if (data.rotate) {
// rotate
obj.rotate = parseFloat(data.rotate.replace(',', '.')) || 0;
if (obj.rotate >= 0) {
this.trigger(obj, 'rotate_right');
} else if (obj.rotate < 0) {
this.trigger(obj, 'rotate_left');
}
this.rotate_position += obj.rotate;
if (this.rotate_position < 0) this.rotate_position = 0;
if (this.rotate_position > 100) this.rotate_position = 100;
obj.rotate_position = this.rotate_position;
newData = true;
}
return newData ? obj : null;
};
Cube.prototype.Control = function (attr, val) {
if (attr === 'rotate_position') {
val = parseFloat(val);
if (val < 0) val = 0;
if (val > 100) val = 100;
if (this.rotate_position !== val) {
this.hub.emit('data', this.sid, this.type, {rotate_position: this.rotate_position});
}
}
};
Cube.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Cube.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = Cube;

90
lib/Sensors/Curtain.js Normal file
View File

@ -0,0 +1,90 @@
'use strict';
function Curtain(sid, ip, hub) {
this.type = 'curtain';
this.sid = sid;
this.hub = hub;
this.ip = ip;
this.curtain_level = null;
}
Curtain.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.curtain_level) {
this.curtain_level = parseFloat(data.curtain_level);
obj.curtain_level = this.curtain_level;
newData = true;
}
if (data.status) {
if (this.status === 'open') {
obj.open = true;
newData = true;
} else
if (this.status === 'close') {
obj.close = true;
newData = true;
} else
if (this.status === 'stop') {
obj.stop = true;
newData = true;
} else {
this.hub.emit('warning', 'Unknown status "' + this.status + '"');
}
}
return newData ? obj : null;
};
Curtain.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Curtain.prototype.Control = function (attr, value) {
let message;
if (attr === 'stop' || attr === 'open' || attr === 'close') {
message = {
cmd: 'write',
model: this.type,
sid: this.sid,
short_id: 0,
data: {
status: attr,
key: this.hub.getKey(this.ip)
}
};
} else if (attr === 'curtain_level') {
message = {
cmd: 'write',
model: this.type,
sid: this.sid,
short_id: 0,
data: {
curtain_level: value.toString(), //Strange thing, working only if string.
key: this.hub.getKey(this.ip)
}
};
} else {
this.hub.emit('warning', 'Unknown control attribute "' + attr + '"');
return;
}
this.hub.sendMessage(message, this.ip);
};
Curtain.prototype.onMessage = function (message) {
if (message.data) {
if (message.data.status) {
this.on = message.data.status === 'on';
this.hub.emit('data', this.sid, this.type, {
state: this.on
});
}
}
};
module.exports = Curtain;

54
lib/Sensors/DoorSensor.js Normal file
View File

@ -0,0 +1,54 @@
'use strict';
function DoorSensor(sid, ip, hub) {
this.type = 'magnet';
this.sid = sid;
this.hub = hub;
this.ip = ip;
this.opened = null;
this.voltage = null;
this.percent = null;
}
DoorSensor.prototype.getData = function (data, isHeartbeat) {
let newData = false;
let obj = {};
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.status && data.status !== 'unknown' && !isHeartbeat) {
this.opened = data.status !== 'close';
obj.state = this.opened;
newData = true;
}
return newData ? obj : null;
};
DoorSensor.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data, true);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
DoorSensor.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = DoorSensor;

242
lib/Sensors/Gateway.js Normal file
View File

@ -0,0 +1,242 @@
'use strict';
function Gateway(sid, ip, hub) {
this.type = 'gateway';
this.sid = sid;
this.ip = ip;
this.hub = hub;
this.token = null;
this.illumination = null;
this.connected = false;
this.lastValues = {
rgb: '#FFFFFF',
dimmer: 100
};
this.rgb = null;
this.dimmer = null;
this.on = null;
this.hub.sendMessage({cmd: 'get_id_list', sid: sid}, this.ip);
this.timeout = null;
return this;
}
Gateway.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.illumination !== undefined) {
this.illumination = parseFloat(data.illumination);
obj.illumination = this.illumination;
newData = true;
}
if (!this.connected) {
this.connected = true;
obj.connected = this.connected;
newData = true;
}
// Start timeout to detect dicsonnect
this.timeout && clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.timeout = null;
this.hub.emit('data', this.sid, this.type, {connected: false});
}, 20000);
if (data.rgb !== undefined) {
let rgb = parseInt(data.rgb, 10);
if (!rgb) {
this.rgb = '#000000';
this.dimmer = 0;
this.on = false;
} else {
rgb = rgb.toString(16);
if (rgb.length === 7) {
rgb = '0' + rgb;
} else if (rgb.length === 6) {
rgb = '00' + rgb;
} else if (rgb.length === 5) {
rgb = '000' + rgb;
} else if (rgb.length === 4) {
rgb = '0000' + rgb;
} else if (rgb.length === 3) {
rgb = '00000' + rgb;
} else if (rgb.length === 2) {
rgb = '000000' + rgb;
} else if (rgb.length === 1) {
rgb = '0000000' + rgb;
}
this.dimmer = parseInt(rgb.substring(0, 2), 16);
this.rgb = '#' + rgb.substring(2).toUpperCase();
this.on = true;
}
obj.on = this.on;
obj.dimmer = this.dimmer;
obj.rgb = this.rgb;
// remember last non null values
if (obj.dimmer) {
this.lastValues.dimmer = obj.dimmer;
}
if (parseInt(obj.rgb.replace('#', ''), 16)) {
this.lastValues.rgb = obj.rgb;
}
newData = true;
}
return newData ? obj : null;
};
Gateway.prototype.heartBeat = function (token, data) {
if (token) {
this.token = token;
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Gateway.prototype.onMessage = function (message) {
if (message.cmd === 'get_id_list_ack') {
this.initSensors(message.data);
} else if (message.cmd === 'write_ack') {
if (message.data.error) {
this.hub.emit('error', message.data.error);
}
const obj_ = this.getData(message.data);
if (obj_) {
this.hub.emit('data', this.sid, this.type, obj_);
}
} else if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Gateway.prototype.Control = function (attr, value) {
if (attr === 'on' || attr === 'dimmer' || attr === 'rgb') {
if (this.dimmer === null) {
this.dimmer = this.lastValues.dimmer;
}
if (this.rgb === null) {
this.rgb = this.lastValues.rgb;
}
if (this.on === null) {
this.on = true;
}
if (attr === 'on') {
this.on = !!value;
if (this.on) {
if (!parseInt(this.rgb.replace('#', ''), 16)) {
this.rgb = this.lastValues.rgb;
}
if (!this.dimmer) {
this.dimmer = this.lastValues.dimmer;
}
}
}
if (attr === 'dimmer') {
this.dimmer = value;
if (this.dimmer < 0) this.dimmer = 0;
if (this.dimmer > 100) this.dimmer = 100;
if (this.dimmer) {
this.on = true;
if (!parseInt(this.rgb.replace('#', ''), 16)) {
this.rgb = this.lastValues.rgb;
}
}
}
if (attr === 'rgb') {
this.rgb = value;
if (parseInt(this.rgb.replace('#', ''), 16)) {
this.on = true;
if (!this.dimmer) {
this.dimmer = this.lastValues.dimmer;
}
}
}
if (this.timer) clearTimeout(this.timer);
// collect data before send
this.timer = setTimeout(() => {
this.timer = null;
let value;
if (!this.on || !this.dimmer || this.rgb === '000000' || this.rgb === '#000000') {
value = 0;
} else {
value = (this.dimmer << 24) | parseInt(this.rgb.replace('#', ''), 16);
}
const message = {
cmd: 'write',
model: this.type,
sid: this.sid,
short_id: 0,
data: {
rgb: value,
key: this.hub.getKey(this.ip)
}
};
this.hub.sendMessage(message, this.ip);
}, 200);
} else if (attr === 'volume') {
if (value < 0) value = 0;
if (value > 100) value = 100;
const message = {
cmd: 'write',
model: this.type,
sid: this.sid,
short_id: 0,
data: {
mid: this.mid || 999,
vol: value,
key: this.hub.getKey(this.ip)
}
};
this.hub.sendMessage(message, this.ip);
} else if (attr === 'mid') {
this.mid = value;
const message = {
cmd: 'write',
model: this.type,
sid: this.sid,
short_id: 0,
data: {
mid: value,
key: this.hub.getKey(this.ip)
}
};
this.hub.sendMessage(message, this.ip);
} else {
this.hub.emit('warning', 'Unknown attribute ' + attr);
}
};
Gateway.prototype.initSensors = function (sids) {
this.hub.sendMessage({cmd: 'read', sid: this.sid}, this.ip);
for (let i = 0; i < sids.length; i++) {
this.hub.sendMessage({cmd: 'read', sid: sids[i]}, this.ip);
}
};
module.exports = Gateway;

78
lib/Sensors/Lock.js Normal file
View File

@ -0,0 +1,78 @@
'use strict';
function Lock(sid, ip, hub) {
this.type = 'lock_aq1';
this.sid = sid;
this.ip = ip;
this.hub = hub;
this.fing_verified = null;
this.psw_verified = null;
this.card_verified = null;
this.verified_wrong = null;
}
// {'cmd': 'read_ack', 'model': 'lock.aq1', 'sid': '158d000222****', 'short_id': 55***, 'data': '{"voltage":3387}'}
// {'cmd': 'read_ack', 'model': 'lock.aq1', 'sid': '158d000222****', 'short_id': 55***, 'data': '{"fing_verified": 65536}'}
// {'cmd': 'read_ack', 'model': 'lock.aq1', 'sid': '158d000222****', 'short_id': 55***, 'data': '{"fing_verified": 65537}'}
// {'cmd': 'read_ack', 'model': 'lock.aq1', 'sid': '158d000222****', 'short_id': 55***, 'data': '{"psw_verified": 131074}'}
// {'cmd': 'read_ack', 'model': 'lock.aq1', 'sid': '158d000222****', 'short_id': 55***, 'data': '{"card_verified": 196608}'}
// {'cmd': 'report', 'model': 'lock.aq1', 'sid': '158d000222****', 'short_id': 55***, 'data': '{"verified_wrong":"3"}'}
Lock.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.fing_verified) {
this.fing_verified = parseInt(data.fing_verified);
obj.fing_verified = this.fing_verified;
newData = true;
}
if (data.psw_verified) {
this.psw_verified = parseInt(data.psw_verified);
obj.psw_verified = this.psw_verified;
newData = true;
}
if (data.card_verified) {
this.card_verified = parseInt(data.card_verified);
obj.card_verified = this.card_verified;
newData = true;
}
if (data.verified_wrong) {
this.verified_wrong = parseInt(data.verified_wrong);
obj.verified_wrong = this.verified_wrong;
newData = true;
}
return newData ? obj : null;
};
Lock.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Lock.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = Lock;

View File

@ -0,0 +1,70 @@
'use strict';
function MotionSensor(sid, ip, hub, model) {
this.type = model;
this.sid = sid;
this.ip = ip;
this.hub = hub;
this.motion = null;
this.voltage = null;
this.percent = null;
}
MotionSensor.prototype.getData = function (data, isHeartbeat) {
let newData = false;
let obj = {};
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.status && !isHeartbeat) {
this.motion = data.status === 'motion';
obj.state = this.motion;
if (this.motion) {
this.no_motion = 0;
obj.no_motion = 0;
}
newData = true;
}
if (data.no_motion !== undefined && !isHeartbeat) {
this.no_motion = parseInt(data.no_motion, 10);
obj.no_motion = this.no_motion;
obj.state = !this.no_motion;
newData = true;
}
if (data.lux !== undefined) {
this.lux = parseInt(data.lux, 10);
obj.lux = this.lux;
newData = true;
}
return newData ? obj : null;
};
MotionSensor.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data, true);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
MotionSensor.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = MotionSensor;

83
lib/Sensors/Plug.js Normal file
View File

@ -0,0 +1,83 @@
'use strict';
function Plug (sid, ip, hub) {
this.type = 'plug';
this.sid = sid;
this.ip = ip;
this.hub = hub;
this.on = null;
this.load_power = null;
this.power_consumed = null;
this.state = null;
this.inuse = null;
}
// {"cmd":"report","model":"plug","sid":"dfdfd","short_id":52239,"data":{"status":"on"}}
// {"cmd":"report","model":"plug","sid":"dfdfdf","short_id":52239,"data":{"status":"off"}}
// {"cmd":"heartbeat","model":"plug","sid":"fdfdfd","short_id":52239,"data":{"voltage":3600,"status":"on","inuse":"0","power_consumed":"26326","load_power":"0.00"}}
Plug.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.load_power) {
this.load_power = parseFloat(data.load_power);
obj.load_power = this.load_power;
newData = true;
}
if (data.power_consumed) {
this.power_consumed = parseFloat(data.power_consumed);
obj.power_consumed = this.power_consumed;
newData = true;
}
if (data.inuse) {
this.inuse = !!parseInt(data.inuse, 10);
obj.inuse = this.inuse;
newData = true;
}
if (data.status) {
this.state = data.status === 'on';
obj.state = this.state;
newData = true;
}
return newData ? obj : null;
};
Plug.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
Plug.prototype.Control = function (attr, value) {
if (attr !== 'channel_0' && attr !== 'state') {
this.hub.emit('warning', 'Unknown attribute ' + attr);
return;
}
const message = {
cmd: 'write',
model: this.type,
sid: this.sid,
short_id: 0,
data: {
channel_0: value ? 'on' : 'off',
key: this.hub.getKey(this.ip)
}
};
this.hub.sendMessage(message, this.ip);
};
Plug.prototype.onMessage = function (message) {
if (message.data) {
if (message.data.status) {
this.on = message.data.status === 'on';
this.hub.emit('data', this.sid, this.type, {
state: this.on
});
}
}
};
module.exports = Plug;

89
lib/Sensors/THSensor.js Normal file
View File

@ -0,0 +1,89 @@
'use strict';
function THSensor(sid, ip, hub, model, options) {
this.type = model;
this.sid = sid;
this.hub = hub;
this.ip = ip;
this.interval = parseInt((options && options.interval) || 5000, 10) || 0;
this.temperature = null;
this.humidity = null;
this.voltage = null;
this.percent = null;
this.lastData = null;
}
THSensor.prototype.getData = function (data) {
let newData = false;
let obj = {};
const ts = Date.now();
if (this.interval && this.lastData) {
const diff = ts - this.lastData;
if (diff > 200 && diff < this.interval) {
obj.doublePress = true;
// deactivate after 300 ms
setTimeout(() => this.hub.emit('data', this.sid, this.type, {doublePress: false}), 300);
this.lastData = null;
} else {
this.lastData = ts;
}
} else {
this.lastData = ts;
}
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.temperature === '10000' || data.temperature === 10000) {
// ignore all values if 10000 as temperature.
return null;
}
if (data.temperature !== undefined) {
this.temperature = parseInt(data.temperature) / 100;
obj.temperature = this.temperature;
newData = true;
}
if (data.humidity !== undefined) {
this.humidity = parseInt(data.humidity) / 100;
obj.humidity = this.humidity;
newData = true;
}
if (data.pressure !== undefined) {
this.pressure = parseInt(data.pressure) / 100;
obj.pressure = this.pressure;
newData = true;
}
return newData ? obj : null;
};
THSensor.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
THSensor.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = THSensor;

View File

@ -0,0 +1,84 @@
'use strict';
function VibrationSensor(sid, ip, hub, model) {
this.type = model;
this.sid = sid;
this.ip = ip;
this.hub = hub;
this.vibration = null;
this.orientationX = null;
this.orientationY = null;
this.orientationZ = null;
this.bed_activity = null;
this.tilt_angle = null;
this.voltage = null;
this.percent = null;
}
VibrationSensor.prototype.getData = function (data, isHeartbeat) {
let newData = false;
let obj = {};
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.status && !isHeartbeat) {
this.vibration = data.status === 'vibration' || data.status === 'vibrate';
if (this.vibration) {
setTimeout(() => this.hub.emit('data', this.sid, this.type, {state: false}), 300);
}
obj.state = this.vibration;
newData = true;
}
if (data.final_tilt_angle !== undefined) {
this.tilt_angle = parseInt(data.final_tilt_angle, 10);
obj.tilt_angle = this.tilt_angle;
newData = true;
}
if (data.coordination !== undefined) {
const parts = data.coordination.split(',').map(num => parseInt(num.trim()));
this.orientationX = parts[0];
this.orientationY = parts[1];
this.orientationZ = parts[2];
obj.orientationX = this.orientationX;
obj.orientationY = this.orientationY;
obj.orientationZ = this.orientationZ;
newData = true;
}
if (data.bed_activity !== undefined) {
this.bed_activity = parseInt(data.bed_activity, 10);
obj.bed_activity = this.bed_activity;
newData = true;
}
return newData ? obj : null;
};
VibrationSensor.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data, true);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
VibrationSensor.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = VibrationSensor;

View File

@ -0,0 +1,89 @@
'use strict';
function WallButtons(sid, ip, hub, model) {
this.type = model;
this.sid = sid;
this.hub = hub;
this.ip = ip;
this.channel_0 = null;
this.channel_1 = null;
this.dual_channel = null;
this.voltage = null;
this.percent = null;
return this;
}
WallButtons.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
if (data.channel_0) {
obj.channel_0_double = data.channel_0 === 'double_click';
this.channel_0 = data.channel_0 === 'click';
obj.channel_0 = this.channel_0;
newData = true;
if (obj.channel_0) {
setTimeout(() => this.hub.emit('data', this.sid, this.type, {channel_0: false}), 300);
}
if (obj.channel_0_double) {
setTimeout(() => this.hub.emit('data', this.sid, this.type, {channel_0_double: false}), 300);
}
}
if (data.channel_1) {
obj.channel_1_double = data.channel_1 === 'double_click';
this.channel_1 = data.channel_1 === 'click';
obj.channel_1 = this.channel_1;
newData = true;
if (obj.channel_1) {
setTimeout(() => this.hub.emit('data', this.sid, this.type, {channel_1: false}), 300);
}
if (obj.channel_1_double) {
setTimeout(() => this.hub.emit('data', this.sid, this.type, {channel_1_double: false}), 300);
}
}
if (data.dual_channel) {
this.dual_channel = data.dual_channel === 'both_click';
obj.dual_channel = this.dual_channel;
newData = true;
if (obj.dual_channel) {
setTimeout(() => this.hub.emit('data', this.sid, this.type, {dual_channel: false}), 300);
}
}
return newData ? obj : null;
};
WallButtons.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
WallButtons.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = WallButtons;

View File

@ -0,0 +1,70 @@
'use strict';
function WallWiredSwitch(sid, ip, hub, model) {
this.type = model;
this.sid = sid;
this.hub = hub;
this.ip = ip;
this.channel_0 = null;
this.channel_1 = null;
return this;
}
WallWiredSwitch.prototype.getData = function (data) {
let newData = false;
let obj = {};
if (data.channel_0) {
this.channel_0 = data.channel_0 === 'on';
obj.channel_0 = this.channel_0;
newData = true;
}
if (data.channel_1) {
this.channel_1 = data.channel_1 === 'on';
obj.channel_1 = this.channel_1;
newData = true;
}
return newData ? obj : null;
};
WallWiredSwitch.prototype.Control = function (attr, value) {
if (attr !== 'channel_0' && attr !== 'channel_1') {
this.hub.emit('warning', 'Unknown attribute ' + attr);
return;
}
const message = {
cmd: 'write',
model: this.type,
sid: this.sid,
short_id: 0,
data: {
key: this.hub.getKey(this.ip)
}
};
message.data[attr] = value ? 'on' : 'off';
this.hub.sendMessage(message, this.ip);
};
WallWiredSwitch.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
WallWiredSwitch.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = WallWiredSwitch;

View File

@ -0,0 +1,57 @@
'use strict';
function WaterSensor(sid, ip, hub) {
this.type = 'sensor_wleak.aq1';
this.sid = sid;
this.hub = hub;
this.ip = ip;
this.motion = null;
this.voltage = null;
this.percent = null;
}
WaterSensor.prototype.getData = function (data, isHeartbeat) {
let newData = false;
let obj = {};
if (data.voltage !== undefined) {
data.voltage = parseInt(data.voltage, 10);
this.voltage = data.voltage / 1000;
this.percent = ((data.voltage - 2200) / 10);
if (this.percent > 100) {
this.percent = 100;
}
if (this.percent < 0) {
this.percent = 0;
}
obj.voltage = this.voltage;
obj.percent = this.percent;
newData = true;
}
// {“cmd”:“report”,“model”:“sensor_wleak.aq1”,“sid”:“aaa000xxxxxxx”,“short_id”:12345,“data”:"{“status”:“leak”}"}
// {“cmd”:“report”,“model”:“sensor_wleak.aq1”,“sid”:“aaa000xxxxxxx”,“short_id”:12345,“data”:"{“status”:“no_leak”}"}
if (data.status && !isHeartbeat) {
this.leak = data.status === 'leak';
obj.state = this.leak;
newData = true;
}
return newData ? obj : null;
};
WaterSensor.prototype.heartBeat = function (token, data) {
if (data) {
const obj = this.getData(data, true);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
WaterSensor.prototype.onMessage = function (message) {
if (message.data) {
const obj = this.getData(message.data);
if (obj) {
this.hub.emit('data', this.sid, this.type, obj);
}
}
};
module.exports = WaterSensor;

370
lib/devices.js Normal file
View File

@ -0,0 +1,370 @@
'use strict';
const Gateway = require('./Sensors/Gateway');
const THSensor = require('./Sensors/THSensor');
const DoorSensor = require('./Sensors/DoorSensor');
const MotionSensor = require('./Sensors/MotionSensor');
const VibrationSensor = require('./Sensors/VibrationSensor');
const Plug = require('./Sensors/Plug');
const Button = require('./Sensors/Button');
const Cube = require('./Sensors/Cube');
const WallButtons = require('./Sensors/WallButtons');
const WallWiredSwitch = require('./Sensors/WallWiredSwitch');
const Alarm = require('./Sensors/Alarm');
const Curtain = require('./Sensors/Curtain');
const WaterSensor = require('./Sensors/WaterSensor');
const Lock = require('./Sensors/Lock');
const states = {
voltage: {name: 'Battery voltage', role: 'battery.voltage', write: false, read: true, type: 'number', unit: 'V', icon: '/icons/battery_v.png', desc: 'Battery voltage'},
percent: {name: 'Battery percent', role: 'battery.percent', write: false, read: true, type: 'number', unit: '%', icon: '/icons/battery_p.png', desc: 'Battery level in percent', min: 0, max: 100},
temperature: {name: 'Temperature', role: 'value.temperature', write: false, read: true, type: 'number', unit: '°C'},
humidity: {name: 'Humidity', role: 'value.humidity', write: false, read: true, type: 'number', unit: '%', min: 0, max: 100},
doublePress: {name: 'Double press', role: 'state', write: false, read: true, type: 'boolean', desc: 'You can press connect button twice'},
opened: {name: 'Is opened', role: 'state', write: false, read: true, type: 'boolean'},
description: {name: 'Alarm description', role: 'state', write: false, read: true, type: 'string'},
motion: {name: 'Is motion', role: 'indicator.motion', write: false, read: true, type: 'boolean'},
no_motion: {name: 'Last motion', role: 'state', write: false, read: true, type: 'number', unit: 'seconds', desc: 'Last motion for at least X seconds'},
fing_verified: {name: 'Finger verified', role: 'value', write: false, read: true, type: 'number'},
psw_verified: {name: 'Password verified', role: 'value', write: false, read: true, type: 'number'},
card_verified: {name: 'Card verified', role: 'value', write: false, read: true, type: 'number'},
verified_wrong: {name: 'Wrong verification', role: 'value', write: false, read: true, type: 'number'},
click: {name: 'Simple click', role: 'button', write: false, read: true, type: 'boolean'},
double: {name: 'Double click', role: 'button', write: false, read: true, type: 'boolean'},
long: {name: 'Long click', role: 'button', write: false, read: true, type: 'boolean'},
channel_0: {name: 'First button pressed', role: 'button', write: false, read: true, type: 'boolean'},
channel_0_double: {name: 'First button pressed double', role: 'button', write: false, read: true, type: 'boolean'},
channel_1: {name: 'Second button pressed', role: 'button', write: false, read: true, type: 'boolean'},
channel_1_double: {name: 'Second button pressed double', role: 'button', write: false, read: true, type: 'boolean'},
dual_channel: {name: 'Both buttons pressed', role: 'button', write: false, read: true, type: 'boolean'},
power: {name: 'Socket plug', role: 'switch', write: true, read: true, type: 'boolean'},
load_power: {name: 'Load power', role: 'value.power', write: false, read: true, type: 'number', unit: 'W'},
power_consumed: {name: 'Power consumed', role: 'value.consumption', write: false, read: true, type: 'number', unit: 'W'},
inuse: {name: 'Is in use', role: 'state', write: false, read: true, type: 'number'},
wall_switch: {name: 'Wall switch', role: 'switch', write: true, read: true, type: 'boolean'},
wall_switch0: {name: 'Wall switch 0', role: 'switch', write: true, read: true, type: 'boolean'},
wall_switch1: {name: 'Wall switch 1', role: 'switch', write: true, read: true, type: 'boolean'},
rotate: {name: 'Rotation angle', role: 'state', write: false, read: true, type: 'number'},
rotate_position: {name: 'Rotation angle', role: 'state', write: true, read: true, type: 'number', min: 0, max: 100, unit: '%'},
flip90: {name: 'Flip on 90°', role: 'button', write: false, read: true, type: 'boolean'},
flip180: {name: 'Flip on 180°', role: 'button', write: false, read: true, type: 'boolean'},
move: {name: 'Move action', role: 'button', write: false, read: true, type: 'boolean'},
tap_twice: {name: 'Tapped twice', role: 'button', write: false, read: true, type: 'boolean'},
shake_air: {name: 'Shaken in air', role: 'button', write: false, read: true, type: 'boolean'},
swing: {name: 'Swing action', role: 'button', write: false, read: true, type: 'boolean'},
alert: {name: 'Alert action', role: 'button', write: false, read: true, type: 'boolean'},
free_fall: {name: 'Free fall action', role: 'button', write: false, read: true, type: 'boolean'},
rotate_left: {name: 'Rotate left', role: 'button', write: false, read: true, type: 'boolean'},
rotate_right: {name: 'Rotate right', role: 'button', write: false, read: true, type: 'boolean'}
};
// type - name as delivered by gateway
// fullName - Name of the device
// ClassName - handler class
// states - list of states for this sensor
const devices = {
gateway: {type: 'gateway', fullName: 'Xiaomi RGB Gateway', ClassName: Gateway, states: {
illumination: {name: 'Illumination', role: 'value.lux', write: false, read: true, type: 'number', unit: 'lux'},
rgb: {name: 'RGB', role: 'level.rgb', write: true, read: true, type: 'string'},
on: {name: 'Light', role: 'switch', write: true, read: true, type: 'boolean'},
dimmer: {name: 'Light', role: 'level.dimmer', write: true, read: true, type: 'number', unit: '%', min: 0, max: 100},
volume: {name: 'Volume', role: 'level.volume', write: true, read: true, type: 'number', unit: '%', min: 0, max: 100},
mid: {name: 'Music ID', role: 'state', write: true, read: false, type: 'number', desc: '10000 - stop, 10005 - custom ringtone'},
connected: {name: 'Is gateway connected', role: 'indicator.reachable', write: true, read: false, type: 'boolean', desc: 'Will be set to false if no packets received in 20 seconds'}
}
},
th: {type: 'sensor_ht', fullName: 'Xiaomi Temperature/Humidity', ClassName: THSensor, states: {
voltage: states.voltage,
percent: states.percent,
temperature: states.temperature,
humidity: states.humidity,
doublePress: states.doublePress
}
},
weather: {type: 'weather.v1', fullName: 'Xiaomi Temperature/Humidity/Pressure', ClassName: THSensor, states: {
voltage: states.voltage,
percent: states.percent,
temperature: states.temperature,
humidity: states.humidity,
pressure: {name: 'Pressure', role: 'value.pressure', write: false, read: true, type: 'number', unit: 'Pa', min: 0, max: 100},
doublePress: states.doublePress
}
},
button: {type: 'switch', fullName: 'Xiaomi Wireless Switch', ClassName: Button, states: {
voltage: states.voltage,
percent: states.percent,
click: states.click,
double: states.double,
long: states.long
}
},
button2: {type: 'sensor_switch.aq2', fullName: 'Xiaomi Wireless Switch Sensor', ClassName: Button, states: {
voltage: states.voltage,
percent: states.percent,
click: states.click,
double: states.double,
long: states.long
}
},
button3: {type: 'sensor_switch.aq3', fullName: 'Xiaomi Wireless Switch Sensor', ClassName: Button, states: {
voltage: states.voltage,
percent: states.percent,
click: states.click,
double: states.double,
long: states.long
}
},
button4: {type: 'remote.b1acn01', fullName: 'Xiaomi Aqara Smart Wireless Switch', ClassName: Button, states: {
voltage: states.voltage,
percent: states.percent,
click: states.click,
double: states.double,
long: states.long
}
},
plug: {type: 'plug', fullName: 'Xiaomi Smart Plug', ClassName: Plug, states: {
voltage: states.voltage,
percent: states.percent,
state: states.power,
load_power: states.load_power,
power_consumed: states.power_consumed,
inuse: states.inuse
}
},
plug86: {type: '86plug', fullName: 'Xiaomi Smart Wall Plug', ClassName: Plug, states: {
voltage: states.voltage,
percent: states.percent,
state: states.power,
load_power: states.load_power,
power_consumed: states.power_consumed,
inuse: states.inuse
}
},
remote_b286acn01: {type: 'remote.b286acn01', fullName: 'Xiaomi Aqara Wireless Remote Switch (Double Rocker)', ClassName: WallButtons, states: {
voltage: states.voltage,
percent: states.percent,
channel_0: states.channel_0,
channel_1: states.channel_1,
dual_channel: states.dual_channel,
channel_0_double: states.channel_0_double,
channel_1_double: states.channel_1_double
}
},
remote_b186acn01: {type: 'remote.b186acn01', fullName: 'Xiaomi Aqara Wireless Remote Switch (Single Rocker)', ClassName: WallButtons, states: {
voltage: states.voltage,
percent: states.percent,
channel_0: states.channel_0,
channel_0_double: states.channel_0_double
}
},
sw2_86: {type: '86sw2', fullName: 'Xiaomi Wireless Dual Wall Switch', ClassName: WallButtons, states: {
voltage: states.voltage,
percent: states.percent,
channel_0: states.channel_0,
channel_1: states.channel_1,
dual_channel: states.dual_channel,
channel_0_double: states.channel_0_double,
channel_1_double: states.channel_1_double
}
},
sw1_86: {type: '86sw1', fullName: 'Xiaomi Wireless Single Wall Switch', ClassName: WallButtons, states: {
voltage: states.voltage,
percent: states.percent,
channel_0: states.click,
channel_0_double: states.double
}
},
natgas: {type: 'natgas', fullName: 'Xiaomi Mijia Honeywell Gas Alarm Detector', ClassName: Alarm, states: {
voltage: states.voltage,
percent: states.percent,
state: {name: 'Alarm state', role: 'indicator.alarm.CO2', write: false, read: true, type: 'boolean'},
description: states.description
}
},
smoke: {type: 'smoke', fullName: 'Xiaomi Mijia Honeywell Fire Alarm Detector', ClassName: Alarm, states: {
voltage: states.voltage,
percent: states.percent,
state: {name: 'Alarm state', role: 'indicator.alarm.fire', write: false, read: true, type: 'boolean'},
description: states.description
}
},
ctrl_ln1: {type: 'ctrl_ln1', fullName: 'Xiaomi Aqara 86 Fire Wall Switch One Button', ClassName: WallWiredSwitch, states: {
channel_0: states.wall_switch
}
},
ctrl_ln1_aq1: {type: 'ctrl_ln1.aq1', fullName: 'Xiaomi Aqara Wall Switch LN', ClassName: WallWiredSwitch, states: {
channel_0: states.wall_switch
}
},
ctrl_ln2: {type: 'ctrl_ln2', fullName: 'Xiaomi 86 zero fire wall switch double key', ClassName: WallWiredSwitch, states: {
channel_0: states.wall_switch0,
channel_1: states.wall_switch1
}
},
ctrl_ln2_aq1: {type: 'ctrl_ln2.aq1', fullName: 'Xiaomi Aqara Wall Switch LN double key', ClassName: WallWiredSwitch, states: {
channel_0: states.wall_switch0,
channel_1: states.wall_switch1
}
},
ctrl_86plug_aq1: {type: 'ctrl_86plug.aq1', fullName: 'Xiaomi Aqara Wall Socket', ClassName: Plug, states: {
voltage: states.voltage,
percent: states.percent,
state: states.power,
load_power: states.load_power,
power_consumed: states.power_consumed,
inuse: states.inuse
}
},
ctrl_neutral2: {type: 'ctrl_neutral2', fullName: 'Xiaomi Wired Dual Wall Switch', ClassName: WallWiredSwitch, states: {
channel_0: states.wall_switch0,
channel_1: states.wall_switch1
}
},
ctrl_neutral1: {type: 'ctrl_neutral1', fullName: 'Xiaomi Wired Single Wall Switch', ClassName: WallWiredSwitch, states: {
channel_0: states.wall_switch
}
},
cube: {type: 'cube', fullName: 'Xiaomi Cube', ClassName: Cube, states: {
voltage: states.voltage,
percent: states.percent,
rotate: states.rotate,
rotate_position: states.rotate_position,
flip90: states.flip90,
flip180: states.flip180,
move: states.move,
tap_twice: states.tap_twice,
shake_air: states.shake_air,
swing: states.swing,
alert: states.alert,
free_fall: states.free_fall,
rotate_left: states.rotate_left,
rotate_right: states.rotate_right,
}
},
cube2: {type: 'sensor_cube.aqgl01', fullName: 'Xiaomi Cube 01', ClassName: Cube, states: {
voltage: states.voltage,
percent: states.percent,
rotate: states.rotate,
rotate_position: states.rotate_position,
flip90: states.flip90,
flip180: states.flip180,
move: states.move,
tap_twice: states.tap_twice,
shake_air: states.shake_air,
swing: states.swing,
alert: states.alert,
free_fall: states.free_fall,
rotate_left: states.rotate_left,
rotate_right: states.rotate_right,
}
},
magnet: {type: 'magnet', fullName: 'Xiaomi Door Sensor', ClassName: DoorSensor, states: {
voltage: states.voltage,
percent: states.percent,
state: states.opened
}
},
magnet2: {type: 'sensor_magnet.aq2', fullName: 'Xiaomi Door Sensor', ClassName: DoorSensor, states: {
voltage: states.voltage,
percent: states.percent,
state: states.opened
}
},
curtain: {type: 'curtain', fullName: 'Xiaomi Aqara Smart Curtain', ClassName: Curtain, states: {
curtain_level: {name: 'Curtain level', role: 'level.blinds', write: true, read: true, type: 'number', min: 0, max: 100, unit: '%'},
open: {name: 'Open', role: 'button.open', write: true, read: false, type: 'boolean'},
close: {name: 'Close', role: 'button.close', write: true, read: false, type: 'boolean'},
stop: {name: 'Stop', role: 'button.stop', write: true, read: false, type: 'boolean'}
}
},
motion: {type: 'motion', fullName: 'Xiaomi Motion Sensor', ClassName: MotionSensor, states: {
voltage: states.voltage,
percent: states.percent,
state: states.motion,
no_motion: states.no_motion
}
},
lock_aq1: {type: 'lock.aq1', fullName: 'Xiaomi Lock', ClassName: Lock, states: {
voltage: states.voltage,
percent: states.percent,
fing_verified: states.fing_verified,
psw_verified: states.psw_verified,
card_verified: states.card_verified,
verified_wrong: states.verified_wrong
}
},
lock_v1: {type: 'lock.v1', fullName: 'Xiaomi Vima Smart Lock', ClassName: Lock, states: {
voltage: states.voltage,
percent: states.percent,
fing_verified: states.fing_verified,
psw_verified: states.psw_verified,
card_verified: states.card_verified,
verified_wrong: states.verified_wrong
}
},
motion2: {type: 'sensor_motion.aq2', fullName: 'Xiaomi Motion Sensor', ClassName: MotionSensor, states: {
voltage: states.voltage,
percent: states.percent,
state: states.motion,
no_motion: states.no_motion,
lux: {name: 'Brightness', role: 'indicator.brightness', write: false, read: true, type: 'number', unit: 'lux'}
}
},
vibration: {type: 'vibration', fullName: 'Xiaomi Vibration Sensor', ClassName: VibrationSensor, states: {
voltage: states.voltage,
percent: states.percent,
state: {name: 'Is vibration', role: 'indicator.vibration', write: false, read: true, type: 'boolean', desc: 'Last tilt angle'},
tilt_angle: {name: 'Tilt angle', role: 'value', write: false, read: true, type: 'number', unit: '°'},
orientationX: {name: 'Orientation X', role: 'value', write: false, read: true, type: 'number', desc: 'Last X orientation'},
orientationY: {name: 'Orientation Y', role: 'value', write: false, read: true, type: 'number', desc: 'Last Y orientation'},
orientationZ: {name: 'Orientation Z', role: 'value', write: false, read: true, type: 'number', desc: 'Last Z orientation'},
bed_activity: {name: 'Bed activity', role: 'value', write: false, read: true, type: 'number', desc: 'Last bed activity'}
}
},
wleak1: {type: 'sensor_wleak.aq1', fullName: 'Xiaomi Aqara Water Sensor', ClassName: WaterSensor, states: {
voltage: states.voltage,
percent: states.percent,
state: {name: 'Is water detected', role: 'indicator.leakage', write: false, read: true, type: 'boolean'}
}
},
};
module.exports = devices;

14
lib/package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "homebridge-mi-home",
"version": "0.0.1",
"description": "Mi home plugin for Homebridge.",
"license": "MIT",
"keywords": [
"homebridge-plugin", "xiaomi", "mi", "mi home"
],
"engines": {
"node": ">=0.12.0",
"homebridge": ">=0.2.0",
"miio": ">=0.12.0"
}
}

83
lib/utils.js Normal file
View File

@ -0,0 +1,83 @@
'use strict';
const fs = require('fs');
const path = require('path');
let controllerDir;
let appName;
/**
* returns application name
*
* The name of the application can be different and this function finds it out.
*
* @returns {string}
*/
function getAppName() {
const parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 2].split('.')[0];
}
/**
* looks for js-controller home folder
*
* @param {boolean} isInstall
* @returns {string}
*/
function getControllerDir(isInstall) {
// Find the js-controller location
const possibilities = [
'yunkong2.js-controller',
'yunkong2.js-controller',
];
/** @type {string} */
let controllerPath;
for (const pkg of possibilities) {
try {
const possiblePath = require.resolve(pkg);
if (fs.existsSync(possiblePath)) {
controllerPath = possiblePath;
break;
}
} catch (e) { /* not found */ }
}
if (controllerPath == 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;

315
main.js Normal file
View File

@ -0,0 +1,315 @@
/**
* yunkong2 MiHome
*
* Copyright 2017-2018, bluefox <dogafox@gmail.com>
*
* License: MIT
*/
'use strict';
const utils = require('./lib/utils'); // Get common adapter utils
const MiHome = require('./lib/Hub');
const adapter = utils.Adapter('mihome');
let objects = {};
let delayed = {};
let connected = null;
let connTimeout;
let hub;
let reconnectTimeout;
let tasks = [];
adapter.on('ready', main);
adapter.on('stateChange', (id, state) => {
if (!id || !state || state.ack) {
return;
}
if (!objects[id]) {
adapter.log.warn('Unknown ID: ' + id);
return;
}
if (hub) {
const pos = id.lastIndexOf('.');
const channelId = id.substring(0, pos);
const attr = id.substring(pos + 1);
if (objects[channelId] && objects[channelId].native) {
const device = hub.getSensor(objects[channelId].native.sid);
if (device && device.Control) {
device.Control(attr, state.val);
} else {
adapter.log.warn('Cannot control ' + id);
}
} else {
adapter.log.warn('Invalid device: ' + id);
}
}
});
adapter.on('unload', callback => {
if (hub) {
try {
hub.stop(callback);
} catch (e) {
console.error('Cannot stop: ' + e);
callback && callback();
}
} else if (callback) {
callback();
}
});
adapter.on('message', obj => {
if (obj) {
switch (obj.command) {
case 'browse':
let browse = new MiHome.Hub({
port: (obj.message.port || adapter.config.port) + 1,
bind: obj.message.bind || '0.0.0.0',
browse: true
});
let result = [];
browse.on('browse', data => (result.indexOf(data.ip) === -1) && result.push(data.ip));
browse.listen();
setTimeout(() => {
browse.stop(() => {
browse = null;
if (obj.callback) adapter.sendTo(obj.from, obj.command, result, obj.callback);
});
}, 3000);
break;
}
}
});
function updateStates(sid, type, data) {
const id = adapter.namespace + '.devices.' + type.replace('.', '_') + '_' + sid;
for (const attr in data) {
if (data.hasOwnProperty(attr)) {
if (objects[id] || objects[id + '.' + attr]) {
adapter.setForeignState(id + '.' + attr, data[attr], true);
} else {
delayed[id + '.' + attr] = data[attr];
}
}
}
}
function syncObjects(callback) {
if (!tasks || !tasks.length) {
callback && callback();
return;
}
const obj = tasks.shift();
adapter.getForeignObject(obj._id, (err, oObj) => {
if (!oObj) {
objects[obj._id] = obj;
adapter.setForeignObject(obj._id, obj, () => {
if (delayed[obj._id] !== undefined) {
adapter.setForeignState(obj._id, delayed[obj._id], true, () => {
delete delayed[obj._id];
setImmediate(syncObjects, callback);
})
} else {
setImmediate(syncObjects, callback);
}
});
} else {
let changed = false;
// merge info together
for (const a in obj.common) {
if (obj.common.hasOwnProperty(a) && a !== 'name' && oObj.common[a] !== obj.common[a]) {
changed = true;
oObj.common[a] = obj.common[a];
}
}
if (JSON.stringify(obj.native) !== JSON.stringify(oObj.native)) {
changed = true;
oObj.native = obj.native;
}
objects[obj._id] = oObj;
if (changed) {
adapter.setForeignObject(oObj._id, oObj, () => {
if (delayed[oObj._id] !== undefined) {
adapter.setForeignState(oObj._id, delayed[oObj._id], true, () => {
delete delayed[oObj._id];
setImmediate(syncObjects, callback);
})
} else {
setImmediate(syncObjects, callback);
}
});
} else {
if (delayed[oObj._id] !== undefined) {
adapter.setForeignState(oObj._id, delayed[oObj._id], true, () => {
delete delayed[oObj._id];
setImmediate(syncObjects, callback);
})
} else {
// init rotate position with previous value
if (oObj._id.match(/\.rotate_position$/)) {
adapter.getForeignState(oObj._id, (err, state) => {
if (state) {
const pos = oObj._id.lastIndexOf('.');
const channelId = oObj._id.substring(0, pos);
if (objects[channelId]) {
const device = hub.getSensor(objects[channelId].native.sid);
if (device && device.Control) {
device.Control('rotate_position', state.val);
}
}
}
setImmediate(syncObjects, callback);
});
} else {
setImmediate(syncObjects, callback);
}
}
}
}
});
}
function createDevice(device, name, callback) {
const id = adapter.namespace + '.devices.' + device.type.replace('.', '_') + '_' + device.sid;
const isStartTasks = !tasks.length;
const dev = Object.keys(MiHome.Devices).find(id => MiHome.Devices[id].type === device.type);
if (dev) {
for (const attr in MiHome.Devices[dev].states) {
if (!MiHome.Devices[dev].states.hasOwnProperty(attr)) continue;
console.log('Create ' + id + '.' + attr);
tasks.push({
_id: id + '.' + attr,
common: MiHome.Devices[dev].states[attr],
type: 'state',
native: {}
});
}
} else {
adapter.log.error('Device ' + device.type + ' not found');
}
tasks.push({
_id: id,
common: {
name: name || (dev && dev.fullName) || device.type,
icon: '/icons/' + device.type.replace('.', '_') + '.png'
},
type: 'channel',
native: {
sid: device.sid,
type: device.type
}
});
isStartTasks && syncObjects(callback);
}
function readObjects(callback) {
adapter.getForeignObjects(adapter.namespace + '.devices.*', (err, list) => {
adapter.subscribeStates('devices.*');
objects = list;
callback && callback();
});
}
function disconnected () {
connTimeout = null;
if (connected) {
connected = false;
adapter.log.info(`Change connection status on timeout after ${adapter.config.heartbeatTimeout}ms: false`);
adapter.setState('info.connection', connected, true);
}
stopMihome();
}
function setConnected(conn) {
if (connected !== conn) {
connected = conn;
adapter.log.info('Change connection status: ' + conn);
adapter.setState('info.connection', connected, true);
}
if (conn && adapter.config.heartbeatTimeout) {
if (connTimeout) {
clearTimeout(connTimeout);
}
connTimeout = setTimeout(disconnected, adapter.config.heartbeatTimeout);
}
}
function stopMihome() {
if (hub) {
try {
hub.stop();
hub = null;
} catch (e) {
}
}
if (!reconnectTimeout) {
reconnectTimeout = setTimeout(startMihome, adapter.config.restartInterval);
}
}
function startMihome() {
reconnectTimeout = null;
setConnected(false);
if (!adapter.config.key && (!adapter.config.keys || !adapter.config.keys.find(e => e.key))) {
adapter.log.error('no key defined. Only read is possible');
}
hub = new MiHome.Hub({
port: adapter.config.port,
bind: adapter.config.bind || '0.0.0.0',
key: adapter.config.key,
keys: adapter.config.keys,
interval: adapter.config.interval
});
hub.on('message', msg => {
setConnected(true);
adapter.log.debug('RAW: ' + JSON.stringify(msg));
});
hub.on('warning', msg => adapter.log.warn(msg));
hub.on('error', error => {
adapter.log.error(error);
stopMihome();
});
hub.on('device', (device, name) => {
if (!objects[adapter.namespace + '.devices.' + device.type.replace('.', '_') + '_' + device.sid]) {
adapter.log.debug('NEW device: ' + device.sid + '(' + device.type + ')');
createDevice(device, name);
} else {
adapter.log.debug('known device: ' + device.sid + '(' + device.type + ')');
}
});
hub.on('data', (sid, type, data) => {
adapter.log.debug('data: ' + sid + '(' + type + '): ' + JSON.stringify(data));
updateStates(sid, type, data);
});
if (!connTimeout && adapter.config.heartbeatTimeout) {
connTimeout = setTimeout(disconnected, adapter.config.heartbeatTimeout);
}
hub.listen();
}
function main() {
if (adapter.config.heartbeatTimeout === undefined) {
adapter.config.heartbeatTimeout = 20000;
} else {
adapter.config.heartbeatTimeout = parseInt(adapter.config.heartbeatTimeout, 10) || 0;
}
adapter.config.restartInterval = parseInt(adapter.config.restartInterval, 10) || 30000;
readObjects(startMihome);
}

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "yunkong2.mihome",
"description": "Control Xiaomi Mi Home smarthome devices/sensors",
"version": "1.2.3",
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit"
},
"homepage": "https://git.spacen.net/yunkong2/yunkong2.mihome",
"license": "MIT",
"keywords": [
"yunkong2",
"mihome",
"xiaomi",
"home automation"
],
"repository": {
"type": "git",
"url": "git+https://git.spacen.net/yunkong2/yunkong2.mihome.git"
},
"dependencies": {},
"devDependencies": {
"gulp": "^3.9.1",
"mocha": "^5.2.0",
"chai": "^4.1.2"
}
"main": "main.js",
"readmeFilename": "README.md"
}

69
test/lib/gateway.js Normal file
View File

@ -0,0 +1,69 @@
'use strict';
const dgram = require('dgram');
const commands = [
{"cmd": "heartbeat", "model": "gateway", "sid":"81726387164871", "short_id": "0", "token": "8475638456384", "data": {"ip":"192.168.10.68"}},
{"cmd": "report", "model": "86sw2", "sid":"1234567abeefc", "short_id": 10256, "data": {"channel_1": "click"}},
{"cmd": "report", "model": "86sw2", "sid":"1234567abeefc", "short_id": 10256, "data": {"dual_channel": "both_click"}},
{"cmd": "report", "model": "weather.v1", "sid":"1652761251244", "short_id": 12817, "data": {"pressure": "100120"}},
{"cmd": "report", "model": "weather.v1", "sid":"1652761251244", "short_id": 12817, "data": {"humidity": "6606"}},
{"cmd": "report", "model": "weather.v1", "sid":"1652761251244", "short_id": 12817, "data": {"temperature": "2030"}},
{"cmd": "report", "model": "cube", "sid":"287658275634875", "short_id": 21396, "data": {"rotate": "6,500"}},
{"cmd": "report", "model": "gateway", "sid":"81726387164871", "short_id": 0, "data": {"rgb": 0, "illumination": 1180}},
{"cmd": "heartbeat", "model": "cube", "sid":"287658275634875", "short_id": 21396, "data": {"voltage": 3025}},
{"cmd": "report", "model": "sensor_wleak.aq1", "sid":"aaa000xxxxxxx", "short_id": 12345, "data": {"status": "leak"}},
{"cmd": "report", "model": "sensor_wleak.aq1", "sid":"aaa000xxxxxxx", "short_id": 12345, "data": {"status": "no_leak"}}
];
function GatewaySimulator () {
const that = this;
this.destroy = function (cb) {
this.socket.close(cb);
this.socket = null;
};
function onMessage(msgBuffer, rinfo) {
let msg;
try {
msg = JSON.parse(msgBuffer.toString());
}
catch (e) {
return;
}
if (msg.cmd === 'whois') {
for (let c = 0; c < commands.length; c++) {
const json = JSON.stringify(commands[c]);
console.log('Send ' + json);
that.socket.send(json, 0, json.length, rinfo.port, rinfo.ip);
}
} else {
msg.mirror = true;
const json = JSON.stringify(msg);
console.log('Mirror ' + json);
that.socket.send(json, 0, json.length, rinfo.port, rinfo.ip);
}
}
this.init = function () {
this.socket = dgram.createSocket('udp4');
this.socket.on('message', onMessage);
this.socket.on('error', error => console.error('ERROR: ' + error));
this.socket.on('listening', () => {
that.socket.setBroadcast(true);
that.socket.setMulticastTTL(128);
that.socket.addMembership('224.0.0.50');
});
this.socket.bind(4321);
};
return this;
}
if (typeof module !== undefined && module.parent) {
module.exports = GatewaySimulator;
} else {
const gw = new GatewaySimulator();
gw.init();
}

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

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

324
test/testAdapter.js Normal file
View File

@ -0,0 +1,324 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
var GW = require(__dirname + '/lib/gateway');
var objects = null;
var states = null;
var onStateChanged = null;
var onObjectChanged = null;
var sendToID = 1;
var gw;
var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.') + 1);
var runningMode = require(__dirname + '/../io-package.json').common.mode;
var checkStates = {
"mihome.0.devices.gateway_81726387164871.illumination": {
"val": 1180,
"ack": true,
"ts": 1509870303537,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303537
},
"mihome.0.devices.gateway_81726387164871.on": {
"val": false,
"ack": true,
"ts": 1509870303539,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303539
},
"mihome.0.devices.gateway_81726387164871.dimmer": {
"val": 0,
"ack": true,
"ts": 1509870303539,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303539
},
"mihome.0.devices.gateway_81726387164871.rgb": {
"val": "#000000",
"ack": true,
"ts": 1509870303539,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303539
},
"mihome.0.devices.86sw2_1234567abeefc.channel_1": {
"val": false,
"ack": true,
"ts": 1509870303830,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303830
},
"mihome.0.devices.86sw2_1234567abeefc.dual_channel": {
"val": false,
"ack": true,
"ts": 1509870303831,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303831
},
"mihome.0.devices.86sw2_1234567abeefc.channel_1_double": {
"val": false,
"ack": true,
"ts": 1509870303574,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303574
},
"mihome.0.devices.weather_v1_1652761251244.temperature": {
"val": 20.3,
"ack": true,
"ts": 1509870303587,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303587
},
"mihome.0.devices.weather_v1_1652761251244.humidity": {
"val": 66.06,
"ack": true,
"ts": 1509870303590,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303590
},
"mihome.0.devices.weather_v1_1652761251244.pressure": {
"val": 1001.2,
"ack": true,
"ts": 1509870303593,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303593
},
"mihome.0.devices.cube_287658275634875.voltage": {
"val": 3.025,
"ack": true,
"ts": 1509870303600,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303600
},
"mihome.0.devices.cube_287658275634875.percent": {
"val": 82.5,
"ack": true,
"ts": 1509870303603,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303603
},
"mihome.0.devices.cube_287658275634875.rotate": {
"val": 6.5,
"ack": true,
"ts": 1509870303605,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303605
},
"mihome.0.devices.cube_287658275634875.rotate_position": {
"val": 6.5,
"ack": true,
"ts": 1509870303607,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303607
},
"mihome.0.devices.cube_287658275634875.rotate_right": {
"val": false,
"ack": true,
"ts": 1509870303835,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303835
},
"mihome.0.devices.sensor_wleak_aq1_aaa000xxxxxxx.state": {
"val": false,
"ack": true,
"ts": 1509870303649,
"q": 0,
"from": "system.adapter.mihome.0",
"lc": 1509870303649
}
};
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, function (err, state) {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = function (id, state) {
if (id === 'messagebox.system.adapter.test.0') {
callback(state.message);
}
};
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: (new Date()).getTime()
}
});
}
describe('Test ' + adapterShortName + ' adapter', function() {
before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(function () {
var config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
config.native.key = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
//config.native.dbtype = 'sqlite';
setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function (id, obj) {
}, function (id, state) {
if (onStateChanged) onStateChanged(id, state);
},
function (_objects, _states) {
objects = _objects;
states = _states;
gw = new GW();
gw.init();
_done();
});
});
});
it('Test ' + adapterShortName + ' instance object: it must exists', function (done) {
objects.getObject('system.adapter.' + adapterShortName + '.0', function (err, obj) {
expect(err).to.be.null;
expect(obj).to.be.an('object');
expect(obj).not.to.be.null;
done();
});
});
it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) {
this.timeout(60000);
checkConnectionOfAdapter(function (res) {
if (res) console.log(res);
if (runningMode === 'daemon') {
expect(res).not.to.be.equal('Cannot check connection');
} else {
//??
}
done();
});
});
it('Test ' + adapterShortName + ' adapter: must connect', function (done) {
this.timeout(5000);
setTimeout(function () {
states.getState(adapterShortName + '.0.info.connection', function (err, state) {
expect(err).to.be.not.ok;
expect(state.val).to.be.equal(true);
done();
});
}, 3000);
});
it('Test ' + adapterShortName + ' adapter: states must exist', function (done) {
this.timeout(5000);
var cnt = 0;
for (var __id in checkStates) {
if (checkStates.hasOwnProperty(__id)) cnt++;
}
for (var id in checkStates) {
if (!checkStates.hasOwnProperty(id)) continue;
(function (_id, checkState) {
states.getState(_id, function (err, state) {
expect(err).to.be.not.ok;
if (!state) {
console.error('cannot find ' + _id);
}
expect(state.val).to.be.equal(checkState.val);
expect(state.ack).to.be.equal(checkState.ack);
console.log('Check ' + _id);
if (!--cnt) {
done();
}
});
})(id, checkStates[id]);
}
});
/*
it('Test ' + adapterShortName + ' adapter: control should work', (done) => {
states.setState('mihome.0.devices.gateway_81726387164871.on', true, err => {
expect(err).to.be.not.ok;
});
}).timeout(5000);
*/
it('Test ' + adapterShortName + ' adapter: detect disconnect', function (done) {
this.timeout(30000);
gw.destroy();
setTimeout(function () {
states.getState(adapterShortName + '.0.info.connection', function (err, state) {
expect(err).to.be.not.ok;
expect(state.val).to.be.equal(false);
done();
});
}, 21000);
});
after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
done();
});
});
});

91
test/testPackageFiles.js Normal file
View File

@ -0,0 +1,91 @@
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs = require('fs');
describe('Test package.json and io-package.json', function() {
it('Test package files', function (done) {
console.log();
var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
var ioPackage = JSON.parse(fileContentIOPackage);
var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
var npmPackage = JSON.parse(fileContentNPMPackage);
expect(ioPackage).to.be.an('object');
expect(npmPackage).to.be.an('object');
expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;
expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);
if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
console.log();
}
expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;
if (ioPackage.common.name.indexOf('template') !== 0) {
if (Array.isArray(ioPackage.common.authors)) {
expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
if (ioPackage.common.authors.length === 1) {
expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
console.log();
}
expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
console.log('WARNING: titleLang is not existing in io-package.json. Please add');
console.log();
}
if (
ioPackage.common.title.indexOf('yunkong2') !== -1 ||
ioPackage.common.title.indexOf('yunkong2') !== -1 ||
ioPackage.common.title.indexOf('adapter') !== -1 ||
ioPackage.common.title.indexOf('Adapter') !== -1
) {
console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.');
console.log();
}
if (ioPackage.common.name.indexOf('vis-') !== 0) {
if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
console.log('WARNING: Admin3 support is missing! Please add it');
console.log();
}
if (ioPackage.common.materialize) {
expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
}
}
var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
if (fileContentReadme.indexOf('## Changelog') === -1) {
console.log('Warning: The README.md should have a section ## Changelog');
console.log();
}
expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
if (!licenseFileExists) {
console.log('Warning: The License should also exist as LICENSE file');
console.log();
}
if (fileContentReadme.indexOf('## License') === -1) {
console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
console.log();
}
done();
});
});