Initial commit

This commit is contained in:
zhongjin 2018-09-30 14:10:06 +08:00
commit e3af2bd74a
22 changed files with 2240 additions and 0 deletions

28
.codeclimate.yml Normal file
View File

@ -0,0 +1,28 @@
---
engines:
duplication:
enabled: true
config:
languages:
- javascript
exclude_fingerprints:
- 43e0e1e6524762f837c44bbffd1e763c
- 96c5a1fe99411cfd260780387b4f2345
eslint:
enabled: true
checks:
complexity:
enabled: false
exclude_fingerprints:
- c4c98b5bf6e9d6534149e4038e291b1a
- 055faf39892c98b5e4b310702bd90ade
- 04f09dc503b35020ec99b1779c671a68
fixme:
enabled: true
ratings:
paths:
- "**.js"
exclude_paths:
- test/
- lib/utils.js
- Gruntfile.js

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
**/*{.,-}min.js

213
.eslintrc Normal file
View File

@ -0,0 +1,213 @@
ecmaFeatures:
modules: true
jsx: true
env:
amd: true
browser: true
es6: true
jquery: true
node: true
# http://eslint.org/docs/rules/
rules:
# Possible Errors
comma-dangle: [2, never]
no-cond-assign: 2
no-console: 0
no-constant-condition: 2
no-control-regex: 2
no-debugger: 2
no-dupe-args: 2
no-dupe-keys: 2
no-duplicate-case: 2
no-empty: 2
no-empty-character-class: 2
no-ex-assign: 2
no-extra-boolean-cast: 2
no-extra-parens: 0
no-extra-semi: 2
no-func-assign: 2
no-inner-declarations: [2, functions]
no-invalid-regexp: 2
no-irregular-whitespace: 2
no-negated-in-lhs: 2
no-obj-calls: 2
no-regex-spaces: 2
no-sparse-arrays: 2
no-unexpected-multiline: 2
no-unreachable: 2
use-isnan: 2
valid-jsdoc: 0
valid-typeof: 2
# Best Practices
accessor-pairs: 2
block-scoped-var: 0
complexity: [2, 6]
consistent-return: 0
curly: 0
default-case: 0
dot-location: 0
dot-notation: 0
eqeqeq: 2
guard-for-in: 2
no-alert: 2
no-caller: 2
no-case-declarations: 2
no-div-regex: 2
no-else-return: 0
no-empty-label: 2
no-empty-pattern: 2
no-eq-null: 2
no-eval: 2
no-extend-native: 2
no-extra-bind: 2
no-fallthrough: 2
no-floating-decimal: 0
no-implicit-coercion: 0
no-implied-eval: 2
no-invalid-this: 0
no-iterator: 2
no-labels: 0
no-lone-blocks: 2
no-loop-func: 2
no-magic-number: 0
no-multi-spaces: 0
no-multi-str: 0
no-native-reassign: 2
no-new-func: 2
no-new-wrappers: 2
no-new: 2
no-octal-escape: 2
no-octal: 2
no-proto: 2
no-redeclare: 2
no-return-assign: 2
no-script-url: 2
no-self-compare: 2
no-sequences: 0
no-throw-literal: 0
no-unused-expressions: 2
no-useless-call: 2
no-useless-concat: 2
no-void: 2
no-warning-comments: 0
no-with: 2
radix: 2
vars-on-top: 0
wrap-iife: 2
yoda: 0
# Strict
strict: 0
# Variables
init-declarations: 0
no-catch-shadow: 2
no-delete-var: 2
no-label-var: 2
no-shadow-restricted-names: 2
no-shadow: 0
no-undef-init: 2
no-undef: 0
no-undefined: 0
no-unused-vars: 0
no-use-before-define: 0
# Node.js and CommonJS
callback-return: 2
global-require: 2
handle-callback-err: 2
no-mixed-requires: 0
no-new-require: 0
no-path-concat: 2
no-process-exit: 2
no-restricted-modules: 0
no-sync: 0
# Stylistic Issues
array-bracket-spacing: 0
block-spacing: 0
brace-style: 0
camelcase: 0
comma-spacing: 0
comma-style: 0
computed-property-spacing: 0
consistent-this: 0
eol-last: 0
func-names: 0
func-style: 0
id-length: 0
id-match: 0
indent: 0
jsx-quotes: 0
key-spacing: 0
linebreak-style: 0
lines-around-comment: 0
max-depth: 0
max-len: 0
max-nested-callbacks: 0
max-params: 0
max-statements: [2, 30]
new-cap: 0
new-parens: 0
newline-after-var: 0
no-array-constructor: 0
no-bitwise: 0
no-continue: 0
no-inline-comments: 0
no-lonely-if: 0
no-mixed-spaces-and-tabs: 0
no-multiple-empty-lines: 0
no-negated-condition: 0
no-nested-ternary: 0
no-new-object: 0
no-plusplus: 0
no-restricted-syntax: 0
no-spaced-func: 0
no-ternary: 0
no-trailing-spaces: 0
no-underscore-dangle: 0
no-unneeded-ternary: 0
object-curly-spacing: 0
one-var: 0
operator-assignment: 0
operator-linebreak: 0
padded-blocks: 0
quote-props: 0
quotes: 0
require-jsdoc: 0
semi-spacing: 0
semi: 0
sort-vars: 0
space-after-keywords: 0
space-before-blocks: 0
space-before-function-paren: 0
space-before-keywords: 0
space-in-parens: 0
space-infix-ops: 0
space-return-throw-case: 0
space-unary-ops: 0
spaced-comment: 0
wrap-regex: 0
# ECMAScript 6
arrow-body-style: 0
arrow-parens: 0
arrow-spacing: 0
constructor-super: 0
generator-star-spacing: 0
no-arrow-condition: 0
no-class-assign: 0
no-const-assign: 0
no-dupe-class-members: 0
no-this-before-super: 0
no-var: 0
object-shorthand: 0
prefer-arrow-callback: 0
prefer-const: 0
prefer-reflect: 0
prefer-spread: 0
prefer-template: 0
require-yield: 0

15
.npmignore Normal file
View File

@ -0,0 +1,15 @@
Gruntfile.js
tasks
node_modules
.idea
.gitignore
.git
.DS_Store
test/lib
test/testAdapter.js
.travis.yml
appveyor.yml
.codeclimate.yml
.eslinkignore
.eslintrc
.vscode

29
.travis.yml Normal file
View File

@ -0,0 +1,29 @@
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
- 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter; fi'
- 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then chmod +x ./cc-test-reporter; fi'
- npm install winston@2.3.1
- 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production'
- 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./cc-test-reporter before-build; fi'
after_success:
- sed -i "s|tmp/node_modules/iobroker.nut/||" ./coverage/lcov.info
- 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT -r 3623f3070bd9cae772a6757bdb0bcac4f8c2ebfbdebbe8e577bea9275810950d; fi'
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

29
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Programm starten",
"program": "${workspaceRoot}/nut.js",
"cwd": "${workspaceRoot}",
"args": [
"--debug", "--force", "--trace-warnings"
]
},
{
"type": "node",
"request": "attach",
"name": "An den Prozess anfügen",
"port": 58585,
"address": "cubietruck4",
"restart": false,
"sourceMaps": false,
"localRoot": "${workspaceRoot}",
"remoteRoot": null
}
]
}

21
LICENSE Normal file
View File

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

132
README.md Normal file
View File

@ -0,0 +1,132 @@
![Logo](admin/nut.png)
# ioBroker.nut
[![Greenkeeper badge](https://badges.greenkeeper.io/Apollon77/ioBroker.nut.svg)](https://greenkeeper.io/)
[![NPM version](http://img.shields.io/npm/v/iobroker.nut.svg)](https://www.npmjs.com/package/iobroker.nut)
[![Downloads](https://img.shields.io/npm/dm/iobroker.nut.svg)](https://www.npmjs.com/package/iobroker.nut)
[![Dependency Status](https://gemnasium.com/badges/github.com/Apollon77/ioBroker.nut.svg)](https://gemnasium.com/github.com/Apollon77/ioBroker.nut)
[![Code Climate](https://codeclimate.com/github/Apollon77/ioBroker.nut/badges/gpa.svg)](https://codeclimate.com/github/Apollon77/ioBroker.nut)
**Tests:** Linux/Mac: [![Travis-CI](http://img.shields.io/travis/Apollon77/ioBroker.nut/master.svg)](https://travis-ci.org/Apollon77/ioBroker.nut)
Windows: [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/Apollon77/ioBroker.nut?branch=master&svg=true)](https://ci.appveyor.com/project/Apollon77/ioBroker-nut/)
[![NPM](https://nodei.co/npm/iobroker.nut.png?downloads=true)](https://nodei.co/npm/iobroker.nut/)
This adapter for ioBroker connects to a defined NUT server to provide the status and details of a connected UPS/USV as ioBroker states, so that it can be used there.
## Description of parameters
### host_ip
IP address of the NUT server. NUT needs to run in server mode and needs to be accessible by the computer the
iobroker NUT adapter runs on. So check firewall settings if you have problems and allow the access. If the UPS
is connected locally you can also use 127.0.0.1 or localhost.
### host_port
Port of NUT. The default port is <b>3493</b>
### ups_name
Name of the UPS as defined in the NUT configuration of the NUT server.</p>
Hint: If you want to connect to an UPS connected to a Synology diskstation the name is simply "ups".
### update_interval
Interval in Seconds to update the data. Default is 300s
## UPS-Monitor Notifies
Included is a small linux shell-script at scripts/nut-notify.sh which can be configured in upsmon.
The script needs execute rights (chmod +x nut-notify.sh).
It should be added to /etc/nut/upsmon.conf like:
```
NOTIFYCMD "cd /opt/iobroker/;./nut-notify.sh"
```
Additionally configure all relevant notify messages like:
```
NOTIFYFLAG ONLINE SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC
NOTIFYFLAG FSD SYSLOG+WALL+EXEC
NOTIFYFLAG COMMOK SYSLOG+WALL+EXEC
NOTIFYFLAG COMMBAD SYSLOG+WALL+EXEC
NOTIFYFLAG SHUTDOWN SYSLOG+WALL+EXEC
NOTIFYFLAG REPLBATT SYSLOG+WALL+EXEC
NOTIFYFLAG NOCOMM SYSLOG+WALL+EXEC
NOTIFYFLAG NOPARENT SYSLOG+WALL+EXEC
```
Important is the added "EXEC" flag.
One simple example for a nut-notify.sh script is:
```
#! /bin/sh
# NUT adapter notify script.
logger -t nut-notify "Notify iobroker $UPSNAME -> $NOTIFYTYPE"
/opt/iobroker/iobroker message nut notify "{\"upsname\":\"$UPSNAME\",\"notifytype\":\"$NOTIFYTYPE\"}"
```
## Troubleshooting
If you have problems and the adapter do not deliver the data you can use the two scripts in directory "test"
of the adapter installation (so normally in node_modules/iobroker.nut/test relative to your iobroker installation
directory) to try it out on the commandline. Call the scripts using "node filename.js" to see the awaited parameters.</p>
* **test_upslist.js**: Connects to the NUT server and returns a list of available UPS names
* **test_upsvars.js**: Connects to the NUT server for a defined UPS and returns a list of available UPS variables
## Todo
* docs for webpage
## Changelog
### 1.1.3 (2018-04-13)
* Fix Admin
### 1.1.2 (2018-03-28)
* Fix status parsing
### 1.1.1
* Enhance error handling
### 1.1.0
* Add possibility to call commands on the UPS
### 1.0.0
* change mode from schedule to deamon
* implement message support to receive messages from upsmon
* add status.severity to get one status about the USV with values idle, operating, operating_critical, action_needed, unknown
### 0.3.0
* add better usable status states under "status" channel
### 0.2.1
* finalizied initial version
### 0.1.0
* initial release for testing
## License
The MIT License (MIT)
Copyright (c) 2016-2018 Apollon77 <ingo@fischer-ka.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64
admin/index.html Normal file
View File

@ -0,0 +1,64 @@
<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>
<!-- you have to define 2 functions in the global scope: -->
<script type="text/javascript">
function load(settings, onChange) {
$('#host_ip').val(settings['host_ip']);
$('#host_port').val(settings['host_port']);
$('#ups_name').val(settings['ups_name']);
$('#update_interval').val(settings['update_interval']);
$('#username').val(settings['username']);
$('#password').val(settings['password']);
}
function save(callback) {
callback({host_ip: $('#host_ip').val().trim(), host_port: $('#host_port').val().trim(), ups_name: $('#ups_name').val().trim(), update_interval: $('#update_interval').val().trim(), username: $('#username').val().trim(), password: $('#password').val().trim()});
}
</script>
<!-- you have to put your config page in a div with id adapter-container -->
<div id="adapter-container">
<table><tr>
<td><img src="nut.png"/></td>
<td><h3 class="translate">page_title</h3></td>
</tr></table>
<p style="padding-left: 12px">
<label for="host_ip" class="translate">host_ip</label> <input class="value" type="text" id="host_ip" size="15"/><br/>
<label for="host_port" class="translate">host_port</label> <input class="value" type="text" id="host_port" size="6"/><br/>
<label for="ups_name" class="translate">ups_name</label> <input class="value" type="text" id="ups_name" size="20"/><br/>
<label for="update_interval" class="translate">update_interval</label> <input class="value" type="text" id="update_interval" size="20"/>s<br/>
<label for="username" class="translate">username</label> <input class="value" type="text" id="username" size="20"/><br/>
<label for="password" class="translate">password</label> <input class="value" type="password" id="password" size="20"/><br/>
</p>
<h3 class="translate">host_ip</h3>
<p class="translate">ip_info</p>
<h3 class="translate">host_port</h3>
<p class="translate">port_info</p>
<h3 class="translate">ups_name</h3>
<p class="translate">name_info</p>
<h3 class="translate">update_interval</h3>
<p class="translate">update_interval_info</p>
<h3>Troubleshooting</h3>
<p class="translate">trouble_info</p>
</div>
</html>

BIN
admin/nut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

34
admin/words.js Normal file
View File

@ -0,0 +1,34 @@
systemDictionary = {
"page_title": {"en": "NUT Adapter Settings", "de": "NUT Adapter Einstellungen", "ru": "NUT Adapter Settings"},
"host_ip": {"en": "NUT Server IP", "de": "IP des NUT Servers", "ru": "IP NUT сервера"},
"host_port": {"en": "NUT Server port", "de": "Port des NUT Servers", "ru": "Порт NUT сервера"},
"ups_name": {"en": "NUT Name of the UPS", "de": "NUT Name der UPS", "ru": "NUT имя UPS"},
"username": {"en": "Username of the UPS", "de": "Username der UPS", "ru": "Username of the UPS"},
"password": {"en": "Password of the UPS", "de": "Passwort der UPS", "ru": "Password of the UPS"},
"update_interval": {"en": "Update Interval", "de": "Aktualisierungsintervall", "ru": "Update Interval"},
"ip_info": {
"en":"IP address of the NUT server. NUT needs to run in server mode and needs to be accessible by the computer the iobroker NUT adapter runs on. So check firewall settings if you have problems and allow the access. If the UPS is connected locally you can also use 127.0.0.1 or localhost.",
"de":"IP Adresse des NUT Servers. NUT muss im Server-Mode konfiguriert sein und von dem Rechner auf dem der iobroker Adapter installiert ist erreichbar sein. Bei Problemen sollten die Firewall-Einstellungen geprüft werden, dass der Zugriff gestattet ist. Wenn die USV am lokalen rechner angeschlossen ist kann 127.0.0.1 oder localhost genutzt werden.",
"ru":"IP address of the NUT server. NUT needs to run in server mode and needs to be accessible by the computer the iobroker NUT adapter runs on. So check firewall settings if you have problems and allow the access. If the UPS is connected locally you can also use 127.0.0.1 or localhost."
},
"port_info": {
"en":"Port of NUT. The default port is 3493.",
"de":"Port des NUT Servers. Der Standardport ist 3493.",
"ru":"Port of NUT. The default port is 3493."
},
"name_info": {
"en":"Name of the UPS as defined in the NUT configuration of the NUT server.</p>Hint: If you want to connect to an UPS connected to a Synology diskstation the name is simply 'ups'.",
"de":"Name der USV, wie in den NUT EInstellungen definiert.</p>Hinweis: Für eine USV, die an eine Synology Diskstation angeschlossen ist, lautet der Name 'ups'.",
"ru":"Name of the UPS as defined in the NUT configuration of the NUT server.</p>Hint: If you want to connect to an UPS connected to a Synology diskstation the name is simply 'ups'."
},
"update_interval_info": {
"en":"Interval in Seconds to update the data.",
"de":"Intervall in Sekunden in dem die Daten aktualisiert werden.",
"ru":"Interval in Seconds to update the data."
},
"trouble_info": {
"en":"When you turn the adapter into debug then you can see all created states and their data in the logfile. If you have problems and the adapter do not deliver the data you can use the two scripts in directory 'test' of the adapter installation (so normally in node_modules/iobroker.nut/test relative to your iobroker installation directory) to try it out on the commandline. Call the scripts using 'node filename.js' to see the awaited parameters.</p><ul><li><b>test_upslist.js</b>: Connects to the NUT server and returns a list of available UPS names</li><li><b>test_upsvars.js</b>: Connects to the NUT server for a defined UPS and returns a list of available UPS variables</li></ul>",
"de":"Wenn der Adapter im Debug Modus gestartet wird, werden im Logfile alle erzeugten States und deren Daten aufgelistet. Wenn der Adapter keine Daten liefert können für direklte Tests auch die beiden Skripte im Verzeichnis 'test' der Adapter-Installation (normalerweise unter node_modules/iobroker.nut/test relativ zur iobroker-Installation) an der Kommandozeile aufgerufen werden. Die Skripte können mit 'node filename.js' aufgerufen werden um die benötigten Parameter zu sehen.<ul><li><b>test_upslist.js</b>: Verbindet sich zu einem NUT Server und gibt die Namen der verbundenen USVs aus</li><li><b>test_upsvars.js</b>: Verbindet sich zu einem NUT Server für eine definierte USV und gibt die verfügbaren UPS Varialen aus</li></ul>",
"ru":"When you turn the adapter into debug then you can see all created states and their data in the logfile. If you have problems and the adapter do not deliver the data you can use the two scripts in directory 'test' of the adapter installation (so normally in node_modules/iobroker.nut/test relative to your iobroker installation directory) to try it out on the commandline. Call the scripts using 'node filename.js' to see the awaited parameters.</p><ul><li><b>test_upslist.js</b>: Connects to the NUT server and returns a list of available UPS names</li><li><b>test_upsvars.js</b>: Connects to the NUT server for a defined UPS and returns a list of available UPS variables</li></ul>"
}
};

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'

74
io-package.json Normal file
View File

@ -0,0 +1,74 @@
{
"common": {
"name": "nut",
"version": "1.1.3",
"news": {
"1.1.3": {
"en": "Fix Admin",
"de": "Fixe Admin",
"ru": "Fix Admin",
"pt": "Corrigir Admin",
"nl": "Admin oplossen",
"fr": "Fix Admin",
"it": "Correggi Admin",
"es": "Fix Admin",
"pl": "Napraw administratora"
},
"1.1.2": {
"en": "fix status parsing",
"de": "Statuserkennung verbessert",
"ru": "fix status parsing"
},
"1.1.1": {
"en": "enhance error handling",
"de": "Fehlerbehandlung verbessert",
"ru": "enhance error handling"
},
"1.1.0": {
"en": "Add possibility to call commands on the UPS",
"de": "Möglichkeit hinzugefügt Kommandos auf der USV auszuführen",
"ru": "Add possibility to call commands on the UPS"
},
"1.0.0": {
"en": "change mode from schedule to deamon, implement message support to receive messages from upsmon, add status.severity",
"de": "Adapter arbeitet nun als Deamon, Notify-Support per upsmon hinzugefügt, status.severity hinzugefügt",
"ru": "change mode from schedule to deamon, implement message support to receive messages from upsmon, add status.severity"
},
"0.3.1": {
"en": "add better usable status states under 'status' channel",
"de": "Besser nutzbare Stati States unter 'status' eingefügt",
"ru": "add better usable status states under 'status' channel"
}
},
"authors": [
"Apollon77 <ingo@fischer-ka.de>"
],
"title": "Network UPS Adapter",
"desc": "Read all data from your UPS/USV via nut protocol",
"platform": "Javascript/Node.js",
"mode": "daemon",
"messagebox": true,
"subscribe": "messagebox",
"stopBeforeUpdate": true,
"icon": "nut.png",
"extIcon": "https://raw.githubusercontent.com/Apollon77/ioBroker.nut/master/admin/nut.png",
"readme": "https://github.com/Apollon77/ioBroker.nut/blob/master/README.md",
"license": "MIT",
"npmLibs": [],
"type": "hardware",
"keywords": ["iobroker", "nut", "ups", "usv"],
"loglevel": "info",
"enabled": false
},
"native": {
"host_ip": "127.0.0.1",
"host_port": "3493",
"ups_name": "nutName",
"update_interval": 300,
"username": "",
"password": ""
},
"objects": [],
"instanceObjects": []
}

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;

378
nut.js Normal file
View File

@ -0,0 +1,378 @@
/**
*
* NUT adapter
*
* Adapter loading NUT data from an UPS
*
*/
/* jshint -W097 */
// jshint strict:true
/*jslint node: true */
/*jslint esversion: 6 */
'use strict';
var path = require('path');
var utils = require(path.join(__dirname,'lib','utils')); // Get common adapter utils
var Nut = require('node-nut');
var nutTimeout;
var nutCommands = null;
var adapter = utils.Adapter('nut');
adapter.on('ready', function (obj) {
main();
});
adapter.on('message', function (msg) {
processMessage(msg);
});
adapter.on('stateChange', function (id, state) {
adapter.log.debug('stateChange ' + id + ' ' + JSON.stringify(state));
var realNamespace = adapter.namespace + '.commands.';
var stateId = id.substring(realNamespace.length);
if (!state || state.ack || id.indexOf(realNamespace) !== 0) return;
var command = stateId.replace(/-/g,'.');
initNutConnection(function(oNut) {
if (adapter.config.username && adapter.config.password) {
adapter.log.info('send username for command ' + command);
oNut.SetUsername(adapter.config.username, function (err) {
if (err) {
adapter.log.error('Err while sending username: '+ err);
}
else {
adapter.log.info('send password for command ' + command);
oNut.SetPassword(adapter.config.password, function (err) {
if (err) {
adapter.log.error('Err while sending password: '+ err);
}
else {
adapter.log.info('send command ' + command);
oNut.RunUPSCommand(adapter.config.ups_name, command, function (err) {
if (err) {
adapter.log.error('Err while sending command ' + command + ': '+ err);
}
getCurrentNutValues(oNut, true);
});
}
});
}
});
}
else {
adapter.log.info('send command ' + command + ' without username and password');
oNut.RunUPSCommand(adapter.config.ups_name, command, function (err) {
if (err) {
adapter.log.error('Err while sending command ' + command + ': '+ err);
}
getCurrentNutValues(oNut, true);
});
}
adapter.setState(id, {ack: true, val: false});
});
});
adapter.on('unload', function (callback) {
if (nutTimeout) clearTimeout(nutTimeout);
});
process.on('SIGINT', function () {
if (nutTimeout) clearTimeout(nutTimeout);
});
process.on('uncaughtException', function (err) {
if (adapter && adapter.log) {
adapter.log.warn('Exception: ' + err);
}
if (nutTimeout) clearTimeout(nutTimeout);
});
function main() {
adapter.getForeignObject('system.adapter.' + adapter.namespace, function (err, obj) {
if (!err && obj && (obj.common.mode !== 'daemon')) {
obj.common.mode = 'daemon';
if (obj.common.schedule) delete(obj.common.schedule);
adapter.setForeignObject(obj._id, obj);
}
});
adapter.setObjectNotExists('status.last_notify', {
type: 'state',
common: {
name: 'status.last_notify',
type: 'string',
read: true,
write: false
},
native: {id: 'status.last_notify'}
});
adapter.getState('status.last_notify', function (err, state) {
if (!err && !state) {
adapter.setState('status.last_notify', {ack: true, val: ''});
}
initNutConnection(function(oNut) {
oNut.GetUPSCommands(adapter.config.ups_name, function(cmdlist, err) {
if (err) {
adapter.log.error('Err while getting all commands: '+ err);
}
else {
adapter.log.debug('Got commands, create and subscribe command states');
initNutCommands(cmdlist);
}
getCurrentNutValues(oNut, true);
var update_interval = parseInt(adapter.config.update_interval,10) || 60;
nutTimeout = setTimeout(updateNutData, update_interval*1000);
});
});
});
}
function initNutCommands(cmdlist) {
adapter.log.debug('Create Channel commands');
adapter.setObjectNotExists('commands', {
type: 'channel',
common: {name: 'commands'},
native: {}
});
if (! cmdlist) return;
nutCommands = cmdlist;
for (var i = 0; i < cmdlist.length; i++) {
var cmdName = cmdlist[i].replace(/\./g,'-');
adapter.log.debug('Create State commands.' + cmdName);
adapter.setObjectNotExists('commands.' + cmdName, {
type: 'state',
common: {
name: 'commands.' + cmdName,
role: 'button',
type: 'boolean',
read: true,
write: true,
def: false
},
native: {id: 'commands.' + cmdName}
});
adapter.setState('commands.' + cmdName, {ack: true, val: false});
}
adapter.subscribeStates('commands.*');
}
/*
Command Datapoint to be used with "NOIFY EVENTS" and upsmon
ONLINE : The UPS is back on line.
ONBATT : The UPS is on battery.
LOWBATT : The UPS battery is low (as determined by the driver).
FSD : The UPS has been commanded into the "forced shutdown" mode.
COMMOK : Communication with the UPS has been established.
COMMBAD : Communication with the UPS was just lost.
SHUTDOWN : The local system is being shut down.
REPLBATT : The UPS needs to have its battery replaced.
NOCOMM : The UPS cant be contacted for monitoring.
*/
function processMessage(message) {
if (!message) return;
adapter.log.info('Message received = ' + JSON.stringify(message));
var updateNut = false;
if (message.command === 'notify' && message.message) {
adapter.log.info('got Notify ' + message.message.notifytype + ' for: ' + message.message.upsname);
var ownName = adapter.config.ups_name + '@' + adapter.config.host_ip;
adapter.log.info('ownName=' + ownName + ' --> ' + (ownName === message.message.upsname));
if (ownName === message.message.upsname) {
updateNut = true;
adapter.setState('status.last_notify', {ack: true, val: message.message.notifytype});
if (message.message.notifytype==='COMMBAD' || message.message.notifytype==='NOCOMM') parseAndSetSeverity("OFF");
}
}
else updateNut = true;
if (updateNut) {
if (nutTimeout) clearTimeout(nutTimeout);
updateNutData();
}
}
function initNutConnection(callback) {
var oNut = new Nut(adapter.config.host_port, adapter.config.host_ip);
oNut.on('error', function(err) {
adapter.log.error('Error happend: ' + err);
adapter.getState('status.last_notify', function (err, state) {
if (!err && !state || (state && state.val!=='COMMBAD' && state.val!=='SHUTDOWN' && state.val!=='NOCOMM')) {
adapter.setState('status.last_notify', {ack: true, val: 'ERROR'});
}
if (!err) parseAndSetSeverity("");
});
});
oNut.on('close', function() {
adapter.log.debug('NUT Connection closed. Done.');
});
oNut.on('ready', function() {
adapter.log.debug('NUT Connection ready');
callback(oNut);
});
oNut.start();
}
function updateNutData() {
adapter.log.info('Start NUT update');
initNutConnection(function(oNut) {
getCurrentNutValues(oNut, true);
});
var update_interval = parseInt(adapter.config.update_interval,10) || 60;
nutTimeout = setTimeout(updateNutData, update_interval*1000);
}
function getCurrentNutValues(oNut, closeConnection) {
oNut.GetUPSVars(adapter.config.ups_name, function(varlist, err) {
if (err) {
adapter.log.error('Err while getting NUT values: '+ err);
}
else {
adapter.log.debug('Got values, start setting them');
storeNutData(varlist);
}
if (closeConnection) oNut.close();
});
}
function storeNutData(varlist) {
var last='';
var current='';
var index=0;
var stateName='';
for (var key in varlist) {
if (!varlist.hasOwnProperty(key)) continue;
index=key.indexOf('.');
if (index > 0) {
current=key.substring(0,index);
}
else {
current='';
last='';
index=-1;
}
if (((last==='') || (last!==current)) && (current!=='')) {
adapter.log.debug('Create Channel '+current);
adapter.setObjectNotExists(current, {
type: 'channel',
common: {name: current},
native: {}
});
}
stateName=current+'.'+key.substring(index+1).replace(/\./g,'-');
adapter.log.debug('Create State '+stateName);
if (stateName === 'battery.charge') {
adapter.setObjectNotExists(stateName, {
type: 'state',
common: {name: stateName, type: 'number', role: 'value.battery', read: true, write: false},
native: {id: stateName}
});
}
else {
adapter.setObjectNotExists(stateName, {
type: 'state',
common: {name: stateName, type: 'string', read: true, write: false},
native: {id: stateName}
});
}
adapter.log.debug('Set State '+stateName+' = '+varlist[key]);
adapter.setState(stateName, {ack: true, val: varlist[key]});
last=current;
}
adapter.log.debug('Create Channel status');
adapter.setObjectNotExists('status', {
type: 'channel',
common: {name: 'status'},
native: {}
});
adapter.setObjectNotExists('status.severity', {
type: 'state',
common: {
name: 'status.severity',
role: 'indicator',
type: 'number',
read: true,
write: false,
def:4,
states: '0:idle;1:operating;2:operating_critical;3:action_needed;4:unknown'
},
native: {id: 'status.severity'}
});
if (varlist['ups.status']) {
parseAndSetSeverity(varlist['ups.status']);
}
else parseAndSetSeverity("");
adapter.log.info('All Nut values set');
}
function parseAndSetSeverity(ups_status) {
var statusMap = {
'OL':{name:'online',severity:'idle'},
'OB':{name:'onbattery',severity:'operating'},
'LB':{name:'lowbattery',severity:'operating_critical'},
'HB':{name:'highbattery',severity:'operating_critical'},
'RB':{name:'replacebattery',severity:'action_needed'},
'CHRG':{name:'charging',severity:'idle'},
'DISCHRG':{name:'discharging',severity:'operating'},
'BYPASS':{name:'bypass',severity:'action_needed'},
'CAL':{name:'calibration',severity:'operating'},
'OFF':{name:'offline',severity:'action_needed'},
'OVER':{name:'overload',severity:'action_needed'},
'TRIM':{name:'trimming',severity:'operating'},
'BOOST':{name:'boosting',severity:'operating'},
'FSD':{name:'shutdown',severity:'operating_critical'}
};
var severity = {
'idle':false,
'operating':false,
'operating_critical':false,
'action_needed':false
};
if (ups_status.indexOf('FSD') !== -1) {
ups_status += ' OB LB';
}
var checker=' '+ups_status+' ';
var stateName="";
for (var idx in statusMap) {
if (statusMap.hasOwnProperty(idx)) {
var found=(checker.indexOf(' ' + idx)>-1);
stateName='status.'+statusMap[idx].name;
adapter.log.debug('Create State '+stateName);
adapter.setObjectNotExists(stateName, {
type: 'state',
common: {name: stateName, type: 'boolean', read: true, write: false},
native: {id: stateName}
});
adapter.log.debug('Set State '+stateName+' = '+found);
adapter.setState(stateName, {ack: true, val: found});
if (found) {
severity[statusMap[idx].severity]=true;
adapter.log.debug('Severity Flag '+statusMap[idx].severity+'=true');
}
}
}
var severityVal = 4;
if (severity.operating_critical) severityVal=2;
else if (severity.action_needed) severityVal=3;
else if (severity.operating) severityVal=1;
else if (severity.idle) severityVal=0;
adapter.log.debug('Set State status.severity = '+severityVal);
adapter.setState('status.severity', {ack: true, val: severityVal});
}

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "iobroker.nut",
"version": "1.1.3",
"description": "Network UPS Adapter",
"author": "Ingo Fischer <ingo@fischer-ka.de>",
"contributors": [],
"homepage": "",
"license": "MIT",
"keywords": [
"iobroker",
"nut",
"ups",
"usv"
],
"repository": {
"type": "git",
"url": "https://github.com/Apollon77/ioBroker.nut"
},
"dependencies": {
"node-nut": "^1.0.0"
},
"devDependencies": {
"mocha": "^5.0.0",
"chai": "^4.1.2",
"nyc": "*"
},
"bugs": {
"url": "https://github.com/Apollon77/ioBroker.nut/issues"
},
"main": "nut.js",
"scripts": {
"test": "nyc --reporter=lcov node_modules/mocha/bin/mocha test/testAdapter.js --exit && node node_modules/mocha/bin/mocha test/testPackageFiles.js"
},
"nyc": {
"exclude": ["!**/node_modules/"],
"include": [
"**/tmp/node_modules/iobroker.nut/*.js"
],
"produce-source-map": true
}
}

5
scripts/nut-notify.sh Normal file
View File

@ -0,0 +1,5 @@
#! /bin/sh
# NUT adapter notify script.
logger -t nut-notify "Notify iobroker $UPSNAME -> $NOTIFYTYPE"
/opt/iobroker/iobroker message nut notify "{\"upsname\":\"$UPSNAME\",\"notifytype\":\"$NOTIFYTYPE\"}"

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

195
test/testAdapter.js Normal file
View File

@ -0,0 +1,195 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
/*jshint expr: true*/
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
var objects = null;
var states = null;
var onStateChanged = null;
var onObjectChanged = null;
var sendToID = 1;
var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, function (err, state) {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = function (id, state) {
if (id === 'messagebox.system.adapter.test.0') {
callback(state.message);
}
};
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: (new Date()).getTime()
}
});
}
describe('Test ' + adapterShortName + ' adapter', function() {
before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(function () {
var config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
//config.native.dbtype = 'sqlite';
setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function(id, obj) {}, function (id, state) {
if (onStateChanged) onStateChanged(id, state);
},
function (_objects, _states) {
objects = _objects;
states = _states;
_done();
});
});
});
it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) {
this.timeout(60000);
checkConnectionOfAdapter(function (res) {
if (res) console.log(res);
expect(res).not.to.be.equal('Cannot check connection');
objects.setObject('system.adapter.test.0', {
common: {
},
type: 'instance'
},
function () {
states.subscribeMessage('system.adapter.test.0');
done();
});
});
});
// We expect ERROR as last Notify necause no nut is running there
it('Test ' + adapterShortName + ' adapter: test initial state as ERROR', function (done) {
this.timeout(25000);
setTimeout(function() {
states.getState('nut.0.status.last_notify', function (err, state) {
if (err) console.error(err);
expect(state).to.exist;
if (!state) {
console.error('state "status.last_notify" not set');
}
else {
console.log('check status.last_notify ... ' + state.val);
expect(state.val).to.exist;
expect(state.val).to.be.equal('ERROR');
}
states.getState('nut.0.status.severity', function (err, state) {
if (err) console.error(err);
expect(state).to.exist;
if (!state) {
console.error('state "status.severity" not set');
}
else {
console.log('check status.severity ... ' + state.val);
}
expect(state.val).to.exist;
expect(state.val).to.be.equal(4);
done();
});
});
}, 10000);
});
it('Test ' + adapterShortName + ' adapter: send notify Message and receive answer', function (done) {
this.timeout(25000);
var now = new Date().getTime();
console.log('send notify with "COMMBAD" to adapter ...');
sendTo('nut.0', 'notify', {notifytype: 'COMMBAD', upsname: 'nutName@127.0.0.1'});
setTimeout(function() {
states.getState('nut.0.status.last_notify', function (err, state) {
if (err) console.error(err);
expect(state).to.exist;
if (!state) {
console.error('state "status.last_notify" not set');
}
else {
console.log('check status.last_notify ... ' + state.val);
}
expect(state.val).to.be.equal('COMMBAD');
states.getState('nut.0.status.severity', function (err, state) {
if (err) console.error(err);
expect(state).to.exist;
if (!state) {
console.error('state "status.severity" not set');
}
else {
console.log('check status.severity ... ' + state.val);
}
expect(state.val).to.exist;
expect(state.val).to.be.equal(4);
done();
});
});
}, 2000);
});
after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
done();
});
});
});

91
test/testPackageFiles.js Normal file
View File

@ -0,0 +1,91 @@
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs = require('fs');
describe('Test package.json and io-package.json', function() {
it('Test package files', function (done) {
console.log();
var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
var ioPackage = JSON.parse(fileContentIOPackage);
var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
var npmPackage = JSON.parse(fileContentNPMPackage);
expect(ioPackage).to.be.an('object');
expect(npmPackage).to.be.an('object');
expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;
expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);
if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
console.log();
}
expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;
if (ioPackage.common.name.indexOf('template') !== 0) {
if (Array.isArray(ioPackage.common.authors)) {
expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
if (ioPackage.common.authors.length === 1) {
expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
console.log();
}
expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
console.log('WARNING: titleLang is not existing in io-package.json. Please add');
console.log();
}
if (
ioPackage.common.title.indexOf('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();
});
});

27
test/test_upslist.js Normal file
View File

@ -0,0 +1,27 @@
if (process.argv.length<3) {
console.log('Call: test_upsvars.js <ip> <port> <ups-name>');
process.exit();
}
var Nut = require('node-nut');
//oNut = new Nut(3493, 'localhost');
oNut = new Nut(process.argv[3], process.argv[2]);
oNut.on('error', function(err) {
console.log('There was an error: ' + err);
});
oNut.on('close', function() {
console.log('Connection closed.');
});
oNut.on('ready', function() {
self = this;
this.GetUPSList(function(upslist) {
console.log(upslist);
self.close();
});
});
oNut.start();

27
test/test_upsvars.js Normal file
View File

@ -0,0 +1,27 @@
if (process.argv.length<3) {
console.log('Call: test_upsvars.js <ip> <port> <ups-name>');
process.exit();
}
var Nut = require('node-nut');
//oNut = new Nut(3493, 'localhost');
oNut = new Nut(process.argv[3], process.argv[2]);
oNut.on('error', function(err) {
console.log('There was an error: ' + err);
});
oNut.on('close', function() {
console.log('Connection closed.');
});
oNut.on('ready', function() {
self = this;
this.GetUPSVars(process.argv[4],function(varlist) {
console.log(varlist);
self.close();
});
});
oNut.start();