From e3af2bd74a39fbfe26898207d38a458d299fe03e Mon Sep 17 00:00:00 2001 From: zhongjin Date: Sun, 30 Sep 2018 14:10:06 +0800 Subject: [PATCH] Initial commit --- .codeclimate.yml | 28 ++ .eslintignore | 1 + .eslintrc | 213 ++++++++++++ .npmignore | 15 + .travis.yml | 29 ++ .vscode/launch.json | 29 ++ LICENSE | 21 ++ README.md | 132 +++++++ admin/index.html | 64 ++++ admin/nut.png | Bin 0 -> 6406 bytes admin/words.js | 34 ++ appveyor.yml | 25 ++ io-package.json | 74 ++++ lib/utils.js | 83 +++++ nut.js | 378 ++++++++++++++++++++ package.json | 41 +++ scripts/nut-notify.sh | 5 + test/lib/setup.js | 728 +++++++++++++++++++++++++++++++++++++++ test/testAdapter.js | 195 +++++++++++ test/testPackageFiles.js | 91 +++++ test/test_upslist.js | 27 ++ test/test_upsvars.js | 27 ++ 22 files changed, 2240 insertions(+) create mode 100644 .codeclimate.yml create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 .vscode/launch.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 admin/index.html create mode 100644 admin/nut.png create mode 100644 admin/words.js create mode 100644 appveyor.yml create mode 100644 io-package.json create mode 100644 lib/utils.js create mode 100644 nut.js create mode 100644 package.json create mode 100644 scripts/nut-notify.sh create mode 100644 test/lib/setup.js create mode 100644 test/testAdapter.js create mode 100644 test/testPackageFiles.js create mode 100644 test/test_upslist.js create mode 100644 test/test_upsvars.js diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..18c3a61 --- /dev/null +++ b/.codeclimate.yml @@ -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 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..96212a3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*{.,-}min.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..9faa375 --- /dev/null +++ b/.eslintrc @@ -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 diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..67f6c67 --- /dev/null +++ b/.npmignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aaf486d --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d708057 --- /dev/null +++ b/.vscode/launch.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08a9cff --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4622033 --- /dev/null +++ b/README.md @@ -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 3493 + +### ups_name +Name of the UPS as defined in the NUT configuration of the NUT server.

+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.

+* **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 + +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. diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..317fa9e --- /dev/null +++ b/admin/index.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + +
+ + + + +

page_title

+

+
+
+
+ s
+
+
+

+

host_ip

+

ip_info

+ +

host_port

+

port_info

+ +

ups_name

+

name_info

+ +

update_interval

+

update_interval_info

+ +

Troubleshooting

+

trouble_info

+ +
+ + diff --git a/admin/nut.png b/admin/nut.png new file mode 100644 index 0000000000000000000000000000000000000000..4c36debd9444eddf8ad0fc0e44c623b5ac1561d9 GIT binary patch literal 6406 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: 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.

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.

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.

  • 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
", + "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.
  • test_upslist.js: Verbindet sich zu einem NUT Server und gibt die Namen der verbundenen USVs aus
  • test_upsvars.js: Verbindet sich zu einem NUT Server für eine definierte USV und gibt die verfügbaren UPS Varialen aus
", + "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.

  • 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
" + } +}; diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..84ca0a9 --- /dev/null +++ b/appveyor.yml @@ -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' diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..03a8669 --- /dev/null +++ b/io-package.json @@ -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 " + ], + "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": [] +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..c8a0eb7 --- /dev/null +++ b/lib/utils.js @@ -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; diff --git a/nut.js b/nut.js new file mode 100644 index 0000000..9785671 --- /dev/null +++ b/nut.js @@ -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 can’t 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}); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1faa9a2 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "iobroker.nut", + "version": "1.1.3", + "description": "Network UPS Adapter", + "author": "Ingo Fischer ", + "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 + } +} diff --git a/scripts/nut-notify.sh b/scripts/nut-notify.sh new file mode 100644 index 0000000..5359347 --- /dev/null +++ b/scripts/nut-notify.sh @@ -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\"}" diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..16857ed --- /dev/null +++ b/test/lib/setup.js @@ -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; +} diff --git a/test/testAdapter.js b/test/testAdapter.js new file mode 100644 index 0000000..8d7cba6 --- /dev/null +++ b/test/testAdapter.js @@ -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(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..c600a60 --- /dev/null +++ b/test/testPackageFiles.js @@ -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 '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + 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(); + }); +}); diff --git a/test/test_upslist.js b/test/test_upslist.js new file mode 100644 index 0000000..d5f3f8e --- /dev/null +++ b/test/test_upslist.js @@ -0,0 +1,27 @@ +if (process.argv.length<3) { + console.log('Call: test_upsvars.js '); + 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(); diff --git a/test/test_upsvars.js b/test/test_upsvars.js new file mode 100644 index 0000000..2bd9def --- /dev/null +++ b/test/test_upsvars.js @@ -0,0 +1,27 @@ +if (process.argv.length<3) { + console.log('Call: test_upsvars.js '); + 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();