Initial commit

This commit is contained in:
zhongjin 2018-12-20 22:02:07 +08:00
commit 6b4ec16652
32 changed files with 6602 additions and 0 deletions

8
.gitignore vendored Normal file
View File

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

9
.npmignore Normal file
View File

@ -0,0 +1,9 @@
gulpfile.js
tasks
tmp
test
.travis.yml
appveyor.yml
admin/i18n
iob_npm.done
package-lock.json

23
.travis.yml Normal file
View File

@ -0,0 +1,23 @@
os:
- linux
- osx
language: node_js
node_js:
- '4'
- '6'
- '8'
- '10'
before_script:
- export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1)
- 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi'
- npm -v
- npm install winston@2.3.1
- 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production'
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

177
LICENSE Normal file
View File

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

191
README.md Normal file
View File

@ -0,0 +1,191 @@
![Logo](admin/node-red.png)
# ioBroker node-red Adapter
==============
[![NPM version](http://img.shields.io/npm/v/iobroker.node-red.svg)](https://www.npmjs.com/package/iobroker.node-red)
[![Downloads](https://img.shields.io/npm/dm/iobroker.node-red.svg)](https://www.npmjs.com/package/iobroker.node-red)
[![Tests](https://travis-ci.org/ioBroker/ioBroker.node-red.svg?branch=master)](https://travis-ci.org/ioBroker/ioBroker.node-red)
[![NPM](https://nodei.co/npm/iobroker.node-red.png?downloads=true)](https://nodei.co/npm/iobroker.node-red/)
# Starts node-red instance and communicates with it.
***This adapter needs at least nodejs 4.x to work***
This adapter uses the node-red server from https://github.com/node-red/node-red
**Note:** If in select ID dialog of the ioBroker node you cannot find some variable, restart node-red instance. By restarting the new list of objects will be created.
## Changelog
### 1.7.1 (2017-09-24)
* (bluefox) use newer version of node-red 0.19.4
* (bluefox) Basic authentication was added
### 1.7.0 (2017-08-23)
* (bluefox) use newer version of node-red 0.19.1
### 1.6.0 (2017-08-06)
* (bluefox) use newer version of node-red 0.18.7
* (bluefox) Admin3 dialog implemented
* (bluefox) RAM settings were added
* (bluefox) add credentialSecret option
### 1.5.1 (2017-02-16)
* (Apollon77) queue set state requests till ioBroker connection has been initialized
### 1.5.0 (2018-02-14)
* (Apollon77) use newer version of node-red 0.18.2
### 1.4.1 (2017-10-03)
* (twonky4) fix blank topic support
### 1.4.0 (2017-08-06)
* (bluefox) use newer version of node-red 0.17.5
### 1.3.0 (2017-04-13)
* (bluefox) Update the select ID dialog
* (bluefox) Add node-red-contrib-polymer
### 1.2.0 (2017-02-14)
* (bluefox) use newer version of node-red 0.16.2
### 1.1.6 (2017-01-24)
* (bluefox) use newer version of node-red 0.16.2
### 1.1.5 (2017-01-03)
* (Erhard Weinell) support concurrent access to GetNode
### 1.1.4 (2016-11-04)
* (bluefox) use newer version of node-red 0.15.2
### 1.1.2 (2016-07-23)
* (nobodyMO) use newer version of node-red 0.14.6
* (nobodyMO) change topic name processing
### 1.1.1 (2016-07-08)
* (nobodyMO) use newer version of node-red 0.14.4
### 1.1.0 (2016-05-22)
* (ploebb) configurable: convert values to string
* (nobodyMO) use newer version of node-red 0.14.3
### 1.0.1 (2016-05-22)
* (bluefox) on some systems node-red was available under wrong URL http://ip:1881/undefined. Fixed
### 1.0.0 (2016-04-29)
* (bluefox) support of npm 2/3
### 0.4.4 (2016-04-29)
* (bluefox) install with flag unsafePerm
### 0.4.3 (2016-04-23)
* (bluefox) use node-red 0.13.4
### 0.4.2 (2016-01-21)
* (nobodyMO) Add httpRoot setting
* (nobodyMO) add filter settings to nodes
### 0.4.1 (2016-01-14)
* (nobodyMO) Add --max-old-space-size=128 to support systems with low memory.
* (nobodyMO) Add version 0.12.5 for node-red because it works.
* (nobodyMO) Add ioBroker get node.
* (nobodyMO) Set _maxListeners = 100 to suppress warnings in the log.
### 0.3.5 (2015-08-23)
* (bluefox) fix error if many additional npm packets
### 0.3.4 (2015-08-10)
* (bluefox) do not include node-red packages into global context
### 0.3.3 (2015-07-24)
* (bluefox) enable node-red 0.11.x
### 0.3.2 (2015-06-29)
* (bluefox) fix error with ioBroker nodes
### 0.3.1 (2015-06-28)
* (bluefox) change link in admin to node-red web server
### 0.3.0 (2015-05-18)
* (bluefox) add flag "stopBeforeUpdate"
* (bluefox) store data in iobroker-data directory
### 0.2.2 (2015-05-17)
* (bluefox) fix error with invalid additional npm package
### 0.2.1 (2015-05-17)
* (bluefox) fix readme link
### 0.2.0 (2015-05-16)
* (bluefox) allow install of additional npm and node-red packets
### 0.1.9 (2015-03-26)
* (bluefox) fix first start
### 0.1.7 (2015-03-25)
* (bluefox) remove warnings
### 0.1.6 (2015-03-18)
* (bluefox) make node-red compatible with ioBroker again
### 0.1.5 (2015-02-12)
* (bluefox) update node-red to 0.10.1
* (bluefox) update select ID dialog
### 0.1.4 (2015-01-07)
* (bluefox) create variables without need to be extra called with "__create__"
### 0.1.3 (2015-01-06)
* (bluefox) make possible creation of variables
### 0.1.2 (2015-01-04)
* (bluefox) print debug message by saving
### 0.1.1 (2015-01-03)
* (bluefox) fix errors with utils.js
### 0.1.0 (2015-01-02)
* (bluefox) enable npm install
### 0.0.8 (2014-12-20)
* (bluefox) support signal stopInstance
### 0.0.7 (2014-12-14)
* (bluefox) support of select ID dialogs
### 0.0.6 (2014-11-26)
* (bluefox) use names like in mqtt: "adapter/instance/device/channel/state"
* (bluefox) suport of "value" or "object" for input node
### 0.0.5 (2014-11-22)
* (bluefox) support of new naming concept
### 0.0.4 (2014-11-05)
* (bluefox) fix some errors
### 0.0.2 (2014-11-04)
* (bluefox) use adapter.js to communicate with ioBroker
### 0.0.1 (2014-11-03)
* (bluefox) initial commit
## Install
```node iobroker.js add node-red```
## Configuration
## License
Copyright 2014-2018 bluefox <dogafox@gmail.com>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

132
admin/index.html Normal file
View File

@ -0,0 +1,132 @@
<html>
<!-- these 4 files always have to be included -->
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
<!-- these two file always have to be included -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<style>
.table_header {
background-color: blue;
color: white;
}
.ip {
width: 150px;
text-align: right;
}
</style>
<!-- you have to define 2 functions in the global scope: -->
<script type="text/javascript">
var devices = [];
function setValue(id, value, onChange) {
var $value = $('#' + id + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', value).change(function() {
onChange();
});
} else {
$value.val(value).on('change', function() {
onChange();
}).on('keyup', function() {
onChange();
});
}
}
// the function loadSettings has to exist ...
function load(settings, onChange) {
if (!settings) return;
devices = settings.devices || [];
if (settings.maxMemory === undefined) settings.maxMemory = 128;
for (var key in settings) {
if (settings.hasOwnProperty(key)) {
setValue(key, settings[key], onChange);
}
}
$('#update').button().click(function () {
sendTo(null, 'update', null, function (error) {
if (!error) {
showMessage(_('Successfully updated'))
} else {
showMessage(_('Cannot update:') + _(error))
}
});
});
var $libs = $('#libraries');
if (common.npmLibs){
$libs.val(common.npmLibs.join(', '));
}
$libs.on('change', function () {
onChange();
}).on('keyup', function () {
$(this).trigger('change');
});
getIsAdapterAlive(function (isAlive) {
if (!isAlive) $('#update').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();
}
});
var libs = $('#libraries').val().split(',');
var common = {npmLibs: []};
for (var l = 0; l < libs.length; l++) {
common.npmLibs.push(libs[l].trim());
}
callback(obj, {localLink: 'http://%ip%:' + obj.port, npmLibs: common.npmLibs});
}
</script>
<!-- you have to put your config page in a div with id adapter-container -->
<div id="adapter-container">
<table><tr>
<td><img src="node-red.png" width="64" height="64"></td>
<td style="padding-top: 20px;padding-left: 10px"><h3 class="translate">node-red adapter settings</h3></td>
</tr></table>
<h4 class="translate">node-red settings</h4>
<table>
<tr><td><label class="translate" for="port">Web server port:</label></td><td colspan="2"><input class="value" id="port" type="number" min="1" max="65565"/></td></tr>
<tr>
<td><label class="translate" for="libraries">Additional npm modules:</label></td><td><input id="libraries" class="value" style="width: 100%"/></td><td class="translate">Divided by comma</td>
</tr>
<tr>
<td><label class="translate" for="httpRoot">http root directory:</label></td><td><input id="httpRoot" class="value" style="width: 100%"/></td>
</tr>
<tr>
<td><label class="translate" for="valueConvert">Convert values to string:</label></td><td> <input class="value" id="valueConvert" type="checkbox" /></td>
</tr>
<tr>
<td><label class="translate" for="maxMemory">Max allocated RAM:</label></td><td> <input class="value" id="maxMemory" type="number" min="32"/></td>
</tr>
</table>
<h4 class="translate">node-red update select dialog</h4>
<button id="update" class="translateB">Update select dialog</button>
</div>
</html>

271
admin/index_m.html Normal file
View File

@ -0,0 +1,271 @@
<html>
<head>
<!-- Materialze style -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
<script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../lib/js/materialize.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<script type="text/javascript">
// https://stackoverflow.com/questions/14733374/how-to-generate-md5-file-hash-on-javascript
var MD5 = function (d) {
var result = MM(V(Y(X(d), 8 * d.length)));
return result.toLowerCase()
};
function MM(d) {
var f = '';
for (var _, m = '0123456789ABCDEF', r = 0; r < d.length; r++) {
_ = d.charCodeAt(r);
f += m.charAt(_ >>> 4 & 15) + m.charAt(15 & _);
}
return f;
}
function X(d) {
var _;
for (_ = Array(d.length >> 2), m = 0; m < _.length; m++) _[m] = 0;
for (m = 0; m < 8 * d.length; m += 8) _[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;
return _;
}
function V(d) {
var _;
for (_ = '', m = 0; m < 32 * d.length; m += 8) {
_ += String.fromCharCode(d[m >> 5] >>> m % 32 & 255);
}
return _;
}
function Y(d, _) {
d[_ >> 5] |= 128 << _ % 32, d[14 + (_ + 64 >>> 9 << 4)] = _;
for (var m = 1732584193, f = -271733879, r = -1732584194, i = 271733878, n = 0; n < d.length; n += 16) {
var h = m, t = f, g = r, e = i;
f = md5_ii(f = md5_ii(f = md5_ii(f = md5_ii(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_ff(f = md5_ff(f = md5_ff(f = md5_ff(f, r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = safe_add(m, h), f = safe_add(f, t), r = safe_add(r, g), i = safe_add(i, e);
}
return Array(m, f, r, i)
}
function md5_cmn(d, _, m, f, r, i) {
return safe_add(bit_rol(safe_add(safe_add(_, d), safe_add(f, i)), r), m)
}
function md5_ff(d, _, m, f, r, i, n) {
return md5_cmn(_ & m | ~_ & f, d, _, r, i, n)
}
function md5_gg(d, _, m, f, r, i, n) {
return md5_cmn(_ & f | m & ~f, d, _, r, i, n)
}
function md5_hh(d, _, m, f, r, i, n) {
return md5_cmn(_ ^ m ^ f, d, _, r, i, n)
}
function md5_ii(d, _, m, f, r, i, n) {
return md5_cmn(m ^ (_ | ~f), d, _, r, i, n)
}
function safe_add(d, _) {
var m = (65535 & d) + (65535 & _);
return (d >> 16) + (_ >> 16) + (m >> 16) << 16 | 65535 & m
}
function bit_rol(d, _) {
return d << _ | d >>> 32 - _
};
</script>
<!-- you have to define 2 functions in the global scope: -->
<script type="text/javascript">
var oldPass = '';
function setValue(id, value, onChange) {
var $value = $('#' + id + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', value).change(function() {
onChange();
});
} else {
$value.val(value).on('change', function() {
onChange();
}).on('keyup', function() {
onChange();
});
}
}
function chips2list(selector) {
var data = $(selector).chips('getData');
var text = [];
for (var lib = 0; lib < data.length; lib++) {
text.push(data[lib].tag);
}
return text.join(' ');
}
function list2chips(selector, list, onChange) {
var chips = list.split(/[,;\s]+/);
var data = [];
for (var c = 0; c < chips.length; c++) {
if (chips[c] && chips[c].trim()) {
data.push({tag: chips[c].trim()});
}
}
$(selector).chips({
data: data,
placeholder: _('Module names'),
secondaryPlaceholder: _('Add module'),
onChipAdd: onChange,
onChipDelete: onChange
});
}
// the function loadSettings has to exist ...
function load(settings, onChange) {
if (!settings) return;
if (settings.user === undefined) {
settings.user = '';
}
if (settings.pass === undefined) {
settings.pass = '';
}
if (settings.bind === undefined) {
settings.bind = '0.0.0.0';
}
if (settings.maxMemory === undefined) {
settings.maxMemory = 128;
}
oldPass = settings.pass;
if (settings.pass) {
settings.pass = '__pass__';
}
for (var key in settings) {
if (settings.hasOwnProperty(key)) {
setValue(key, settings[key], onChange);
}
}
$('#update').click(function () {
sendTo(null, 'update', null, function (error) {
if (!error) {
showMessage(_('Successfully updated'))
} else {
showMessage(_('Cannot update:') + _(error))
}
});
});
getIPs(function (ips) {
for (var i = 0; i < ips.length; i++) {
$('#bind').append('<option value="' + ips[i].address + '">' + ips[i].name + '</option>');
}
$('#bind.value').val(settings.bind).select();
});
if (typeof common.npmLibs === 'object') {
common.npmLibs = common.npmLibs.join(';');
}
list2chips('.libraries', common.npmLibs || '', onChange);
getIsAdapterAlive(function (isAlive) {
if (!isAlive) {
$('#update').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();
}
});
if (obj.pass) {
if (obj.pass !== '__pass__') {
obj.pass = MD5(obj.pass);
} else {
obj.pass = oldPass;
}
}
common.npmLibs = chips2list('.libraries');
callback(obj, {localLink: 'http://%ip%:' + obj.port, npmLibs: common.npmLibs});
}
</script>
</head>
<body>
<div class="m adapter-container">
<div class="row">
<div class="col s12">
<div class="row">
<div class="col s6">
<img src="node-red.png" class="logo">
</div>
</div>
<div class="row">
<div class="input-field col s12 m8 l6">
<label class="translate">Additional npm modules:</label>
<div class="chips libraries"></div>
</div>
</div>
<div class="row">
<div class="input-field col s12 m8 l4">
<select class="value" id="bind"></select>
<label class="translate" for="bind">IP:</label>
</div>
<div class="input-field col s12 m4 l2">
<input class="value" id="port" type="number" min="1" max="65565"/>
<label class="translate" for="port">Web server port:</label>
</div>
</div>
<div class="row">
<div class="input-field col s12 m4 l3">
<input id="user" class="value" type="text"/>
<label class="translate" for="user">User</label>
<span class="translate">Leave blank to disable basic authentication</span>
</div>
<div class="input-field col s12 m4 l3">
<input id="pass" class="value" type="password"/>
<label class="translate" for="pass">Password</label>
</div>
</div>
<div class="row">
<div class="input-field col s12 m4 l3">
<input id="httpRoot" class="value" type="text" style="width: 100%"/>
<label class="translate" for="httpRoot">http root directory:</label>
</div>
</div>
<div class="row">
<div class="input-field col s12 m4 l3">
<input class="value" id="valueConvert" type="checkbox" />
<label class="translate" for="valueConvert">Convert values to string:</label>
</div>
<div class="input-field col s12 m4 l3">
<input class="value" id="maxMemory" type="number" min="32"/>
<label class="translate" for="maxMemory">Max allocated RAM:</label>
</div>
</div>
<div class="row">
<div class="col s12">
<a id="update" class="waves-effect waves-light btn"><i class="material-icons right">cloud</i><span class="translate">Update select dialog</span></a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

BIN
admin/node-red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

20
admin/words.js Normal file
View File

@ -0,0 +1,20 @@
// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n
/*global systemDictionary:true */
'use strict';
systemDictionary = {
"node-red settings": { "en": "node-red settings", "de": "node-red Einstellungen", "ru": "node-red Настройки", "pt": "configurações de nó vermelho", "nl": "node-red instellingen", "fr": "paramètres de node-red", "it": "impostazioni del node-red", "es": "configuración de node-red", "pl": "ustawienia node-red"},
"Web server port:": { "en": "Web server port", "de": "Web-Server-Port", "ru": "Порт веб сервера", "pt": "Porta do servidor da Web", "nl": "Webserverpoort", "fr": "Port du serveur Web", "it": "Porta del server Web", "es": "Puerto del servidor web", "pl": "Port serwera internetowego"},
"node-red update select dialog": { "en": "node-red update select dialog", "de": "node-red SelectID Dialog aktualisieren", "ru": "Обновить переменные в диалоге node-red", "pt": "caixa de diálogo de seleção de atualização do nó-vermelho", "nl": "update select dialoogvenster", "fr": "boîte de dialogue de sélection de mise à jour de noeud", "it": "finestra di dialogo di selezione aggiornamento node-red", "es": "diálogo de selección de actualización node-red", "pl": "okno dialogowe aktualizacji uaktualnienia węzła"},
"Update select dialog": { "en": "Update select dialog", "de": "Update Dialog um ID zu selektieren", "ru": "Обновить переменные в диалоге выбора объектов", "pt": "Atualizar caixa de diálogo de seleção", "nl": "Update select dialoogvenster", "fr": "Mettre à jour le dialogue", "it": "Aggiorna la finestra di selezione", "es": "Actualizar diálogo de selección", "pl": "Zaktualizuj wybrane okno dialogowe"},
"Divided by comma": { "en": "Divided by comma", "de": "Getrennt mit Komma", "ru": "Через запятую", "pt": "Dividido por vírgula", "nl": "Verdeeld door een komma", "fr": "Divisé par une virgule", "it": "Diviso in virgola", "es": "Dividido por coma", "pl": "Podzielone przecinkiem"},
"Additional npm modules:": { "en": "Additional npm modules", "de": "Zusätzliche NPM-Module", "ru": "Дополнительные NPM Модули", "pt": "Módulos npm adicionais", "nl": "Extra npm-modules", "fr": "Modules NPM supplémentaires", "it": "Ulteriori moduli npm", "es": "Módulos npm adicionales", "pl": "Dodatkowe moduły npm"},
"http root directory:": { "en": "http root directory", "de": "http Stammpfad", "ru": "http root directory", "pt": "diretório raiz http", "nl": "http root directory", "fr": "répertoire racine http", "it": "directory root http", "es": "directorio raíz http", "pl": "główny katalog http"},
"Convert values to string:": { "en": "Convert ioBroker values to string", "de": "ioBroker-Werte in String konvertieren:", "ru": "Конвертировать значения из ioBroker в строки", "pt": "Converter valores de ioBroker em string", "nl": "IoBroker-waarden converteren naar tekenreeks", "fr": "Convertir les valeurs de ioBroker en chaîne", "it": "Converti i valori di ioBroker in stringa", "es": "Convierta los valores de ioBroker en una cadena", "pl": "Konwertuj wartości ioBroker na ciąg"},
"Add module": { "en": "Add module", "de": "Modul hinzufügen", "ru": "Добавить модуль", "pt": "Adicionar módulo", "nl": "Module toevoegen", "fr": "Ajouter un module", "it": "Aggiungi modulo", "es": "Agregar módulo", "pl": "Dodaj moduł"},
"Max allocated RAM:": { "en": "Max allocated RAM", "de": "Max zugewiesener RAM", "ru": "Выделено RAM", "pt": "RAM alocada máxima", "nl": "Max toegewezen RAM", "fr": "Max allouée RAM", "it": "RAM allocata massima", "es": "RAM máxima asignada", "pl": "Maksymalna przydzielona pamięć RAM"},
"Module names": { "en": "Module names", "de": "Modulnamen", "ru": "Имена модулей", "pt": "Nomes de módulos", "nl": "Module namen", "fr": "Noms de modules", "it": "Nomi dei moduli", "es": "Nombres de módulos", "pl": "Nazwy modułów"},
"User": { "en": "User", "de": "Benutzer", "ru": "Имя пользователя", "pt": "Do utilizador", "nl": "Gebruiker", "fr": "Utilisateur", "it": "Utente", "es": "Usuario", "pl": "Użytkownik"},
"Leave blank to disable basic authentication": { "en": "Leave blank to disable basic authentication", "de": "Leer lassen, um die Basic-Authentication zu deaktivieren", "ru": "Оставить пустым, чтобы отключить basic authentication", "pt": "Deixe em branco para desativar a autenticação básica", "nl": "Laat dit leeg om basisverificatie uit te schakelen", "fr": "Laissez vide pour désactiver l'authentification de base", "it": "Lascia vuoto per disabilitare l'autenticazione di base", "es": "Dejar en blanco para deshabilitar la autenticación básica", "pl": "Pozostaw puste, aby wyłączyć podstawowe uwierzytelnianie"},
"Password": { "en": "Password", "de": "Passwort", "ru": "Пароль", "pt": "Senha", "nl": "Wachtwoord", "fr": "Mot de passe", "it": "Parola d'ordine", "es": "Contraseña", "pl": "Hasło"},
};

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/ioBroker/ioBroker.js-controller/tarball/master --production'
test_script:
- echo %cd%
- node --version
- npm --version
- npm test
build: 'off'

400
gulpfile.js Normal file
View File

@ -0,0 +1,400 @@
'use strict';
const gulp = require('gulp');
const fs = require('fs');
const pkg = require('./package.json');
const iopackage = require('./io-package.json');
const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
/*const appName = getAppName();
function getAppName() {
const parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 1].split('.')[0].toLowerCase();
}
*/
const fileName = 'words.js';
const languages = {
en: {},
de: {},
ru: {},
pt: {},
nl: {},
fr: {},
it: {},
es: {},
pl: {}
};
function lang2data(lang, isFlat) {
let str = isFlat ? '' : '{\n';
let count = 0;
for (const w in lang) {
if (lang.hasOwnProperty(w)) {
count++;
if (isFlat) {
str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n';
} else {
const 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 {
let words;
if (fs.existsSync(src + 'js/' + fileName)) {
words = fs.readFileSync(src + 'js/' + fileName).toString();
} else {
words = fs.readFileSync(src + fileName).toString();
}
const lines = words.split(/\r\n|\r|\n/g);
let 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');
const 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) {
let text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n';
text += '/*global systemDictionary:true */\n';
text += '\'use strict\';\n\n';
text += 'systemDictionary = {\n';
for (const word in data) {
if (data.hasOwnProperty(word)) {
text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50);
let line = '';
for (const 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) {
const langs = Object.assign({}, languages);
const data = readWordJs(src);
if (data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (const j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (const l in langs) {
if (!langs.hasOwnProperty(l)) continue;
const keys = Object.keys(langs[l]);
//keys.sort();
const obj = {};
for (let 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) {
const langs = Object.assign({}, languages);
const data = readWordJs(src);
if (data) {
for (const word in data) {
if (data.hasOwnProperty(word)) {
for (const lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (const j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
const keys = Object.keys(langs.en);
//keys.sort();
for (const l in langs) {
if (!langs.hasOwnProperty(l)) continue;
const obj = {};
for (let 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 (const 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) {
const dirs = fs.readdirSync(src + 'i18n/');
const langs = {};
const bigOne = {};
const order = Object.keys(languages);
dirs.sort(function (a, b) {
const posA = order.indexOf(a);
const 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;
}
});
const keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n');
for (let l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
const lang = dirs[l];
const values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n');
langs[lang] = {};
keys.forEach(function (word, i) {
langs[lang][word] = values[i].replace(/<\/ i>/g, '</i>').replace(/<\/ b>/g, '</b>').replace(/<\/ span>/g, '</span>').replace(/% s/g, ' %s');
});
const words = langs[lang];
for (const word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
const aWords = readWordJs();
const temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt'];
if (aWords) {
// Merge words together
for (const 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) {
const dirs = fs.readdirSync(src + 'i18n/');
const langs = {};
const bigOne = {};
const order = Object.keys(languages);
dirs.sort(function (a, b) {
const posA = order.indexOf(a);
const 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 (let l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
const lang = dirs[l];
langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString();
langs[lang] = JSON.parse(langs[lang]);
const words = langs[lang];
for (const word in words) {
if (words.hasOwnProperty(word)) {
bigOne[word] = bigOne[word] || {};
if (words[word] !== EMPTY) {
bigOne[word][lang] = words[word];
}
}
}
}
// read actual words.js
const aWords = readWordJs();
const temporaryIgnore = ['pt', 'fr', 'nl', 'it'];
if (aWords) {
// Merge words together
for (const 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]) {
const news = iopackage.common.news;
const 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) {
const readme = fs.readFileSync('README.md').toString();
const pos = readme.indexOf('## Changelog\n');
if (pos !== -1) {
const readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
const readmeEnd = readme.substring(pos + '## Changelog\n'.length);
if (readme.indexOf(version) === -1) {
const timestamp = new Date();
const date = timestamp.getFullYear() + '-' +
('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' +
('0' + (timestamp.getDate()).toString(10)).slice(-2);
let 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']);

138
io-package.json Normal file
View File

@ -0,0 +1,138 @@
{
"common": {
"name": "node-red",
"version": "1.7.1",
"title": "node-red",
"news": {
"1.7.1": {
"en": "Used newer version of node-red 0.19.4\nBasic authentication was added",
"de": "Verwendete neuere Version von node-red 0.19.4\nStandardauthentifizierung wurde hinzugefügt",
"ru": "Используется новая версия node-red 0.19.4\nДобавлена ​​базовая аутентификация",
"pt": "Usado versão mais recente do nó vermelho 0.19.4\nAutenticação básica foi adicionada",
"nl": "Gebruikte nieuwere versie van knoop-rood 0.19.4\nBasisverificatie is toegevoegd",
"fr": "Version plus récente de node-red 0.19.4\nL'authentification de base a été ajoutée",
"it": "Utilizzata la versione più recente di node-red 0.19.4\nÈ stata aggiunta l'autenticazione di base",
"es": "Versión más nueva usada de node-red 0.19.4\nAutenticación básica fue agregada",
"pl": "Używana nowsza wersja węzła-czerwonego 0.19.4\nDodano uwierzytelnianie podstawowe"
},
"1.7.0": {
"en": "Used newer version of node-red 0.19.1",
"de": "Benutzte neuere Version von node-red 0.19.1",
"ru": "Используется новая версия node-red 0.19.1",
"pt": "Usado versão mais recente do nó vermelho 0.19.1",
"nl": "Gebruikte nieuwere versie van knoop-rood 0.19.1",
"fr": "Version plus récente de node-red 0.19.1",
"it": "Utilizzata la versione più recente di node-red 0.19.1",
"es": "Versión más nueva usada de node-red 0.19.1",
"pl": "Używana nowsza wersja węzła-czerwonego 0.19.1"
},
"1.6.0": {
"en": "newer version of node-red 0.18.7 used\nAdmin3 dialog implemented\nRAM settings were added",
"de": "neuere Version von node-red 0.18.7 verwendet\nAdmin3-Dialog implementiert\nRAM-Einstellungen wurden hinzugefügt",
"ru": "более новая версия node-red 0.18.7 используется\nДиалог Admin3 реализован\nДобавлены настройки RAM",
"pt": "versão mais recente do node-red 0.18.7 usado\nDiálogo Admin3 implementado\nConfigurações de RAM foram adicionadas",
"nl": "nieuwere versie van knooppunt-rood 0.18.7 gebruikt\nAdmin3-dialoogvenster geïmplementeerd\nRAM-instellingen zijn toegevoegd",
"fr": "version plus récente de node-red 0.18.7 d'occasion\nBoîte de dialogue Admin3 implémentée\nParamètres RAM ont été ajoutés",
"it": "versione più recente di node-red 0.18.7 usata\nFinestra di dialogo Admin3 implementata\nSono state aggiunte le impostazioni della RAM",
"es": "nueva versión de node-red 0.18.7 usada\nDiálogo Admin3 implementado\nSe agregaron configuraciones de RAM",
"pl": "zastosowano nowszą wersję węzła-czerwonego 0.18.7\nZaimplementowano okno Admin3\nDodano ustawienia pamięci RAM"
},
"1.5.1": {
"en": "queue set state requests till ioBroker connection has been initialized",
"de": "Verzögern von ioBroker-Schreibaktionen bis Verbindung zu ioBroker initialisiert wurde",
"ru": "queue set state requests till ioBroker connection has been initialized"
},
"1.5.0": {
"en": "use newer version of node-red 0.18.2",
"de": "Neue Version von node-red 0.18.2",
"ru": "Новая версия node-red 0.18.2"
},
"1.4.1": {
"en": "fix blank topic support",
"de": "Korrigiere lehre Topics",
"ru": "Поправлены пустые топики"
},
"1.4.0": {
"en": "use newer version of node-red 0.17.5",
"de": "Neue Version von node-red 0.17.5",
"ru": "Новая версия node-red 0.17.5"
},
"1.3.0": {
"en": "Update the select ID dialog",
"de": "Update ID Auswahl",
"ru": "Обновлён диалог выбора объектов"
},
"1.2.0": {
"en": "add node-red-contrib-os, node-red-dashboard, node-red-contrib-aggregator by default",
"de": "Hinzugefügt: node-red-contrib-os, node-red-dashboard, node-red-contrib-aggregator",
"ru": "Добавлены: node-red-contrib-os, node-red-dashboard, node-red-contrib-aggregator"
},
"1.1.6": {
"en": "use newer version of node-red 0.16.2",
"de": "Neue Version von node-red 0.16.2",
"ru": "Новая версия node-red 0.16.2"
}
},
"desc": {
"en": "This adapter uses node-red as a service. No additional node-red instance required.",
"de": "Adapter benutzt node-red als Servcie. Kein zusätzliches node-red Programm nötig.",
"ru": "Драйвер создает node-red сервер и позволяет общаться с ним.",
"pt": "Este adaptador usa node-red como um serviço. Nenhuma instância node-red adicional é necessária.",
"nl": "Deze adapter gebruikt node-red als een service. Geen extra node-red exemplaar vereist.",
"fr": "Cet adaptateur utilise node-red en tant que service. Aucune instance node-red supplémentaire requise.",
"it": "Questo adattatore utilizza node-red come servizio. Nessuna istanza aggiuntiva node-red richiesta.",
"es": "Este adaptador usa node-red como un servicio. No se requiere ninguna instancia adicional de node-red.",
"pl": "Ten adapter używa node-red jako usługi. Żadna dodatkowa instancja node-red nie jest wymagana."
},
"authors": [
"bluefox <dogafox@gmail.com>"
],
"license": "Apache-2.0",
"platform": "Javascript/Node.js",
"mode": "daemon",
"messagebox": true,
"loglevel": "info",
"icon": "node-red.png",
"keywords": [
"node-red",
"logic",
"script"
],
"extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.node-red/master/admin/node-red.png",
"localLink": "http://%ip%:%port%%httpRoot%",
"enabled": true,
"singletonHost": true,
"supportStopInstance": true,
"unsafePerm": true,
"materialize": true,
"type": "logic",
"readme": "https://github.com/ioBroker/ioBroker.node-red/blob/master/README.md",
"stopBeforeUpdate": true,
"adminTab": {
"link": "http://%ip%:%port%%httpRoot%",
"fa-icon": "settings_input_composite"
}
},
"native": {
"bind": "0.0.0.0",
"port": 1880,
"httpRoot": "/",
"npmLibs": "",
"valueConvert": true,
"maxMemory": 128,
"user": "",
"pass": ""
},
"objects": [],
"instanceObjects": [
{
"_id": "",
"type": "channel",
"common": {
"name": "States created by node-red.%INSTANCE%",
"role": "info"
},
"native": {}
}
]
}

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 = [
'iobroker.js-controller',
'ioBroker.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;

443
main.js Normal file
View File

@ -0,0 +1,443 @@
/**
*
* ioBroker node-red Adapter
*
* (c) 2014 bluefox<bluefox@ccu.io>
*
* Apache 2.0 License
*
*/
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
'use strict';
const utils = require(__dirname + '/lib/utils'); // Get common adapter utils
const adapter = utils.Adapter({
name: 'node-red',
systemConfig: true, // get the system configuration as systemConfig parameter of adapter
unload: unloadRed
});
const fs = require('fs');
const path = require('path');
const spawn = require('child_process').spawn;
const Notify = require('fs.notify');
const attempts = {};
const additional = [];
let secret;
let userdataDir = __dirname + '/userdata/';
adapter.on('message', function (obj) {
if (obj) processMessage(obj);
processMessages();
});
adapter.on('ready', function () {
installLibraries(main);
});
function installNpm(npmLib, callback) {
const path = __dirname;
if (typeof npmLib === 'function') {
callback = npmLib;
npmLib = undefined;
}
const cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '" --save';
adapter.log.info(cmd + ' (System call)');
// Install node modules as system call
// System call used for update of js-controller itself,
// because during installation npm packet will be deleted too, but some files must be loaded even during the install process.
const exec = require('child_process').exec;
const child = exec(cmd);
child.stdout.on('data', function(buf) {
adapter.log.info(buf.toString('utf8'));
});
child.stderr.on('data', function(buf) {
adapter.log.error(buf.toString('utf8'));
});
child.on('exit', function (code, signal) {
if (code) {
adapter.log.error('Cannot install ' + npmLib + ': ' + code);
}
// command succeeded
if (callback) callback(npmLib);
});
}
function installLibraries(callback) {
let allInstalled = true;
if (typeof adapter.common.npmLibs === 'string') {
adapter.common.npmLibs = adapter.common.npmLibs.split(/[,;\s]+/);
}
if (adapter.common && adapter.common.npmLibs) {
for (let lib = 0; lib < adapter.common.npmLibs.length; lib++) {
if (adapter.common.npmLibs[lib] && adapter.common.npmLibs[lib].trim()) {
adapter.common.npmLibs[lib] = adapter.common.npmLibs[lib].trim();
if (!fs.existsSync(__dirname + '/node_modules/' + adapter.common.npmLibs[lib] + '/package.json')) {
if (!attempts[adapter.common.npmLibs[lib]]) {
attempts[adapter.common.npmLibs[lib]] = 1;
} else {
attempts[adapter.common.npmLibs[lib]]++;
}
if (attempts[adapter.common.npmLibs[lib]] > 3) {
adapter.log.error('Cannot install npm packet: ' + adapter.common.npmLibs[lib]);
continue;
}
installNpm(adapter.common.npmLibs[lib], function () {
installLibraries(callback);
});
allInstalled = false;
break;
} else {
if (additional.indexOf(adapter.common.npmLibs[lib]) === -1) additional.push(adapter.common.npmLibs[lib]);
}
}
}
}
if (allInstalled) callback();
}
// is called if a subscribed state changes
//adapter.on('stateChange', function (id, state) {
//});
function unloadRed (callback) {
// Stop node-red
stopping = true;
if (redProcess) {
adapter.log.info("kill node-red task");
redProcess.kill();
redProcess = null;
}
if (notificationsCreds) notificationsCreds.close();
if (notificationsFlows) notificationsFlows.close();
if (callback) callback();
}
function processMessage(obj) {
if (!obj || !obj.command) return;
switch (obj.command) {
case 'update':
writeStateList(error => {
if (typeof obj.callback === 'function') adapter.sendTo(obj.from, obj.command, error, obj.callback);
});
break;
case 'stopInstance':
unloadRed();
break;
}
}
function processMessages() {
adapter.getMessage((err, obj) => {
if (obj) {
processMessage(obj.command, obj.message);
processMessages();
}
});
}
function getNodeRedPath() {
let nodeRed = __dirname + '/node_modules/node-red';
if (!fs.existsSync(nodeRed)) {
nodeRed = path.normalize(__dirname + '/../node-red');
if (!fs.existsSync(nodeRed)) {
nodeRed = path.normalize(__dirname + '/../node_modules/node-red');
if (!fs.existsSync(nodeRed)) {
adapter && adapter.log && adapter.log.error('Cannot find node-red packet!');
throw new Error('Cannot find node-red packet!');
}
}
}
return nodeRed;
}
let redProcess;
let stopping;
let notificationsFlows;
let notificationsCreds;
let saveTimer;
const nodePath = getNodeRedPath();
function startNodeRed() {
adapter.config.maxMemory = parseInt(adapter.config.maxMemory, 10) || 128;
const args = ['--max-old-space-size=' + adapter.config.maxMemory, nodePath + '/red.js', '-v', '--settings', userdataDir + 'settings.js'];
adapter.log.info('Starting node-red: ' + args.join(' '));
redProcess = spawn('node', args);
redProcess.on('error', function (err) {
adapter.log.error('catched exception from node-red:' + JSON.stringify(err));
});
redProcess.stdout.on('data', function (data) {
if (!data) return;
data = data.toString();
if (data[data.length - 2] === '\r' && data[data.length - 1] === '\n') data = data.substring(0, data.length - 2);
if (data[data.length - 2] === '\n' && data[data.length - 1] === '\r') data = data.substring(0, data.length - 2);
if (data[data.length - 1] === '\r') data = data.substring(0, data.length - 1);
if (data.indexOf('[err') !== -1) {
adapter.log.error(data);
} else if (data.indexOf('[warn]') !== -1) {
adapter.log.warn(data);
} else {
adapter.log.debug(data);
}
});
redProcess.stderr.on('data', function (data) {
if (!data) return;
if (data[0]) {
let text = '';
for (let i = 0; i < data.length; i++) {
text += String.fromCharCode(data[i]);
}
data = text;
}
if (data.indexOf && data.indexOf('[warn]') === -1) {
adapter.log.warn(data);
} else {
adapter.log.error(JSON.stringify(data));
}
});
redProcess.on('exit', function (exitCode) {
adapter.log.info('node-red exited with ' + exitCode);
redProcess = null;
if (!stopping) {
setTimeout(startNodeRed, 5000);
}
});
}
function setOption(line, option, value) {
const toFind = "'%%" + option + "%%'";
const pos = line.indexOf(toFind);
if (pos !== -1) {
return line.substring(0, pos) + ((value !== undefined) ? value : (adapter.config[option] === null || adapter.config[option] === undefined) ? '' : adapter.config[option]) + line.substring(pos + toFind.length);
}
return line;
}
function writeSettings() {
const config = JSON.stringify(adapter.systemConfig);
const text = fs.readFileSync(__dirname + '/settings.js').toString();
const lines = text.split('\n');
let npms = '\r\n';
const dir = __dirname.replace(/\\/g, '/') + '/node_modules/';
const nodesDir = '"' + __dirname.replace(/\\/g, '/') + '/nodes/"';
const bind = '"' + (adapter.config.bind || '0.0.0.0') + '"';
const auth = adapter.config.user && adapter.config.pass ? JSON.stringify({user: adapter.config.user, pass: adapter.config.pass}) : '""';
const pass = '"' + adapter.config.pass + '"';
for (let a = 0; a < additional.length; a++) {
if (additional[a].match(/^node-red-/)) continue;
npms += ' "' + additional[a] + '": require("' + dir + additional[a] + '")';
if (a !== additional.length - 1) {
npms += ', \r\n';
}
}
// update from 1.0.1 (new convert-option)
if (adapter.config.valueConvert === null ||
adapter.config.valueConvert === undefined ||
adapter.config.valueConvert === '' ||
adapter.config.valueConvert === 'true' ||
adapter.config.valueConvert === '1' ||
adapter.config.valueConvert === 1) {
adapter.config.valueConvert = true;
}
if (adapter.config.valueConvert === 0 ||
adapter.config.valueConvert === '0' ||
adapter.config.valueConvert === 'false') {
adapter.config.valueConvert = false;
}
for (let i = 0; i < lines.length; i++) {
lines[i] = setOption(lines[i], 'port');
lines[i] = setOption(lines[i], 'auth', auth);
lines[i] = setOption(lines[i], 'pass', pass);
lines[i] = setOption(lines[i], 'bind', bind);
lines[i] = setOption(lines[i], 'port');
lines[i] = setOption(lines[i], 'instance', adapter.instance);
lines[i] = setOption(lines[i], 'config', config);
lines[i] = setOption(lines[i], 'functionGlobalContext', npms);
lines[i] = setOption(lines[i], 'nodesdir', nodesDir);
lines[i] = setOption(lines[i], 'httpRoot');
lines[i] = setOption(lines[i], 'credentialSecret', secret);
lines[i] = setOption(lines[i], 'valueConvert');
}
fs.writeFileSync(userdataDir + 'settings.js', lines.join('\n'));
}
function writeStateList(callback) {
adapter.getForeignObjects('*', 'state', ['rooms', 'functions'], function (err, obj) {
// remove native information
for (const i in obj) {
if (obj.hasOwnProperty(i) && obj[i].native) {
delete obj[i].native;
}
}
fs.writeFileSync(nodePath + '/public/iobroker.json', JSON.stringify(obj));
if (callback) callback(err);
});
}
function saveObjects() {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
let cred = undefined;
let flows = undefined;
try {
if (fs.existsSync(userdataDir + 'flows_cred.json')) {
cred = JSON.parse(fs.readFileSync(userdataDir + 'flows_cred.json'));
}
} catch(e) {
adapter.log.error('Cannot save ' + userdataDir + 'flows_cred.json');
}
try {
if (fs.existsSync(userdataDir + 'flows.json')) {
flows = JSON.parse(fs.readFileSync(userdataDir + 'flows.json'));
}
} catch(e) {
adapter.log.error('Cannot save ' + userdataDir + 'flows.json');
}
//upload it to config
adapter.setObject('flows', {
common: {
name: 'Flows for node-red'
},
native: {
cred: cred,
flows: flows
},
type: 'config'
}, function () {
adapter.log.info('Save ' + userdataDir + 'flows.json');
});
}
function syncPublic(path) {
if (!path) path = '/public';
const dir = fs.readdirSync(__dirname + path);
if (!fs.existsSync(nodePath + path)) {
fs.mkdirSync(nodePath + path);
}
for (let i = 0; i < dir.length; i++) {
const stat = fs.statSync(__dirname + path + '/' + dir[i]);
if (stat.isDirectory()) {
syncPublic(path + '/' + dir[i]);
} else {
if (!fs.existsSync(nodePath + path + '/' + dir[i])) {
fs.createReadStream(__dirname + path + '/' + dir[i]).pipe(fs.createWriteStream(nodePath + path + '/' + dir[i]));
}
}
}
}
function installNotifierFlows(isFirst) {
if (!notificationsFlows) {
if (fs.existsSync(userdataDir + 'flows.json')) {
if (!isFirst) saveObjects();
// monitor project file
notificationsFlows = new Notify([userdataDir + 'flows.json']);
notificationsFlows.on('change', function () {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(saveObjects, 500);
});
} else {
// Try to install notifier every 10 seconds till the file will be created
setTimeout(function () {
installNotifierFlows();
}, 10000);
}
}
}
function installNotifierCreds(isFirst) {
if (!notificationsCreds) {
if (fs.existsSync(userdataDir + 'flows_cred.json')) {
if (!isFirst) saveObjects();
// monitor project file
notificationsCreds = new Notify([userdataDir + 'flows_cred.json']);
notificationsCreds.on('change', function () {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(saveObjects, 500);
});
} else {
// Try to install notifier every 10 seconds till the file will be created
setTimeout(function () {
installNotifierCreds();
}, 10000);
}
}
}
function main() {
// Find userdata directory
// normally /opt/iobroker/node_modules/iobroker.js-controller
// but can be /example/ioBroker.js-controller
const controllerDir = utils.controllerDir;
const parts = controllerDir.split('/');
if (parts.length > 1 && parts[parts.length - 2] === 'node_modules') {
parts.splice(parts.length - 2, 2);
userdataDir = parts.join('/');
userdataDir += '/iobroker-data/node-red/';
}
// create userdata directory
if (!fs.existsSync(userdataDir)) {
fs.mkdirSync(userdataDir);
}
syncPublic();
// Read configuration
adapter.getObject('flows', function (err, obj) {
if (obj && obj.native && obj.native.cred) {
const c = JSON.stringify(obj.native.cred);
// If really not empty
if (c !== '{}' && c !== '[]') {
fs.writeFileSync(userdataDir + 'flows_cred.json', JSON.stringify(obj.native.cred));
}
}
if (obj && obj.native && obj.native.flows) {
const f = JSON.stringify(obj.native.flows);
// If really not empty
if (f !== '{}' && f !== '[]') {
fs.writeFileSync(userdataDir + 'flows.json', JSON.stringify(obj.native.flows));
}
}
installNotifierFlows(true);
installNotifierCreds(true);
adapter.getForeignObject('system.config', (err, obj) => {
if (obj && obj.native && obj.native.secret) {
//noinspection JSUnresolvedVariable
secret = obj.native.secret;
}
// Create settings for node-red
writeSettings();
writeStateList(() => startNodeRed());
});
});
}

BIN
nodes/icons/iobroker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

330
nodes/ioBroker.html Normal file
View File

@ -0,0 +1,330 @@
<!--
Copyright 2013-2017 bluefox<dogafox@gmail.com>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/x-red" data-template-name="ioBroker in">
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="Topic" style="width:60%"><input type="button" id="node-input-topic-button" style="display:inline-block;width:10%" value="."/>
</div>
<div id="dialog-select-member" style="display:none"></div>
<div class="form-row">
<label for="node-input-payloadType"><i class="fa fa-envelope"></i> Payload</label>
<select id="node-input-payloadType" style="width:125px !important">
<option value="value">value</option>
<option value="object">object</option>
</select>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-onlyack" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-onlyack" style="width: 70%;">Send only on then ack==true</label>
</div>
<div class="form-row">
<label for="node-input-func"><i class="fa fa-wrench"></i> Mode</label>
<select type="text" id="node-input-func" style="width:74%;">
<option value="all">Send all events</option>
<option value="rbe">block unless value changes</option>
<option value="deadband">block unless changes by more than</option>
</select>
</div>
<div class="form-row" id="node-bandgap">
<label for="node-input-gap">&nbsp;</label>
<input type="text" id="node-input-gap" placeholder="e.g. 10 or 5%" style="width:71%;">
</div>
</script>
<script type="text/x-red" data-help-name="ioBroker in">
<p>ioBroker input node. Connects to a ioBroker and subscribes to the specified topic. The topic may contain redis wildcards (*).</p>
<p>Outputs an object called <b>msg</b> containing <b>msg.topic, msg.payload, msg.timestamp, msg.lastchange and msg.acknowledged.</p>
<p>The checkbox determines whether only States with ack == true or all events are forwarded.</p>
<p>The select box Mode offers further filtering options. The options are the same as for the RBE node.</p>
</script>
<link rel="stylesheet" type="text/css" href="vendor/ui.fancytree.min.css"/>
<style type="text/css">
#dialog-select-member select{
height: 24px !important;
line-height: 24px !important;
}
</style>
<script type="text/javascript" src="vendor/jquery.fancytree-all.min.js"></script>
<script type="text/javascript" src="selectID.js"></script>
<script type="text/javascript">
RED.nodes.registerType('ioBroker in',{
category: 'input',
defaults: {
name: {value:""},
topic: {value:"*",required:true},
payloadType: {value:'value'},
onlyack: {value:""},
func: {value:"all"},
gap: {value:"",validate:RED.validators.regex(/^(\d*[.]*\d*|)(%|)$/)}
},
color:"#a8bfd8",
inputs:0,
outputs:1,
icon: "ioBroker.png",
label: function() {
return this.name||this.topic||"ioBroker";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (!window.__iobroker) {
$.getJSON("iobroker.json", function(data) {
window.__iobroker = data;
$('#dialog-select-member').selectId('init', {
objects: window.__iobroker,
noMultiselect: true,
columns: ['name', 'role', 'enum', 'room'],
states: null,
noImg: true
});
});
} else {
$('#dialog-select-member').selectId('init', {
objects: window.__iobroker,
noMultiselect: true,
columns: ['name', 'role', 'enum', 'room'],
states: null,
noImg: true
});
}
$('#node-input-topic-button').button({
icons: {primary: 'ui-icon-folder-open'},
text: false
}).click(function () {
$('#dialog-select-member').selectId('show', $('#node-input-topic').val(), undefined, function (newId, oldId) {
var oldObj = $('#dialog-select-member').selectId('getInfo', oldId)
var newObj = $('#dialog-select-member').selectId('getInfo', newId)
$('#node-input-topic').val(newId);
var oldName = $('#node-input-name').val();
if (!oldName || !oldObj || (oldObj.common && oldName == oldObj.common.name && newObj.common)) {
$('#node-input-name').val(newObj.common.name);
}
});
});
$("#node-input-func").on("change",function() {
if ($("#node-input-func").val() == null) $("#node-input-func").val ("all");
if ($("#node-input-func").val() === "deadband") {
$("#node-bandgap").show();
} else {
$("#node-bandgap").hide();
}
});
}
});
</script>
<script type="text/x-red" data-template-name="ioBroker out">
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="Topic" style="width:60%"><input type="button" id="node-input-topic-button" style="display:inline-block;width:10%" value="."/>
</div>
<div id="dialog-select-member" style="display:none"></div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-ack"><i class="fa fa-arrow-up"></i> Type</label>
<select id="node-input-ack" style="width:73% !important">
<option value="true">value</option>
<option value="false">command</option>
</select>
</div>
<div class="form-row">
<label for="node-input-autoCreate"><i class="fa fa-arrow-up"></i> Auto create</label>
<select id="node-input-autoCreate" style="width:73% !important">
<option value="true">Create states if not exist</option>
<option value="false">Ignore messages for non existing states</option>
</select>
</div>
<div class="form-tips">Tip: Leave topic blank if you want to set them via msg properties.</div>
</script>
<script type="text/x-red" data-help-name="ioBroker out">
<p>Connects to a ioBroker and publishes <b>msg.payload</b> either to the <b>msg.topic</b> or to the topic specified in the edit window. The value in the edit window has precedence.</p>
<p>If <b>msg.payload</b> contains an object it will be stringified before being sent.</p>
<p>If <b>msg.payload</b> contains "__create__", the object will be only created, but no value will be written.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('ioBroker out',{
category: 'output',
defaults: {
name: {value:""},
topic: {value:""},
ack: {value:"false"},
autoCreate: {value: "false"}
},
color:"#a8bfd8",
inputs:1,
outputs:0,
icon: "ioBroker.png",
align: "right",
label: function() {
return this.name||this.topic||"ioBroker";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (!window.__iobroker) {
$.getJSON("iobroker.json", function(data) {
window.__iobroker = data;
$('#dialog-select-member').selectId('init', {
objects: window.__iobroker,
noMultiselect: true,
columns: ['name', 'role', 'enum', 'room'],
states: null,
noImg: true
});
});
} else {
$('#dialog-select-member').selectId('init', {
objects: window.__iobroker,
noMultiselect: true,
columns: ['name', 'role', 'enum', 'room'],
states: null,
noImg: true
});
}
$('#node-input-topic-button').button({
icons: {primary: 'ui-icon-folder-open'},
text: false
}).click(function () {
$('#dialog-select-member').selectId('show', $('#node-input-topic').val(), undefined, function (newId, oldId) {
var oldObj = $('#dialog-select-member').selectId('getInfo', oldId);
var newObj = $('#dialog-select-member').selectId('getInfo', newId);
$('#node-input-topic').val(newId);
var oldName = $('#node-input-name').val();
if (!oldName || !oldObj || (oldObj.common && oldName == oldObj.common.name && newObj.common)) {
$('#node-input-name').val(newObj.common.name);
}
});
});
}
});
</script>
<script type="text/x-red" data-template-name="ioBroker get">
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="Topic" style="width:60%"><input type="button" id="node-input-topic-button" style="display:inline-block;width:10%" value="."/>
</div>
<div id="dialog-select-member" style="display:none"></div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-payloadType"><i class="fa fa-envelope"></i> Payload</label>
<select id="node-input-payloadType" style="width:125px !important">
<option value="value">value</option>
<option value="object">object</option>
</select>
</div>
<div class="form-row">
<label for="node-input-attrname"><i class="fa fa-tag"></i> Attribute</label>
<input type="text" id="node-input-attrname" placeholder="attrname">
</div>
<div class="form-tips">Tip: Leave topic blank if you want to set them via msg properties.</div>
</script>
<script type="text/x-red" data-help-name="ioBroker get">
<p>Connects to a ioBroker and returns the requested value or the object in the massage attribute in the properties dialog, e.g. <b>msg.payload</b>. The object could be identified either by the <b>msg.topic</b> or specified in the poperties dialog. The value in the poperties dialog has precedence.</p>
<p>The msg object also contains the attributs <b>msg.timestamp</b>, <b>msg.lastchange</b> and <b>msg.acknowledged</b>. All other attributes ob the input msg object will be passed to the output msg object.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('ioBroker get',{
category: 'function',
defaults: {
name: {value:""},
topic: {value:""},
attrname: {value:"payload"},
payloadType: {value:'value'}
},
color:"#a8bfd8",
inputs:1,
outputs:1,
icon: "ioBroker.png",
label: function() {
return this.name||this.topic||"ioBroker get";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (!window.__iobroker) {
$.getJSON("iobroker.json", function(data) {
window.__iobroker = data;
$('#dialog-select-member').selectId('init', {
objects: window.__iobroker,
noMultiselect: true,
columns: ['name', 'role', 'enum', 'room'],
states: null,
noImg: true
});
});
} else {
$('#dialog-select-member').selectId('init', {
objects: window.__iobroker,
noMultiselect: true,
columns: ['name', 'role', 'enum', 'room'],
states: null,
noImg: true
});
}
$('#node-input-topic-button').button({
icons: {primary: 'ui-icon-folder-open'},
text: false
}).click(function () {
$('#dialog-select-member').selectId('show', $('#node-input-topic').val(), undefined, function (newId, oldId) {
var oldObj = $('#dialog-select-member').selectId('getInfo', oldId);
var newObj = $('#dialog-select-member').selectId('getInfo', newId);
$('#node-input-topic').val(newId);
var oldName = $('#node-input-name').val();
if (!oldName || !oldObj || (oldObj.common && oldName == oldObj.common.name && newObj.common)) {
$('#node-input-name').val(newObj.common.name);
}
});
});
}
});
</script>

389
nodes/ioBroker.js Normal file
View File

@ -0,0 +1,389 @@
/**
* Copyright 2013,2014 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
'use strict';
require('events').EventEmitter.prototype._maxListeners = 100;
var util = require('util');
var utils = require(__dirname + '/../lib/utils');
//var redis = require("redis");
//var hashFieldRE = /^([^=]+)=(.*)$/;
// Get the redis address
var settings = require(process.env.NODE_RED_HOME + '/red/red').settings;
var instance = settings.get('iobrokerInstance') || 0;
var config = settings.get('iobrokerConfig');
var valueConvert = settings.get('valueConvert');
if (typeof config == 'string') {
config = JSON.parse(config);
}
var adapter;
try {
adapter = utils.Adapter({name: 'node-red', instance: instance, config: config});
} catch(e) {
console.log(e);
}
var nodes = [];
var nodeSets = [];
var ready = false;
var log = adapter && adapter.log && adapter.log.warn ? adapter.log.warn : console.log;
adapter.on('ready', function () {
ready = true;
adapter.subscribeForeignStates('*');
while (nodes.length) {
var node = nodes.pop();
if (node instanceof IOBrokerInNode) {
adapter.on('stateChange', node.stateChange);
}
node.status({fill: 'green', shape: 'dot', text: 'connected'});
}
var count = 0;
while (nodeSets.length) {
var nodeSetData = nodeSets.pop();
nodeSetData.node.emit('input', nodeSetData.msg);
count++;
}
if (count > 0) log(count + ' queued state values set in ioBroker');
});
// name is like system.state, pattern is like "*.state" or "*" or "*system*"
function getRegex(pattern) {
if (!pattern || pattern === '*') return null;
if (pattern.indexOf('*') === -1) return null;
if (pattern[pattern.length - 1] !== '*') pattern = pattern + '$';
if (pattern[0] !== '*') pattern = '^' + pattern;
pattern = pattern.replace(/\*/g, '[a-zA-Z0-9.\s]');
pattern = pattern.replace(/\./g, '\\.');
return new RegExp(pattern);
}
function checkState(node, id, val, callback) {
if (node.idChecked) {
return callback && callback();
}
if (node.topic) {
node.idChecked = true;
}
adapter.getObject(id, function (err, obj) {
if (!obj) {
adapter.getForeignObject(id, function (err, obj) {
if (!obj) {
log('State "' + id + '" was created in the ioBroker as ' + adapter._fixId(id));
// Create object
adapter.setObject(id, {
common: {
name: id,
role: 'info',
type: 'state',
desc: 'Created by Node-Red'
},
native: {},
type: 'state'
}, function (err) {
if (val !== undefined && val !== null && val !== '__create__') {
adapter.setState(id, val, function () {
callback && callback();
});
} else {
adapter.setState(id, undefined, function () {
callback && callback();
});
}
});
} else {
node._id = obj._id;
if (val !== undefined && val !== null && val !== '__create__') {
adapter.setForeignState(obj._id, val, function () {
callback && callback();
});
} else {
callback && callback();
}
}
});
} else {
if (val !== undefined && val !== null && val !== '__create__') {
adapter.setForeignState(obj._id, val, function () {
callback && callback();
});
} else {
callback && callback();
}
}
});
}
function IOBrokerInNode(n) {
var node = this;
RED.nodes.createNode(node,n);
node.topic = (n.topic || '*').replace(/\//g, '.');
node.regex = new RegExp('^node-red\\.' + instance + '\\.');
// If no adapter prefix, add own adapter prefix
if (node.topic && node.topic.indexOf('.') === -1) {
node.topic = adapter.namespace + '.' + node.topic;
}
node.regexTopic = getRegex(this.topic);
node.payloadType = n.payloadType;
node.onlyack = (n.onlyack == true || false);
node.func = n.func || 'all';
node.gap = n.gap || '0';
node.pc = false;
if (node.gap.substr(-1) === '%') {
node.pc = true;
node.gap = parseFloat(node.gap);
}
node.g = node.gap;
node.previous = {};
if (node.topic) {
var id = node.topic;
// If no wildchars and belongs to this adapter
if (id.indexOf('*') === -1 && (node.regex.test(id) || id.indexOf('.') !== -1)) {
checkState(node, id);
}
}
if (ready) {
node.status({fill: 'green', shape: 'dot', text: 'connected'});
} else {
node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
}
node.stateChange = function(topic, obj) {
if (node.regexTopic) {
if (!node.regexTopic.test(topic)) return;
} else if (node.topic !== '*' && node.topic !== topic) {
return;
}
if (node.onlyack && obj.ack != true) return;
var t = topic.replace(/\./g, '/') || '_no_topic';
//node.log ("Function: " + node.func);
if (node.func === 'rbe') {
if (obj.val === node.previous[t]) {
return;
}
} else if (node.func === 'deadband') {
var n = parseFloat(obj.val.toString());
if (!isNaN(n)) {
//node.log('Old Value: ' + node.previous[t] + ' New Value: ' + n);
if (node.pc) { node.gap = (node.previous[t] * node.g / 100) || 0; }
if (!node.previous.hasOwnProperty(t)) {
node.previous[t] = n - node.gap;
}
if (!Math.abs(n - node.previous[t]) >= node.gap) {
return;
}
} else {
node.warn('no number found in value');
return;
}
}
node.previous[t] = obj.val;
node.send({
topic: t,
payload: (node.payloadType === 'object') ? obj : ((obj.val === null || obj.val === undefined) ? '' : (valueConvert ? obj.val.toString() : obj.val)),
acknowledged:obj.ack,
timestamp: obj.ts,
lastchange: obj.lc,
from: obj.from
});
node.status({fill: 'green', shape: 'dot', text: (node.payloadType === 'object') ? JSON.stringify(obj) : ((obj.val === null || obj.val === undefined) ? '' : obj.val.toString() ) });
};
node.on('close', function() {
adapter.removeListener('stateChange', node.stateChange);
});
if (ready) {
adapter.on('stateChange', node.stateChange);
} else {
nodes.push(node);
}
}
RED.nodes.registerType('ioBroker in', IOBrokerInNode);
function IOBrokerOutNode(n) {
var node = this;
RED.nodes.createNode(node,n);
node.topic = n.topic;
node.ack = (n.ack === 'true' || n.ack === true);
node.autoCreate = (n.autoCreate === 'true' || n.autoCreate === true);
node.regex = new RegExp('^node-red\\.' + instance + '\\.');
if (ready) {
node.status({fill: 'green', shape: 'dot', text: 'connected'});
} else {
node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
}
function setState(id, val, ack) {
if (node.idChecked) {
if (val !== '__create__') {
adapter.setState(id, {val: val, ack: ack});
}
} else {
checkState(node, id, {val: val, ack: ack});
}
}
node.on('input', function(msg) {
var id = node.topic || msg.topic;
if (!ready) {
nodeSets.push({'node': node, 'msg': msg});
//log('Message for "' + id + '" queued because ioBroker connection not initialized');
return;
}
if (id) {
id = id.replace(/\//g, '.');
// Create variable if not exists
if (node.autoCreate && !node.idChecked) {
id = id.replace(/\//g, '.');
// If no wildchars and belongs to this adapter
if (id.indexOf('*') === -1 && (node.regex.test(id) || id.indexOf('.') !== -1)) {
checkState(node, id);
}
}
// If not this adapter state
if (!node.regex.test(id) && id.indexOf('.') !== -1) {
// Check if state exists
adapter.getForeignState(id, function (err, state) {
if (!err && state) {
adapter.setForeignState(id, {val: msg.payload, ack: node.ack});
node.status({fill: 'green', shape: 'dot', text: msg.payload.toString() });
} else {
log('State "' + id + '" does not exist in the ioBroker');
}
});
} else {
if (id.indexOf('*') !== -1) {
log('Invalid topic name "' + id + '" for ioBroker');
} else {
setState(id, msg.payload, node.ack);
node.status({fill: 'green', shape: 'dot', text: msg.payload.toString() });
}
}
} else {
node.warn('No key or topic set');
}
});
if (!ready) {
nodes.push(node);
}
//node.on("close", function() {
//
// });
}
RED.nodes.registerType('ioBroker out', IOBrokerOutNode);
function IOBrokerGetNode(n) {
var node = this;
RED.nodes.createNode(node,n);
node.topic = (typeof n.topic=== 'string' && n.topic.length > 0 ? n.topic.replace(/\//g, '.') : null) ;
// If no adapter prefix, add own adapter prefix
if (node.topic && node.topic.indexOf('.') === -1) {
node.topic = adapter.namespace + '.' + node.topic;
}
node.regex = new RegExp('^node-red\\.' + instance + '\\.');
//node.regex = getRegex(this.topic);
node.payloadType = n.payloadType;
node.attrname = n.attrname;
if (node.topic) {
var id = node.topic;
// If no wildchars and belongs to this adapter
if (id.indexOf('*') === -1 && (node.regex.test(id) || id.indexOf('.') !== -1)) {
checkState(node, id);
}
}
if (ready) {
node.status({fill: 'green', shape: 'dot', text: 'connected'});
} else {
node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
}
node.getStateValue = function (msg) {
return function (err, state) {
if (!err && state) {
msg[node.attrname] = (node.payloadType === 'object') ? state : ((state.val === null || state.val === undefined) ? '' : (valueConvert ? state.val.toString() : state.val));
msg.acknowledged = state.ack;
msg.timestamp = state.ts;
msg.lastchange = state.lc;
node.status({
fill: 'green',
shape: 'dot',
text: (node.payloadType === 'object') ? JSON.stringify(state) : ((state.val === null || state.val === undefined) ? '' : state.val.toString())
});
node.send(msg);
} else {
log('State "' + id + '" does not exist in the ioBroker');
}
};
};
node.on('input', function(msg) {
var id = node.topic || msg.topic;
if (!ready) {
nodeSets.push({'node': node, 'msg': msg});
//log('Message for "' + id + '" queued because ioBroker connection not initialized');
return;
}
if (id) {
id = id.replace(/\//g, '.');
// If not this adapter state
if (!node.regex.test(id) && id.indexOf('.') !== -1) {
// Check if state exists
adapter.getForeignState(id, node.getStateValue(msg));
} else {
if (id.indexOf('*') !== -1) {
log('Invalid topic name "' + id + '" for ioBroker');
} else {
adapter.getState(id, node.getStateValue(msg));
}
}
} else {
node.warn('No key or topic set');
}
});
if (!ready) {
nodes.push(node);
}
}
RED.nodes.registerType('ioBroker get', IOBrokerGetNode);
};

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "iobroker.node-red",
"description": "This adapter uses node-red as a service of ioBroker. No additional node-red instance required.",
"version": "1.7.1",
"author": {
"name": "bluefox",
"email": "dogafox@gmail.com"
},
"contributors": [
{
"name": "bluefox",
"email": "dogafox@gmail.com"
}
],
"homepage": "https://github.com/ioBroker/ioBroker.node-red",
"license": "Apache-2.0",
"keywords": [
"ioBroker",
"node-red",
"home automation"
],
"repository": {
"type": "git",
"url": "https://github.com/ioBroker/ioBroker.node-red"
},
"optionalDependencies": {
"js2xmlparser": "^3.0.0",
"fs.notify": "^0.0.4",
"feedparser": "^2.2.9",
"mongodb": "^3.1.3"
},
"dependencies": {
"node-red": "^0.19.4",
"node-red-contrib-os": "^0.1.7",
"node-red-dashboard": "^2.9.8",
"node-red-contrib-aggregator": "^1.3.0",
"node-red-contrib-polymer": "^0.0.21"
},
"devDependencies": {
"gulp": "^3.9.1",
"request": "^2.88.0",
"mocha": "^5.2.0",
"chai": "^4.1.2"
},
"bugs": {
"url": "https://github.com/ioBroker/ioBroker.node-red/issues"
},
"main": "main.js",
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit"
},
"readmeFilename": "README.md"
}

176
public/schedule/LICENSE.txt Normal file
View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<!--
Copyright 2014 Nicholas J Humfrey
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<head>
<title>Node-RED Schedule</title>
<link rel="shortcut icon" href="../favicon.ico" />
<meta charset="utf-8" />
<script src="../jquery/js/jquery-1.11.1.min.js"></script>
<script src="../bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="../bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="./style.css" />
<script src="./schedule.js"></script>
</head>
<body spellcheck="false">
<div class="navbar navbar-inverse navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="brand" href="#"><img src="../node-red.png"> <span class="red">Node-RED Schedule</span> </a>
<div class="btn-group pull-right">
<a class="btn btn-primary" href="../" id="btn-back"><i class="icon-home icon-white"></i> Back to Admin</a>
</div>
</div>
</div>
</div>
<div class="container">
<table id="schedule" class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Hour</th>
<th>Min</th>
<th>Day</th>
<th>Month</th>
<th>Day of Week</th>
<th>Topic</th>
<th>Payload</th>
</thead>
<tbody>
</tbody>
</table>
</div>
</body>
</html>

28
public/schedule/style.css Normal file
View File

@ -0,0 +1,28 @@
body {
font: 14px "Helvetica" !important;
}
a.brand {
line-height: 16px;
}
a.brand span {
vertical-align: middle;
font-size: 16px !important;
}
a.brand:hover span.red {
color: rgb(197, 112, 112) !important;
}
a.brand:hover {
color: #bbb !important;
}
a.brand img {
height: 16px;
}
#schedule {
margin-top: 20px;
}

2465
public/selectID.js Normal file

File diff suppressed because it is too large Load Diff

BIN
public/vendor/icons.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

File diff suppressed because one or more lines are too long

8
public/vendor/ui.fancytree.min.css vendored Normal file

File diff suppressed because one or more lines are too long

144
settings.js Normal file
View File

@ -0,0 +1,144 @@
/**
* Copyright 2013 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
// The `https` setting requires the `fs` module. Uncomment the following
// to make it available:
//var fs = require("fs");
module.exports = {
// the tcp port that the Node-RED web server is listening on
uiPort: '%%port%%',
uiHost: '%%bind%%',
// By default, the Node-RED UI accepts connections on all IPv4 interfaces.
// The following property can be used to listen on a specific interface. For
// example, the following would only allow connections from the local machine.
//uiHost: "127.0.0.1",
iobrokerInstance: '%%instance%%',
iobrokerConfig: '%%config%%',
// Retry time in milliseconds for MQTT connections
mqttReconnectTime: 15000,
// Retry time in milliseconds for Serial port connections
serialReconnectTime: 15000,
// Retry time in milliseconds for TCP socket connections
//socketReconnectTime: 10000,
// Timeout in milliseconds for TCP server socket connections
// defaults to no timeout
//socketTimeout: 120000,
// Maximum number of lines in debug window before pruning
debugMaxLength: 1000,
// The file containing the flows. If not set, it defaults to flows_<hostname>.json
flowFile: 'flows.json',
// To enabled pretty-printing of the flow within the flow file, set the following
// property to true:
flowFilePretty: true,
// By default, all user data is stored in the Node-RED install directory. To
// use a different location, the following property can be used
userDir: __dirname + '/',
// Node-RED scans the `nodes` directory in the install directory to find nodes.
// The following property can be used to specify an additional directory to scan.
nodesDir: '%%nodesdir%%',
// By default, the Node-RED UI is available at http://localhost:1880/
// The following property can be used to specify a different root path.
// If set to false, this is disabled.
//httpAdminRoot: '/admin',
// You can protect the user interface with a userid and password by using the following property.
// The password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password')
httpAdminAuth: '%%auth%%',
// Some nodes, such as HTTP In, can be used to listen for incoming http requests.
// By default, these are served relative to '/'. The following property
// can be used to specifiy a different root path. If set to false, this is
// disabled.
//httpNodeRoot: '/nodes',
// To password protect the node-defined HTTP endpoints, the following property
// can be used.
// The password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password')
//httpNodeAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"},
// When httpAdminRoot is used to move the UI to a different root path, the
// following property can be used to identify a directory of static content
// that should be served at http://localhost:1880/.
//httpStatic: '/home/nol/node-red-dashboard/',
// To password protect the static content, the following property can be used.
// The password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password')
//httpStaticAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"},
// The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot',
// to apply the same root to both parts.
httpRoot: "'%%httpRoot%%'",
// The following property can be used in place of 'httpAdminAuth' and 'httpNodeAuth',
// to apply the same authentication to both parts.
//httpAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"},
// The following property can be used to disable the editor. The admin API
// is not affected by this option. To disable both the editor and the admin
// API, use either the httpRoot or httpAdminRoot properties
//disableEditor: false,
// The following property can be used to enable HTTPS
// See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
// for details on its contents.
// See the comment at the top of this file on how to load the `fs` module used by
// this setting.
//
//https: {
// key: fs.readFileSync('privatekey.pem'),
// cert: fs.readFileSync('certificate.pem')
//},
// The following property can be used to configure cross-origin resource sharing
// in the HTTP nodes.
// See https://github.com/troygoode/node-cors#configuration-options for
// details on its contents. The following is a basic permissive set of options:
//httpNodeCors: {
// origin: "*",
// methods: "GET,PUT,POST,DELETE"
//},
// Anything in this hash is globally available to all functions.
// It is accessed as context.global.
// eg:
// functionGlobalContext: { os:require('os') }
// can be accessed in a function block as:
// context.global.os
valueConvert: '%%valueConvert%%',
credentialSecret: "'%%credentialSecret%%'",
functionGlobalContext: {
//'%%functionGlobalContext%%'
// os:require('os'),
// bonescript:require('bonescript'),
// arduino:require('duino')
}
};

17
tasks/jscs.js Normal file
View File

@ -0,0 +1,17 @@
var srcDir = __dirname + "/../";
module.exports = {
all: {
src: [
srcDir + "*.js",
srcDir + "lib/*.js",
srcDir + "adapter/example/*.js",
srcDir + "tasks/**/*.js",
srcDir + "www/**/*.js",
'!' + srcDir + "www/lib/**/*.js",
'!' + srcDir + 'node_modules/**/*.js',
'!' + srcDir + 'adapter/*/node_modules/**/*.js'
],
options: require('./jscsRules.js')
}
};

36
tasks/jscsRules.js Normal file
View File

@ -0,0 +1,36 @@
module.exports = {
force: true,
"requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], /*"if",*/
"requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"],
"requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true,
"disallowSpacesInFunctionDeclaration": {"beforeOpeningRoundBrace": true},
"disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true},
"requireSpacesInFunctionExpression": {"beforeOpeningCurlyBrace": true},
"requireSpacesInAnonymousFunctionExpression": {"beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true},
"requireSpacesInNamedFunctionExpression": {"beforeOpeningCurlyBrace": true},
"requireSpacesInFunctionDeclaration": {"beforeOpeningCurlyBrace": true},
"disallowMultipleVarDecl": true,
"requireBlocksOnNewline": true,
"disallowEmptyBlocks": true,
"disallowSpacesInsideObjectBrackets": true,
"disallowSpacesInsideArrayBrackets": true,
"disallowSpaceAfterObjectKeys": true,
"disallowSpacesInsideParentheses": true,
"requireCommaBeforeLineBreak": true,
//"requireAlignedObjectValues": "all",
"requireOperatorBeforeLineBreak": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
// "disallowLeftStickedOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
// "requireRightStickedOperators": ["!"],
// "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
//"disallowSpaceAfterBinaryOperators": [","],
"disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"],
"disallowSpaceBeforePostfixUnaryOperators": ["++", "--"],
"requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
"requireSpaceAfterBinaryOperators": ["?", ">", ",", ">=", "<=", "<", "+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
//"validateIndentation": 4,
//"validateQuoteMarks": { "mark": "\"", "escape": true },
"disallowMixedSpacesAndTabs": true,
"disallowKeywordsOnNewLine": ["else", "catch"]
};

17
tasks/jshint.js Normal file
View File

@ -0,0 +1,17 @@
var srcDir = __dirname + "/../";
module.exports = {
options: {
force: true
},
all: [
srcDir + "*.js",
srcDir + "lib/*.js",
srcDir + "adapter/example/*.js",
srcDir + "tasks/**/*.js",
srcDir + "www/**/*.js",
'!' + srcDir + "www/lib/**/*.js",
'!' + srcDir + 'node_modules/**/*.js',
'!' + srcDir + 'adapter/*/node_modules/**/*.js'
]
};

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

@ -0,0 +1,728 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
// check if tmp directory exists
var fs = require('fs');
var path = require('path');
var child_process = require('child_process');
var rootDir = path.normalize(__dirname + '/../../');
var pkg = require(rootDir + 'package.json');
var debug = typeof v8debug === 'object';
pkg.main = pkg.main || 'main.js';
var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/');
adapterName = adapterName[adapterName.length - 2];
var adapterStarted = false;
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 3].split('.')[0];
}
var appName = getAppName().toLowerCase();
var objects;
var states;
var pid = null;
function copyFileSync(source, target) {
var targetFile = target;
//if target is a directory a new file with the same name will be created
if (fs.existsSync(target)) {
if ( fs.lstatSync( target ).isDirectory() ) {
targetFile = path.join(target, path.basename(source));
}
}
try {
fs.writeFileSync(targetFile, fs.readFileSync(source));
}
catch (err) {
console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)");
}
}
function copyFolderRecursiveSync(source, target, ignore) {
var files = [];
var base = path.basename(source);
if (base === adapterName) {
base = pkg.name;
}
//check if folder needs to be created or integrated
var targetFolder = path.join(target, base);
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
//copy
if (fs.lstatSync(source).isDirectory()) {
files = fs.readdirSync(source);
files.forEach(function (file) {
if (ignore && ignore.indexOf(file) !== -1) {
return;
}
var curSource = path.join(source, file);
var curTarget = path.join(targetFolder, file);
if (fs.lstatSync(curSource).isDirectory()) {
// ignore grunt files
if (file.indexOf('grunt') !== -1) return;
if (file === 'chai') return;
if (file === 'mocha') return;
copyFolderRecursiveSync(curSource, targetFolder, ignore);
} else {
copyFileSync(curSource, curTarget);
}
});
}
}
if (!fs.existsSync(rootDir + 'tmp')) {
fs.mkdirSync(rootDir + 'tmp');
}
function storeOriginalFiles() {
console.log('Store original files...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) {
objects['system.adapter.admin.0'].common.enabled = false;
}
if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) {
objects['system.adapter.admin.1'].common.enabled = false;
}
fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects));
try {
f = fs.readFileSync(dataDir + 'states.json');
fs.writeFileSync(dataDir + 'states.json.original', f);
}
catch (err) {
console.log('no states.json found - ignore');
}
}
function restoreOriginalFiles() {
console.log('restoreOriginalFiles...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json.original');
fs.writeFileSync(dataDir + 'objects.json', f);
try {
f = fs.readFileSync(dataDir + 'states.json.original');
fs.writeFileSync(dataDir + 'states.json', f);
}
catch (err) {
console.log('no states.json.original found - ignore');
}
}
function checkIsAdapterInstalled(cb, counter, customName) {
customName = customName || pkg.name.split('.').pop();
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsAdapterInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.' + customName + '.0']) {
console.log('checkIsAdapterInstalled: ready!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
} else {
console.warn('checkIsAdapterInstalled: still not ready');
}
} catch (err) {
}
if (counter > 20) {
console.error('checkIsAdapterInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsAdapterInstalled: wait...');
setTimeout(function() {
checkIsAdapterInstalled(cb, counter + 1);
}, 1000);
}
}
function checkIsControllerInstalled(cb, counter) {
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsControllerInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0']) {
console.log('checkIsControllerInstalled: installed!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
}
} catch (err) {
}
if (counter > 20) {
console.log('checkIsControllerInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsControllerInstalled: wait...');
setTimeout(function() {
checkIsControllerInstalled(cb, counter + 1);
}, 1000);
}
}
function installAdapter(customName, cb) {
if (typeof customName === 'function') {
cb = customName;
customName = null;
}
customName = customName || pkg.name.split('.').pop();
console.log('Install adapter...');
var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js';
// make first install
if (debug) {
child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
} else {
// add controller
var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
waitForEnd(_pid, function () {
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
});
}
}
function waitForEnd(_pid, cb) {
if (!_pid) {
cb(-1, -1);
return;
}
_pid.on('exit', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
_pid.on('close', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
}
function installJsController(cb) {
console.log('installJsController...');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') ||
!fs.existsSync(rootDir + 'tmp/' + appName + '-data')) {
// try to detect appName.js-controller in node_modules/appName.js-controller
// travis CI installs js-controller into node_modules
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"');
// copy all
// stop controller
console.log('Stop controller if running...');
var _pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js stop', {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// copy all files into
if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp');
if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){
console.log('Copy js-controller...');
copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/');
}
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js setup first --console', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
__pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(__pid, function () {
checkIsControllerInstalled(function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
console.log('Setup finished.');
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
});
} else {
// check if port 9000 is free, else admin adapter will be added to running instance
var client = new require('net').Socket();
client.connect(9000, '127.0.0.1', function() {
console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.');
process.exit(0);
});
setTimeout(function () {
client.destroy();
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => install from git');
child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', {
cwd: rootDir + 'tmp/',
stdio: [0, 1, 2]
});
} else {
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
child_process.exec('node ' + appName + '.js setup first', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
child_process.fork(appName + '.js', ['setup', 'first'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
}
// let npm install admin and run setup
checkIsControllerInstalled(function () {
var _pid;
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
}, 1000);
}
} else {
setTimeout(function () {
console.log('installJsController: js-controller installed');
if (cb) cb(false);
}, 0);
}
}
function copyAdapterToController() {
console.log('Copy adapter...');
// Copy adapter to tmp/node_modules/appName.adapter
copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']);
console.log('Adapter copied.');
}
function clearControllerLog() {
var dirPath = rootDir + 'tmp/log';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear controller log...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Controller log cleared');
} catch (err) {
console.error('cannot clear log: ' + err);
}
}
}
function clearDB() {
var dirPath = rootDir + 'tmp/iobroker-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;
}

105
test/testFunctions.js Normal file
View File

@ -0,0 +1,105 @@
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
var request = require('request');
var objects = null;
var states = null;
var onStateChanged = null;
var onObjectChanged = null;
var port = 18888;
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
if (counter > 20) {
cb && cb('Cannot check connection');
return;
}
states.getState('system.adapter.node-red.0.alive', function (err, state) {
if (err) console.error(err);
if (state && state.val) {
cb && cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
cb && cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, function (err, state) {
if (err) console.error(err);
if (value === null && !state) {
cb && cb();
} else
if (state && (value === undefined || state.val === value)) {
cb && cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
describe('Test node-red', function() {
before('Test node-red: 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.port = port;
setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function (id, obj) {
if (onObjectChanged) onObjectChanged(id, obj);
}, function (id, state) {
if (onStateChanged) onStateChanged(id, state);
},
function (_objects, _states) {
objects = _objects;
states = _states;
states.subscribe('*');
_done();
});
});
});
it('Test node-red: Check if adapter started', function (done) {
this.timeout(5000);
checkConnectionOfAdapter(done);
});
it('Test node-red: check creation of state', function (done) {
this.timeout(20000);
// check if node-red is running
setTimeout(function () {
request('http://localhost:' + port, function (error, response, body) {
expect(error).to.be.not.ok;
expect(body).to.be.ok;
done();
});
}, 5000);
});
after('Test node-red: Stop js-controller', function (done) {
this.timeout(6000);
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('iobroker') !== -1 ||
ioPackage.common.title.indexOf('ioBroker') !== -1 ||
ioPackage.common.title.indexOf('adapter') !== -1 ||
ioPackage.common.title.indexOf('Adapter') !== -1
) {
console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
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();
});
});