From 0cc08f136db8dc12d5daa1ad3f4f74d83b058ca2 Mon Sep 17 00:00:00 2001 From: zhongjin Date: Wed, 16 Jan 2019 09:07:10 +0800 Subject: [PATCH] Initial commit --- .gitignore | 7 + .npmignore | 12 + .travis.yml | 24 + LICENSE | 22 + README.md | 194 ++++ admin/i18n/de/translations.json | 49 + admin/i18n/en/translations.json | 49 + admin/i18n/es/translations.json | 49 + admin/i18n/fr/translations.json | 49 + admin/i18n/it/translations.json | 49 + admin/i18n/nl/translations.json | 49 + admin/i18n/pl/translations.json | 49 + admin/i18n/pt/translations.json | 49 + admin/i18n/ru/translations.json | 49 + admin/index.html | 177 ++++ admin/index_m.html | 183 ++++ admin/sonoff.png | Bin 0 -> 1782 bytes admin/words.js | 53 + appveyor.yml | 26 + gulpfile.js | 401 ++++++++ io-package.json | 184 ++++ lib/datapoints.js | 139 +++ lib/server.js | 1643 +++++++++++++++++++++++++++++++ lib/utils.js | 83 ++ main.js | 99 ++ package.json | 46 + test/lib/mqttClient.js | 94 ++ test/lib/objects.js | 1411 ++++++++++++++++++++++++++ test/lib/setup.js | 718 ++++++++++++++ test/lib/states.js | 708 +++++++++++++ test/testPackageFiles.js | 92 ++ test/testServer.js | 264 +++++ 32 files changed, 7021 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100755 README.md create mode 100644 admin/i18n/de/translations.json create mode 100644 admin/i18n/en/translations.json create mode 100644 admin/i18n/es/translations.json create mode 100644 admin/i18n/fr/translations.json create mode 100644 admin/i18n/it/translations.json create mode 100644 admin/i18n/nl/translations.json create mode 100644 admin/i18n/pl/translations.json create mode 100644 admin/i18n/pt/translations.json create mode 100644 admin/i18n/ru/translations.json create mode 100644 admin/index.html create mode 100644 admin/index_m.html create mode 100644 admin/sonoff.png create mode 100644 admin/words.js create mode 100644 appveyor.yml create mode 100644 gulpfile.js create mode 100755 io-package.json create mode 100644 lib/datapoints.js create mode 100644 lib/server.js create mode 100644 lib/utils.js create mode 100644 main.js create mode 100755 package.json create mode 100644 test/lib/mqttClient.js create mode 100644 test/lib/objects.js create mode 100644 test/lib/setup.js create mode 100644 test/lib/states.js create mode 100644 test/testPackageFiles.js create mode 100644 test/testServer.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac2a676 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.idea +tmp +admin/i18n/*/flat.txt +admin/i18n/flat.txt +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..06b4fd6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,12 @@ +gulpfile.js +tasks +test +tmp +docs +.idea +.travis.yml +.git +appveyor.yml +admin/i18n +iob_npm.done +package-lock.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0066549 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' + - '10' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install winston@2.3.1 + - npm install mqtt@2.13.0 + - 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e981671 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2019 bluefox + +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 100755 index 0000000..575b75b --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +![Logo](admin/sonoff.png) +# ioBroker Sonoff +============== + +[![NPM version](http://img.shields.io/npm/v/iobroker.sonoff.svg)](https://www.npmjs.com/package/iobroker.sonoff) +[![Downloads](https://img.shields.io/npm/dm/iobroker.sonoff.svg)](https://www.npmjs.com/package/iobroker.sonoff) +[![Tests](https://travis-ci.org/ioBroker/ioBroker.sonoff.svg?branch=master)](https://travis-ci.org/ioBroker/ioBroker.sonoff) + +[![NPM](https://nodei.co/npm/iobroker.sonoff.png?downloads=true)](https://nodei.co/npm/iobroker.sonoff/) + +Requires node.js 4.0 or higher. + +## Usage + +This adapter communicates with Sonoff devices with Tasmota firmware or ESP devices via MQTT. + +Following topics are expected: +- tele/DeviceNAME/STATE +- tele/DeviceNAME/SENSOR +- tele/DeviceNAME/INFOx +- tele/DeviceNAME/ENERGY +- cmnd/DeviceNAME/POWERx +- stat/DeviceNAME/POWERx +- /DeviceNAME/BM280/Temperature +- /DeviceNAME/BM280/Humidity +- /DeviceNAME/BM280/Temperatur +- /DeviceNAME/BM280/Feuchtigkeit +- /DeviceNAME/BM280/Vcc +- /DeviceNAME/BM280/VCC +- /DeviceNAME/BM280/Laufzeit +- /DeviceNAME/BM280/RSSI +- /DeviceNAME/BM280/POWER +- /DeviceNAME/BM280/POWER1 +- /DeviceNAME/BM280/POWER2 +- /DeviceNAME/BM280/POWER3 +- /DeviceNAME/BM280/POWER4 +- /DeviceNAME/BM280/Switch1 +- /DeviceNAME/BM280/Switch2 +- /DeviceNAME/BM280/Total +- /DeviceNAME/BM280/Today +- /DeviceNAME/BM280/heute +- /DeviceNAME/BM280/Yesterday +- /DeviceNAME/BM280/gestern +- /DeviceNAME/BM280/Faktor +- /DeviceNAME/BM280/Factor +- /DeviceNAME/BM280/Power +- /DeviceNAME/BM280/Leistung +- /DeviceNAME/BM280/Voltage +- /DeviceNAME/BM280/Spannung +- /DeviceNAME/BM280/Current +- /DeviceNAME/BM280/Strom +- /DeviceNAME/BM280/Punkt +- /DeviceNAME/BM280/Counter1 +- /DeviceNAME/BM280/Counter2 +- /DeviceNAME/BM280/Counter3 +- /DeviceNAME/BM280/Counter4 +- /DeviceNAME/BM280/Pressure +- /DeviceNAME/BM280/SeaPressure +- /DeviceNAME/BM280/Druck +- /DeviceNAME/BM280/Approx. Altitude +- /DeviceNAME/BM280/Module +- /DeviceNAME/BM280/Version +- /DeviceNAME/BM280/Hostname +- /DeviceNAME/BM280/IPAddress +- /DeviceNAME/BM280/IPaddress +- /DeviceNAME/BM280/RestartReason +- /DeviceNAME/BM280/CarbonDioxide +- /DeviceNAME/DHT11/Illuminance +- /DeviceNAME/SonoffSC/Light +- /DeviceNAME/SonoffSC/Noise +- /DeviceNAME/SonoffSC/AirQuality +- /DeviceNAME/SDS0X1/PM2.5 +- /DeviceNAME/SDS0X1/PM10 +- /DeviceNAME/SDS0X1/UvLevel +- /DeviceNAME/SDS0X1/Latitude +- /DeviceNAME/SDS0X1/Longitude +- /DeviceNAME/SR04/Distance + +**Note**: The list could be easily extended. Please send *Pull Requests* or *debug data* for unknown states to developer (via issue). + +## Auto-creation of objects +In the web config you can determine which MQTT telegrams create the new objects not in default datapoints + +* TELE_SENSOR creates objects from tele/xxx/SENSOR telegrams +* TELE_STATE creates objects from tele/xxx/STATE telegrams +* STAT_RESULT creates objects from stat/xxx/RESULT telegrams + +Usually TELE_SENSOR should be sufficient for most users. + +## Flags for LED controllers +The mode states will be created only if device has one of the states: + +- 'Red', 'Green', 'Blue', 'WW', 'CW', 'Color', 'RGB_POWER', 'WW_POWER', 'CW_POWER', 'Hue', 'Saturation' + +States: + +* modeLedExor - exor for white LEDs and color leds => if the white LEDs are switched on, color LEDs are switched off and vice versa (default true) +* modeReadColors - allow for color read from MQTT (default false) + +## Changelog + +### 2.2.3 (2019-01-10) +* (simatec) Support for comapct mode + +### 2.2.2 (2018-06-22) +* (bluefox) Configuration was fixed + +### 2.2.1 (2018-06-20) +* (bluefox) '-' in names was allowed again + +### 2.2.0 (2018-05-22) +* (gemu2015) auto generate objects, support for arrays (channel), ledcontrollers improved + +### 2.1.3 (2018-05-08) +* (bluefox) Added HC-SR04 Ultrasonic Sensor + +### 2.1.2 (2018-04-23) +* (bluefox) Added support of UvLight, Longitude and Latitude + +### 2.1.1 (2018-04-13) +* (bluefox) Support of the particle concentration sensor + +### 2.1.0 (2018-03-30) +* (gemu2015) Support of the devices control (many thanks :) +* (gemu2015) Support of many new values +* (modmax) Update alive status of the clients +* (modmax) Added POWER5-8 and Switch3-4 + +### 2.0.2 (2018-03-19) +* (modmax) Fixing reconnection of clients +* (bluefox) Add SeaPressure + +### 2.0.1 (2018-03-17) +* (bluefox) Replace stream handler +* (bluefox) Add timeout for clients +* (bluefox) Add Light/Noise/AirQuality +* (bluefox) Do not send pingresp for invalid clients + +### 1.0.3 (2018-03-03) +* (bluefox) Add Analog0/1/2/3 sensor + +### 1.0.2 (2018-02-17) +* (Apollon77) Add Illuminance sensor + +### 1.0.1 (2018-02-05) +* (bluefox) Ready for admin3 +* (bluefox) Added CO2 sensor + +### 1.0.0 (2017-11-27) +* (AlZiBa) typo @ alive +* (AlZiBa) add Todays power consumption for Sonoff POW +* (AlZiBa) unit of power consumption is kWh + +### 0.3.3 (2017-11-03) +* (bluefox) Add counters + +### 0.3.2 (2017-10-22) +* (Tan-DE) Small change for Switch1. Switch2 and additional IPaddress added. + +### 0.3.1 (2017-10-12) +* (bluefox) Fix tests and LWT + +### 0.3.0 (2017-10-06) +* (bluefox) Add INFO and ESP + +### 0.2.0 (2017-10-05) +* (bluefox) Add ENERGY and DS18x20 + +### 0.1.0 (2017-10-01) +* (bluefox) initial commit + +## License + +The MIT License (MIT) + +Copyright (c) 2017-2019, bluefox + +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/i18n/de/translations.json b/admin/i18n/de/translations.json new file mode 100644 index 0000000..591d21e --- /dev/null +++ b/admin/i18n/de/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Sonoff Adapter-Einstellungen", + "Type:": "Typ", + "Port:": "Port", + "User:": "Username", + "Password:": "Kennwort", + "Password confirmation:": "Kennwort-Wiederholung", + "URL:": "URL", + "Secure:": "SSL", + "Public certificate:": "Publikzertifikat", + "Private certificate:": "Privatzertifikat", + "Chained certificate:": "Kettenzertifikat", + "Patterns:": "Subscribe patterns", + "Use WebSockets:": "Benutze auch WebSockets", + "Connection": "Verbindung", + "Sonoff Settings": "Sonoff Einstellungen", + "Client ID:": "Client ID", + "chars": "Symbolen", + "ms": "ms", + "Interval before send topics by connection:": "Interval vom Senden von allen Topics bei der verbindung", + "Send interval:": "Sendeintervall", + "Use chunk patch:": "Benutze Patch für Chunking", + "Divided by comma": "Mit Komma getrennt, z.B 'mqtt/0/#,javascript/#'", + "Mask to publish": "z.B 'mqtt.0.*,javascript.*'", + "Store only on change:": "Publish nur bei Änderung", + "Trace output for every message:": "Trace Ausgabe für jede Meldung", + "Test connection": "Teste Verbindung zum Server", + "Result: ": "Ergebnis ", + "connected": "verbunden", + "Main settings": "Allgemeine Einstellungen", + "Connection settings": "Verbindungseinstellungen", + "Authentication settings": "Authentication Einstellungen", + "Adapter settings": "Sonoff Einstellungen", + "Mask to publish own states:": "Maske für Bekanntgeben von eigenen States", + "Send states (ack=true) too:": "Sende auch Zustände (ack=true)", + "Publish all states at start:": "Bekanntgeben eigene States beim Verbinden", + "Use different topic names for set and get:": "Unterschiedliche Namen für setzten und lesen", + "Publish states on subscribe:": "Bekanntgeben von States bei Subscribe", + "Set certificates or load it first in the system settings (right top).": "Setze Zertificate oder lade die erst unter System/Einstellungen (oben rechts).", + "Prefix for topics:": "Prefix für alle Topics", + "Max topic length:": "Maximale Topicnamelänge", + "Server": "Server/broker", + "Client": "Client/subscriber", + "For tele/SENSOR": "Für tele/SENSOR", + "For tele/STATE": "Für tele/STATE", + "For stat/RESULT": "Für stat/RESULT", + "Client timeout (sec):": "Client-Zeitüberschreitung (Sek.)", + "Auto creation of states": "Automatische Erstellung von Zuständen" +} \ No newline at end of file diff --git a/admin/i18n/en/translations.json b/admin/i18n/en/translations.json new file mode 100644 index 0000000..080259b --- /dev/null +++ b/admin/i18n/en/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Sonoff adapter settings", + "Type:": "Type", + "Port:": "Port", + "User:": "User", + "Password:": "Password", + "Password confirmation:": "Password confirmation", + "URL:": "URL", + "Secure:": "Secure", + "Public certificate:": "Public certificate", + "Private certificate:": "Private certificate", + "Chained certificate:": "Chained certificate", + "Patterns:": "Subscribe patterns", + "Use WebSockets:": "Use WebSockets too", + "Connection": "Connection", + "Sonoff Settings": "Sonoff Settings", + "Client ID:": "Client ID", + "chars": "chars", + "ms": "ms", + "Interval before send topics by connection:": "Interval before send topics by connection", + "Send interval:": "Send interval", + "Use chunk patch:": "Use chunk patch", + "Divided by comma": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'", + "Mask to publish": "e.g. 'mqtt.0.*,javascript.*'", + "Store only on change:": "Publish only on change", + "Trace output for every message:": "Trace output for every message", + "Test connection": "Test connection with Server", + "Result: ": "Result ", + "connected": "connected", + "Main settings": "Main settings", + "Connection settings": "Connection settings", + "Authentication settings": "Authentication settings", + "Adapter settings": "Sonoff settings", + "Mask to publish own states:": "Mask to publish own states", + "Send states (ack=true) too:": "Send states (ack=true) too", + "Publish all states at start:": "Publish own states on connect", + "Use different topic names for set and get:": "Use different topic names for set and get", + "Publish states on subscribe:": "Publish states on subscribe", + "Set certificates or load it first in the system settings (right top).": "Set certificates or load it first in the system settings (right top).", + "Prefix for topics:": "Prefix for all topics", + "Max topic length:": "Max topic name length", + "Server": "Server/broker", + "Client": "Client/subscriber", + "For tele/SENSOR": "For tele/SENSOR", + "For tele/STATE": "For tele/STATE", + "For stat/RESULT": "For stat/RESULT", + "Client timeout (sec):": "Client timeout (sec)", + "Auto creation of states": "Auto creation of states" +} \ No newline at end of file diff --git a/admin/i18n/es/translations.json b/admin/i18n/es/translations.json new file mode 100644 index 0000000..9915bd6 --- /dev/null +++ b/admin/i18n/es/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Configuraciones del adaptador Sonoff", + "Type:": "Tipo", + "Port:": "Puerto", + "User:": "Usuario", + "Password:": "Contraseña", + "Password confirmation:": "confirmación de contraseña", + "URL:": "URL", + "Secure:": "Seguro", + "Public certificate:": "Certificado público", + "Private certificate:": "Certificado privado", + "Chained certificate:": "Certificado encadenado", + "Patterns:": "Suscribir patrones", + "Use WebSockets:": "Use WebSockets también", + "Connection": "Conexión", + "Sonoff Settings": "Configuraciones Sonoff", + "Client ID:": "Identificación del cliente", + "chars": "caracteres", + "ms": "Sra", + "Interval before send topics by connection:": "Intervalo antes de enviar temas por conexión", + "Send interval:": "Intervalo de envío", + "Use chunk patch:": "Use el fragmento de fragmento", + "Divided by comma": "Dividido por coma, p. 'mqtt/0/#, javascript/#'", + "Mask to publish": "p.ej. 'mqtt.0.*, javascript.*'", + "Store only on change:": "Publicar solo en el cambio", + "Trace output for every message:": "Salida de seguimiento para cada mensaje", + "Test connection": "Prueba de conexión con el servidor", + "Result: ": "Resultado", + "connected": "conectado", + "Main settings": "Ajustes principales", + "Connection settings": "Configuración de conexión", + "Authentication settings": "Configuraciones de autenticación", + "Adapter settings": "Configuraciones Sonoff", + "Mask to publish own states:": "Máscara para publicar estados propios", + "Send states (ack=true) too:": "Enviar estados (ack = true) también", + "Publish all states at start:": "Publicar estados propios al conectar", + "Use different topic names for set and get:": "Use diferentes nombres de tema para establecer y obtener", + "Publish states on subscribe:": "Publicar estados al suscribirse", + "Set certificates or load it first in the system settings (right top).": "Establezca certificados o cárguelos primero en la configuración del sistema (arriba a la derecha).", + "Prefix for topics:": "Prefijo para todos los temas", + "Max topic length:": "Longitud máxima del nombre del tema", + "Server": "Servidor/intermediario", + "Client": "Cliente/suscriptor", + "For tele/SENSOR": "Por tele/SENSOR", + "For tele/STATE": "Por tele/STATE", + "For stat/RESULT": "Por stat/RESULT", + "Client timeout (sec):": "Tiempo de espera del cliente (seg)", + "Auto creation of states": "Creación automática de estados" +} \ No newline at end of file diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr/translations.json new file mode 100644 index 0000000..680983a --- /dev/null +++ b/admin/i18n/fr/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Paramètres de l'adaptateur Sonoff", + "Type:": "Type", + "Port:": "Port", + "User:": "Utilisateur", + "Password:": "Mot de passe", + "Password confirmation:": "Confirmation mot de passe", + "URL:": "URL", + "Secure:": "Garantir", + "Public certificate:": "Certificat public", + "Private certificate:": "Certificat privé", + "Chained certificate:": "Certificat chaîné", + "Patterns:": "Modèles de souscription", + "Use WebSockets:": "Utilisez aussi WebSockets", + "Connection": "Connexion", + "Sonoff Settings": "Paramètres Sonoff", + "Client ID:": "identité du client", + "chars": "chars", + "ms": "Mme", + "Interval before send topics by connection:": "Intervalle avant d'envoyer des sujets par connexion", + "Send interval:": "Envoyer un intervalle", + "Use chunk patch:": "Utiliser un patch de morceau", + "Divided by comma": "Divisé par une virgule, par ex. 'mqtt/0/#, javascript/#'", + "Mask to publish": "par exemple. 'mqtt.0.*, javascript.*'", + "Store only on change:": "Publier uniquement en cas de modification", + "Trace output for every message:": "Trace de sortie pour chaque message", + "Test connection": "Tester la connexion avec le serveur", + "Result: ": "Résultat", + "connected": "connecté", + "Main settings": "Réglages principaux", + "Connection settings": "Paramètres de connexion", + "Authentication settings": "Paramètres d'authentification", + "Adapter settings": "Paramètres Sonoff", + "Mask to publish own states:": "Masque pour publier ses propres états", + "Send states (ack=true) too:": "Envoyer des états (ack = true) aussi", + "Publish all states at start:": "Publier ses propres états sur se connecter", + "Use different topic names for set and get:": "Utilisez différents noms de sujet pour définir et obtenir", + "Publish states on subscribe:": "Publier les états sur l'abonnement", + "Set certificates or load it first in the system settings (right top).": "Définissez des certificats ou chargez-les d'abord dans les paramètres du système (en haut à droite).", + "Prefix for topics:": "Préfixe pour tous les sujets", + "Max topic length:": "Longueur maximale du nom de la rubrique", + "Server": "Serveur/courtier", + "Client": "Client/abonné", + "For tele/SENSOR": "Pour tele/SENSOR", + "For tele/STATE": "Pour tele/STATE", + "For stat/RESULT": "Pour stat/RESULT", + "Client timeout (sec):": "Délai d'expiration du client (sec)", + "Auto creation of states": "Création automatique d'états" +} \ No newline at end of file diff --git a/admin/i18n/it/translations.json b/admin/i18n/it/translations.json new file mode 100644 index 0000000..5c0461a --- /dev/null +++ b/admin/i18n/it/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Impostazioni dell'adattatore Sonoff", + "Type:": "genere", + "Port:": "Porta", + "User:": "Utente", + "Password:": "Parola d'ordine", + "Password confirmation:": "conferma password", + "URL:": "URL", + "Secure:": "Sicuro", + "Public certificate:": "Certificato pubblico", + "Private certificate:": "Certificato privato", + "Chained certificate:": "Certificato incatenato", + "Patterns:": "Iscriviti modelli", + "Use WebSockets:": "Utilizza anche WebSockets", + "Connection": "Connessione", + "Sonoff Settings": "Impostazioni Sonoff", + "Client ID:": "Identificativo cliente", + "chars": "caratteri", + "ms": "Signorina", + "Interval before send topics by connection:": "Intervallo prima di inviare argomenti per connessione", + "Send interval:": "Invia intervallo", + "Use chunk patch:": "Usa patch pezzo", + "Divided by comma": "Diviso in virgola, ad es. 'MQTT/0/#, JavaScript/#'", + "Mask to publish": "per esempio. 'Mqtt.0. *, Javascript. *'", + "Store only on change:": "Pubblica solo sul cambiamento", + "Trace output for every message:": "Traccia l'output per ogni messaggio", + "Test connection": "Verifica la connessione con il server", + "Result: ": "Risultato", + "connected": "collegato", + "Main settings": "Impostazioni principali", + "Connection settings": "Impostazioni di connessione", + "Authentication settings": "Impostazioni di autenticazione", + "Adapter settings": "Impostazioni Sonoff", + "Mask to publish own states:": "Maschera per pubblicare i propri stati", + "Send states (ack=true) too:": "Invia stati (ack = true) anche", + "Publish all states at start:": "Pubblica i tuoi stati sulla connessione", + "Use different topic names for set and get:": "Usa diversi nomi di argomenti per impostare e ottenere", + "Publish states on subscribe:": "Pubblica stati su iscriviti", + "Set certificates or load it first in the system settings (right top).": "Imposta i certificati o caricali prima nelle impostazioni di sistema (in alto a destra).", + "Prefix for topics:": "Prefisso per tutti gli argomenti", + "Max topic length:": "Lunghezza massima del nome dell'argomento", + "Server": "Server/mediatore", + "Client": "Client/abbonato", + "For tele/SENSOR": "Per tele/SENSOR", + "For tele/STATE": "Per tele/STATE", + "For stat/RESULT": "Per stat/RESULT", + "Client timeout (sec):": "Timeout del client (sec)", + "Auto creation of states": "Creazione automatica di stati" +} \ No newline at end of file diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl/translations.json new file mode 100644 index 0000000..bdc5c6c --- /dev/null +++ b/admin/i18n/nl/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Configurações do adaptador Sonoff", + "Type:": "Tipo", + "Port:": "Porta", + "User:": "Do utilizador", + "Password:": "Senha", + "Password confirmation:": "ConfirmaÇão Da Senha", + "URL:": "URL", + "Secure:": "Seguro", + "Public certificate:": "Certificado público", + "Private certificate:": "Certificado privado", + "Chained certificate:": "Certificado acorrentado", + "Patterns:": "Assinar padrões", + "Use WebSockets:": "Use também WebSockets", + "Connection": "Conexão", + "Sonoff Settings": "Configurações de Sonoff", + "Client ID:": "ID do Cliente", + "chars": "caracteres", + "ms": "Senhora", + "Interval before send topics by connection:": "Intervalo antes de enviar tópicos por conexão", + "Send interval:": "Intervalo de envio", + "Use chunk patch:": "Use patch de pedaços", + "Divided by comma": "Dividido por vírgula, e. 'mqtt/0/#, javascript/#'", + "Mask to publish": "por exemplo. 'mqtt.0.*, javascript.*'", + "Store only on change:": "Publique apenas na mudança", + "Trace output for every message:": "Saída de rastreamento para cada mensagem", + "Test connection": "Teste a conexão com o Servidor", + "Result: ": "Resultado", + "connected": "conectado", + "Main settings": "Configurações principais", + "Connection settings": "Configurações de conexão", + "Authentication settings": "Configurações de autenticação", + "Adapter settings": "Configurações de Sonoff", + "Mask to publish own states:": "Máscara para publicar estados próprios", + "Send states (ack=true) too:": "Enviar estados (ack = true) também", + "Publish all states at start:": "Publique seus próprios estados na conexão", + "Use different topic names for set and get:": "Use nomes de tópicos diferentes para definir e obter", + "Publish states on subscribe:": "Publique estados na assinatura", + "Set certificates or load it first in the system settings (right top).": "Defina certificados ou carregue primeiro nas configurações do sistema (parte superior direita).", + "Prefix for topics:": "Prefix para todos os tópicos", + "Max topic length:": "Nome máximo do nome do tópico", + "Server": "Servidor/corretor", + "Client": "Cliente/assinante", + "For tele/SENSOR": "Voor tele/SENSOR", + "For tele/STATE": "Voor tele/STATE", + "For stat/RESULT": "Voor stat/RESULT", + "Client timeout (sec):": "Clienttimeout (sec)", + "Auto creation of states": "Automatisch creëren van staten" +} \ No newline at end of file diff --git a/admin/i18n/pl/translations.json b/admin/i18n/pl/translations.json new file mode 100644 index 0000000..08fc090 --- /dev/null +++ b/admin/i18n/pl/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Ustawienia adaptera Sonoff", + "Type:": "Rodzaj", + "Port:": "Port", + "User:": "Użytkownik", + "Password:": "Hasło", + "Password confirmation:": "Potwierdzenie hasła", + "URL:": "URL", + "Secure:": "Bezpieczne", + "Public certificate:": "Certyfikat publiczny", + "Private certificate:": "Prywatny certyfikat", + "Chained certificate:": "Przykuty certyfikat", + "Patterns:": "Wzorce subskrypcji", + "Use WebSockets:": "Użyj także WebSockets", + "Connection": "Połączenie", + "Sonoff Settings": "Ustawienia Sonoff", + "Client ID:": "Identyfikator klienta", + "chars": "znaki", + "ms": "ms", + "Interval before send topics by connection:": "Interwał przed wysłaniem tematów według połączenia", + "Send interval:": "Interwał wysyłania", + "Use chunk patch:": "Użyj poprawki kawałka", + "Divided by comma": "Podzielone przecinkiem, np. \"mqtt / 0 / #, javascript / # '", + "Mask to publish": "na przykład \"mqtt.0. *, javascript. *\"", + "Store only on change:": "Opublikuj tylko w przypadku zmiany", + "Trace output for every message:": "Dane wyjściowe śledzenia dla każdej wiadomości", + "Test connection": "Sprawdź połączenie z serwerem", + "Result: ": "Wynik", + "connected": "połączony", + "Main settings": "Ustawienia główne", + "Connection settings": "Ustawienia połączenia", + "Authentication settings": "Ustawienia uwierzytelniania", + "Adapter settings": "Ustawienia Sonoff", + "Mask to publish own states:": "Maskuj, aby publikować własne stany", + "Send states (ack=true) too:": "Wyślij także stany (ack = true)", + "Publish all states at start:": "Opublikuj własne stany przy połączeniu", + "Use different topic names for set and get:": "Używaj różnych nazw tematów do ustawiania i pobierania", + "Publish states on subscribe:": "Opublikuj stany w subskrypcji", + "Set certificates or load it first in the system settings (right top).": "Ustaw certyfikaty lub załaduj najpierw w ustawieniach systemu (prawy górny).", + "Prefix for topics:": "Prefiks dla wszystkich tematów", + "Max topic length:": "Maksymalna długość nazwy tematu", + "Server": "Serwer / broker", + "Client": "Klient / subskrybent", + "For tele/SENSOR": "Dla tele/SENSOR", + "For tele/STATE": "Dla tele/STATE", + "For stat/RESULT": "Dla stat/RESULT", + "Client timeout (sec):": "Limit czasu klienta (s)", + "Auto creation of states": "Automatyczne tworzenie stanów" +} \ No newline at end of file diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt/translations.json new file mode 100644 index 0000000..432868b --- /dev/null +++ b/admin/i18n/pt/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Configurações do adaptador Sonoff", + "Type:": "Tipo", + "Port:": "Porta", + "User:": "Do utilizador", + "Password:": "Senha", + "Password confirmation:": "ConfirmaÇão Da Senha", + "URL:": "URL", + "Secure:": "Seguro", + "Public certificate:": "Certificado público", + "Private certificate:": "Certificado privado", + "Chained certificate:": "Certificado acorrentado", + "Patterns:": "Assinar padrões", + "Use WebSockets:": "Use também WebSockets", + "Connection": "Conexão", + "Sonoff Settings": "Configurações de Sonoff", + "Client ID:": "ID do Cliente", + "chars": "caracteres", + "ms": "Senhora", + "Interval before send topics by connection:": "Intervalo antes de enviar tópicos por conexão", + "Send interval:": "Intervalo de envio", + "Use chunk patch:": "Use patch de pedaços", + "Divided by comma": "Dividido por vírgula, e. 'mqtt / 0 / #, javascript / #'", + "Mask to publish": "por exemplo. 'mqtt.0. *, javascript. *'", + "Store only on change:": "Publique apenas na mudança", + "Trace output for every message:": "Saída de rastreamento para cada mensagem", + "Test connection": "Teste a conexão com o Servidor", + "Result: ": "Resultado", + "connected": "conectado", + "Main settings": "Configurações principais", + "Connection settings": "Configurações de conexão", + "Authentication settings": "Configurações de autenticação", + "Adapter settings": "Configurações de Sonoff", + "Mask to publish own states:": "Máscara para publicar estados próprios", + "Send states (ack=true) too:": "Enviar estados (ack = true) também", + "Publish all states at start:": "Publique seus próprios estados na conexão", + "Use different topic names for set and get:": "Use nomes de tópicos diferentes para definir e obter", + "Publish states on subscribe:": "Publique estados na assinatura", + "Set certificates or load it first in the system settings (right top).": "Defina certificados ou carregue primeiro nas configurações do sistema (parte superior direita).", + "Prefix for topics:": "Prefix para todos os tópicos", + "Max topic length:": "Nome máximo do nome do tópico", + "Server": "Servidor / corretor", + "Client": "Cliente / assinante", + "For tele/SENSOR": "Para tele/SENSOR", + "For tele/STATE": "Para tele/STATE", + "For stat/RESULT": "Para stat/RESULT", + "Client timeout (sec):": "Tempo limite do cliente (seg)", + "Auto creation of states": "Criação automática de estados" +} \ No newline at end of file diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru/translations.json new file mode 100644 index 0000000..382c02a --- /dev/null +++ b/admin/i18n/ru/translations.json @@ -0,0 +1,49 @@ +{ + "Sonoff adapter settings": "Настройки драйвера Sonoff", + "Type:": "Тип", + "Port:": "Порт", + "User:": "Имя пользователя", + "Password:": "Пароль", + "Password confirmation:": "Подтверждение пароля", + "URL:": "URL", + "Secure:": "SSL", + "Public certificate:": "'Public' сертификат", + "Private certificate:": "'Private' сертификат", + "Chained certificate:": "'Chained' сертификат", + "Patterns:": "Patterns", + "Use WebSockets:": "Сервер WebSockets тоже", + "Connection": "Соединение", + "Sonoff Settings": "Настройки Sonoff", + "Client ID:": "ID Клиента", + "chars": "символов", + "ms": "мс", + "Interval before send topics by connection:": "Интервал перед отсылкой всей топиков после соединения", + "Send interval:": "Интервал между пакетами", + "Use chunk patch:": "Использовать заплатку для Chunking", + "Divided by comma": "Использовать запятую, как разделитеть. Например 'mqtt/0/#,javascript/#'", + "Mask to publish": "Использовать запятую, как разделитеть. Например 'mqtt.0.*,javascript.*'", + "Store only on change:": "Отсылать только изменения", + "Trace output for every message:": "Вывод лога для каждого изменения", + "Test connection": "Проверить настройки", + "Result: ": "Результат ", + "connected": "успешно", + "Main settings": "Основные настройки", + "Connection settings": "Настройки соединения", + "Authentication settings": "Настройки аутентификации", + "Adapter settings": "Настройки Sonoff", + "Mask to publish own states:": "Маска для собственных значений", + "Send states (ack=true) too:": "Посылать не только команды, но и состояния (ack=true)", + "Publish all states at start:": "Выдавать собственные значения при старте", + "Use different topic names for set and get:": "Использовать разные имена для чтения и записи", + "Publish states on subscribe:": "Публиковать состояния при подписке", + "Set certificates or load it first in the system settings (right top).": "Нужно выбрать сертификаты или сначала загрузить их в системных настройках (вверху справа).", + "Prefix for topics:": "Префикс для всех значений", + "Max topic length:": "Максимальная длина имени топика", + "Server": "Сервер/брокер", + "Client": "Клиент/подписчик", + "For tele/SENSOR": "Для tele/SENSOR", + "For tele/STATE": "Для tele/STATE", + "For stat/RESULT": "Для stat/RESULT", + "Client timeout (sec):": "Client timeout (sec):", + "Auto creation of states": "Auto creation of states" +} \ No newline at end of file diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..8e46009 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + +
+ + + +

Sonoff adapter settings

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

Connection settings

Authentication settings

+
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..7b8a262 --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+
+
+
+
Connection settings
+
+
+
+
+ + +
+
+ + +
+
+
+
+
Authentication settings
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
Auto creation of states
+
+
+
+
+ + For tele/SENSOR +
+
+ + For tele/STATE +
+
+ + For stat/RESULT +
+
+
+
+
+ + diff --git a/admin/sonoff.png b/admin/sonoff.png new file mode 100644 index 0000000000000000000000000000000000000000..8d9ed5467dc37744d57ce8524ff4287dadd7a788 GIT binary patch literal 1782 zcmd6n>049R7RFBmEudJmh(M_1f>0HTSOwAQLl6)r5UbaL3zX4Rv=KC|p(v(sm1>Iv zBtR6J2xSmL8jMt_1|m}`D6K6eRKN;_LLtn<*=L_~c6$73|ADUOx8C(W>(l!1?tLjV zB*@aj-U7og%bhy{K47NkRhyYGI`i^6!3|h#i3BZ6H|zB#t1* z5zlqPrA~OBGoH5+m%89mS6uoIeseX*;eb3Bkmm}d?|>UC!41Z&0tN1%zyo~k0c4&) zwhqYN1M+o5krycR28BMLa1$ur42rjaZ?=L`E-3XQO8kkkK%$&sJ5jL%lr0z1Q%OoFX!j??3 zb^(y)KI_I(Y>`)ua@e2MD;aLeU0=$Bh}wT_3NnqALv1b$Mj$;J;cyP5B*3FJ)=iD zkfDP%=;_f;dh|Ix)`P}h&=W7vcrQKilAh>AlYMCN4>Z+}rUuaTpJ;|*5X}ss*Y}GCiv$PWg9v19Bc~o#F`cyoY7=4T{ z@ZiS_I2$}XJ=Sep&zHqAuZXuZ!0&)Ca#WA+Z|7QTlMH2L-O=;i(juygVh){HWz+WJ zL5%LQWy*L2D@Cu)kJz-(VcQg1eJ1j4Y13sL%n@QRjYn4EV`D=DwX zxU&tO?%ZVKVve`5+I>Q5Sv?s4S#dhD*B34~oQBTI_&~z|Tf6&Gnt^YVk#8qUTx#`; z>%NY&>)8tuyguzHvP#>U^lrkOS=1V3pjQZ%|D|qs^knNNK)hM|TMWumW8Zzn^OLU_ z3+S=9#WB|x9yBxu*H^C^_R-p{{{P|g>`Sd`u@q3<-)oA>hCYCt*^O1I55rAuzbAFU37ms zx#_2Kz_VyB8p~X8L+7IQb;s(?vibFyPq4T{JiW?=b)_4d3um#DoY!M@xO*t}gVIV~ zv6&m9HVsOmNbSv%`)|!2{rwH7lC3uDY-vj%Y@;G|9#@_hnEJ4DuUVx$+Y&OPG b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); + + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); + langs[lang] = {}; + keys.forEach(function (word, i) { + langs[lang][word] = values[i].replace(/<\/ i>/g, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); + }); + + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} +function languages2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} + +gulp.task('adminWords2languages', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('adminWords2languagesFlat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('adminLanguagesFlat2words', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('adminLanguages2words', function (done) { + languages2words('./admin/'); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + var news = iopackage.common.news; + var newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('updateReadme', function (done) { + var readme = fs.readFileSync('README.md').toString(); + var pos = readme.indexOf('## Changelog\n'); + if (pos !== -1) { + var readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + var readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) === -1) { + var timestamp = new Date(); + var date = timestamp.getFullYear() + '-' + + ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ('0' + (timestamp.getDate()).toString(10)).slice(-2); + + var news = ''; + if (iopackage.common.news && iopackage.common.news[pkg.version]) { + news += '* ' + iopackage.common.news[pkg.version].en; + } + + fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + done(); +}); + +gulp.task('default', ['updatePackages', 'updateReadme']); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100755 index 0000000..b4ffb52 --- /dev/null +++ b/io-package.json @@ -0,0 +1,184 @@ +{ + "common": { + "name": "sonoff", + "version": "2.2.3", + "news": { + "2.2.3": { + "en": "Support for compact mode", + "de": "Unterstützung für den Kompaktmodus", + "ru": "Поддержка компактного режима", + "pt": "Suporte para o modo compacto", + "nl": "Ondersteuning voor de compacte modus", + "fr": "Prise en charge du mode compact", + "it": "Supporto per la modalità compatta", + "es": "Soporte para modo compacto", + "pl": "Obsługa trybu kompaktowego" + }, + "2.2.2": { + "en": "Configuration was fixed", + "de": "Die Konfiguration wurde repariert", + "ru": "Конфигурация была исправлена", + "pt": "Configuração foi corrigida", + "nl": "Configuratie was opgelost", + "fr": "La configuration a été corrigée", + "it": "La configurazione è stata riparata", + "es": "La configuración fue arreglada", + "pl": "Konfiguracja została naprawiona" + }, + "2.2.1": { + "en": "- in names was allowed again", + "de": "- in Namen wurde wieder erlaubt", + "ru": "- в именах снова было разрешено", + "pt": "- nos nomes foi permitido novamente", + "nl": "- in namen was weer toegestaan", + "fr": "- dans les noms a été autorisé à nouveau", + "it": "- nei nomi è stato permesso di nuovo", + "es": "- en los nombres se permitió de nuevo", + "pl": "- w nazwach było ponownie dozwolone" + }, + "2.2.0": { + "en": "Auto generate of objects was implemented", + "de": "Auto-Generierung von Objekten wurde implementiert", + "ru": "Была реализована автоматическая генерация объектов", + "pt": "Geração automática de objetos foi implementada", + "nl": "Automatisch genereren van objecten is geïmplementeerd", + "fr": "La génération automatique d'objets a été implémentée", + "it": "È stata implementata la generazione automatica di oggetti", + "es": "Generación automática de objetos fue implementado", + "pl": "Automatycznie generowano obiekty" + }, + "2.1.3": { + "en": "Added HC-SR04 Ultrasonic Sensor", + "de": "HC-SR04 Ultraschallsensor hinzugefügt", + "ru": "Добавлен ультразвуковой датчик HC-SR04", + "pt": "Adicionado Sensor Ultrassônico HC-SR04", + "nl": "HC-SR04 ultrasone sensor toegevoegd", + "fr": "Capteur à ultrasons HC-SR04 ajouté", + "it": "Aggiunto il sensore a ultrasuoni HC-SR04", + "es": "Sensor ultrasónico HC-SR04 agregado", + "pl": "Dodano czujnik ultradźwiękowy HC-SR04" + }, + "2.1.2": { + "en": "Added support of UvLight, Longitude and Latitude", + "de": "Zusätzliche Unterstützung von UvLight, Longitude und Latitude", + "ru": "Добавлена ​​поддержка UvLight, долготы и широты", + "pt": "Adicionado suporte de UvLight, Longitude e Latitude", + "nl": "Ondersteuning toegevoegd van UvLight, Longitude en Latitude", + "fr": "Ajout du support de UvLight, Longitude et Latitude", + "it": "Aggiunto il supporto di UvLight, Longitude e Latitude", + "es": "Se agregó soporte de UvLight, Longitude y Latitude", + "pl": "Dodano obsługę UvLight, długości i szerokości geograficznej" + }, + "2.1.1": { + "en": "Support of the particle concentration sensor", + "de": "Unterstützung des Partikelkonzentrationssensors", + "ru": "Поддержка датчика концентрации частиц", + "pt": "Suporte do sensor de concentração de partículas", + "nl": "Ondersteuning van de deeltjesconcentratiesensor", + "fr": "Support du capteur de concentration de particules", + "it": "Supporto del sensore di concentrazione di particelle", + "es": "Soporte del sensor de concentración de partículas", + "pl": "Wsparcie czujnika stężenia cząstek" + }, + "2.1.0": { + "en": "Support of the devices control", + "de": "Unterstützung der Gerätekontrolle", + "ru": "Поддержка управления устройствами", + "pt": "Suporte do controle de dispositivos", + "nl": "Ondersteuning van de apparatencontrole", + "fr": "Prise en charge du contrôle des appareils", + "it": "Supporto del controllo dei dispositivi", + "es": "Soporte del control de dispositivos", + "pl": "Obsługa sterowania urządzeniami" + }, + "2.0.2": { + "en": "Fixing re-connection of clients\nSeaPressure was added", + "de": "Reparieren der Neuverbindung von Clients\nSeaPressure wurde hinzugefügt", + "ru": "Фиксация повторного подключения клиентов\nДобавлено SeaPressure", + "pt": "Reparando conexão de clientes\nA SeaPressure foi adicionada", + "nl": "Bevestiging van opnieuw verbinden van clients\nSeaPressure is toegevoegd", + "fr": "Correction de la reconnexion des clients\nSeaPressure a été ajouté", + "it": "Risolvere la riconnessione dei client\nSeaPressure è stato aggiunto", + "es": "Reparar la reconexión de los clientes\nSeaPressure fue agregado", + "pl": "Naprawianie ponownego połączenia klientów\nSeaPressure zostało dodane" + }, + "2.0.1": { + "en": "Add Light/Noise/AirQuality\nDo not send pingresp for invalid clients", + "de": "Fügen Sie den Light/Noise/AirQuality hinzu\nSendet keine Pingresp für ungültige Clients", + "ru": "Добавлены Light/Noise/AirQuality\nНе отправляen pingresp для недействительных клиентов", + "pt": "Adicionar Light/Noise/AirQuality", + "nl": "Light/Noise/AirQuality toevoegen", + "fr": "Ajouter un capteur Light/Noise/AirQuality", + "it": "Aggiungi il Light/Noise/AirQuality", + "es": "Agregar Light/Noise/AirQuality", + "pl": "Dodaj Light/Noise/AirQuality" + } + }, + "title": "Sonoff", + "desc": { + "en": "This adapter allows to connect sonoff components via MQTT", + "de": "Dieser Adapter ermöglicht den Anschluss von Sonoff-Komponenten über MQTT", + "ru": "Этот адаптер позволяет подключать компоненты Sonoff через MQTT", + "pt": "Este adaptador permite conectar os componentes do sonoff via MQTT", + "nl": "Met deze adapter kunnen sonoff-componenten via MQTT worden aangesloten", + "fr": "Cet adaptateur permet de connecter des composants sonoff via MQTT", + "it": "Questo adattatore consente di collegare i componenti Sonoff via MQTT", + "es": "Este adaptador permite conectar componentes de sonof a través de MQTT", + "pl": "Ten adapter umożliwia łączenie komponentów sonoff za pośrednictwem MQTT" + }, + "authors": [ + "bluefox " + ], + "license": "MIT", + "platform": "Javascript/Node.js", + "mode": "daemon", + "compact": true, + "enabled": true, + "readme": "https://github.com/ioBroker/ioBroker.sonoff/blob/master/README.md", + "loglevel": "info", + "icon": "sonoff.png", + "materialize": true, + "keywords": ["ITEAD", "MQTT", "Sonoff", "Tasmota"], + "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.sonoff/master/admin/sonoff.png", + "type": "lighting" + }, + "native": { + "port": 1883, + "bind": "0.0.0.0", + "user": "", + "pass": "", + "timeout": 300, + "TELE_SENSOR": false, + "TELE_STATE": false, + "STAT_RESULT": false, + "storeClientsTime": 1440, + "defaultQoS": 0, + "retransmitInterval": 2000, + "retransmitCount": 10 + }, + "objects": [ + + ], + "instanceObjects": [ + { + "_id": "info", + "type": "channel", + "common": { + "name": "Information" + }, + "native": {} + }, + { + "_id": "info.connection", + "type": "state", + "common": { + "role": "info.clients", + "name": "List of connected clients", + "type": "string", + "read": true, + "write": false + }, + "native": {} + } + ] +} diff --git a/lib/datapoints.js b/lib/datapoints.js new file mode 100644 index 0000000..ead799b --- /dev/null +++ b/lib/datapoints.js @@ -0,0 +1,139 @@ +module.exports = { + Action: {type: 'number', role: 'value', read: true, write: true}, + AirQuality: {type: 'number', role: 'value', read: true, write: false, unit: '%'}, + A0: {type: 'number', role: 'value', read: true, write: false}, + A1: {type: 'number', role: 'value', read: true, write: false}, + A2: {type: 'number', role: 'value', read: true, write: false}, + A3: {type: 'number', role: 'value', read: true, write: false}, + ANALOG: {type: 'object', role: 'state', read: true, write: false}, + Analog0: {type: 'number', role: 'value', read: true, write: false}, + Analog1: {type: 'number', role: 'value', read: true, write: false}, + Analog2: {type: 'number', role: 'value', read: true, write: false}, + Analog3: {type: 'number', role: 'value', read: true, write: false}, + Arm: {type: 'number', role: 'value', read: true, write: true}, + 'Approx. Altitude': {type: 'number', role: 'value.altitude', read: true, write: false, unit: 'm'}, + Bits: {type: 'number', role: 'value', read: true, write: false}, + Blue: {type: 'number', role: 'level.color.blue', read: true, write: true, min: 0, max: 100}, + Boiler: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Burner: {type: 'number', role: 'value', read: true, write: false}, + CT: {type: 'number', role: 'level.color.temp', read: true, write: true}, + CW: {type: 'number', role: 'level.color.coldwhite', read: true, write: true}, + CW_POWER: {type: 'boolean', role: 'switch.light', read: true, write: true, storeMap: true}, + CarbonDioxide: {type: 'number', role: 'value.CO2', read: true, write: false, unit: 'ppm'}, + Channel: {type: 'array', role: 'level', read: true, write: true}, + Collector: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Color: {type: 'string', role: 'level.color.rgb', read: true, write: true}, + Counter1: {type: 'number', role: 'value', read: true, write: false}, + Counter2: {type: 'number', role: 'value', read: true, write: false}, + Counter3: {type: 'number', role: 'value', read: true, write: false}, + Counter4: {type: 'number', role: 'value', read: true, write: false}, + Current: {type: 'number', role: 'value.current', read: true, write: false, unit: 'A'}, + Data: {type: 'string', role: 'state', read: true, write: true}, + Days: {type: 'string', role: 'state', read: true, write: true}, + Dimmer: {type: 'number', role: 'level.dimmer', read: true, write: true}, + Distance: {type: 'number', role: 'value', read: true, write: false, unit: 'cm'}, + Druck: {type: 'number', role: 'value.pressure', read: true, write: false, unit: 'P'}, + Factor: {type: 'number', role: 'value', read: true, write: false}, + Fade: {type: 'boolean', role: 'switch', read: true, write: true}, + Faktor: {type: 'number', role: 'value', read: true, write: false}, + Feuchtigkeit: {type: 'number', role: 'value.humidity', read: true, write: false, unit: '%'}, + Green: {type: 'number', role: 'level.color.green', read: true, write: true, min: 0, max: 100}, + Hostname: {type: 'string', role: 'state', read: true, write: false}, + Hue: {type: 'number', role: 'level.color.hue', read: true, write: true, min: 0}, // max ?? + Humidity: {type: 'number', role: 'value.humidity', read: true, write: false, unit: '%'}, + HSBColor: {type: 'string', role: 'value.hsb', read: true, write: true}, + IPAddress: {type: 'string', role: 'state', read: true, write: false}, + IPaddress: {type: 'string', role: 'state', read: true, write: false}, + Illuminance: {type: 'number', role: 'value.illuminance', read: true, write: false, unit: 'lx'}, + IrReceived: {type: 'object', role: 'state', read: true, write: false}, + IRSend: {type: 'string', role: 'state', read: true, write: true}, + Latitude: {type: 'number', role: 'value.gps.latitude', read: true, write: false, unit: '°'}, + Laufzeit: {type: 'number', role: 'value.duration', read: true, write: false, unit: 'hours'}, /// ? + LedBar: {type: 'number', role: 'value', read: true, write: true}, + LED_Mode: {type: 'number', role: 'value', read: true, write: true}, + Leistung: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'W'}, + Light: {type: 'number', role: 'value', read: true, write: false, unit: 'lx'}, + Longitude: {type: 'number', role: 'value.gps.longitude', read: true, write: false, unit: '°'}, + modeLedExor: {type: 'boolean', role: 'switch', read: true, write: true, def: false, desc: 'exor for white LEDs and color leds => if the white LEDs are switched on, color LEDs are switched off and vice versa'}, + modeReadColors:{type: 'boolean', role: 'switch', read: true, write: true, def: true, desc: 'allow for color read from MQTT'}, + Mode: {type: 'number', role: 'value', read: true, write: true}, + Module: {type: 'string', role: 'state', read: true, write: false}, + Noise: {type: 'number', role: 'value', read: true, write: false, unit: 'dB'}, + Output: {type: 'number', role: 'value', read: true, write: true}, + Outsidetemp: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + 'PM2.5': {type: 'number', role: 'value', read: true, write: false, unit: 'ug/m3'}, + PM10: {type: 'number', role: 'value', read: true, write: false, unit: 'ug/m3'}, + POWER1: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER2: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER3: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER4: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER5: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER6: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER7: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER8: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + POWER: {type: 'boolean', role: 'switch', read: true, write: true, storeMap: true}, + Protocol: {type: 'string', role: 'state', read: true, write: true}, + Power: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'W'}, + Power_curr: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'W'}, + Pressure: {type: 'number', role: 'value.pressure', read: true, write: false, unit: 'P'}, + Punkt: {type: 'number', role: 'value', read: true, write: false, unit: '?'}, /// ? + RGB_POWER: {type: 'boolean', role: 'switch.light', read: true, write: true, storeMap: true}, + RSSI: {type: 'number', role: 'value.rssi', read: true, write: false}, + Red: {type: 'number', role: 'level.color.red', read: true, write: true, min: 0, max: 100}, + Repeat: {type: 'number', role: 'value', read: true, write: true}, + RestartReason: {type: 'string', role: 'state', read: true, write: false}, + Returns: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Roomtemp: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Saturation: {type: 'number', role: 'level.color.saturation', read: true, write: true, min: 0}, // max: 100 ?? + Scheme: {type: 'number', role: 'value', read: true, write: false}, + SeaPressure: {type: 'number', role: 'value.pressure', read: true, write: false, unit: 'P'}, + Solarpump: {type: 'number', role: 'value', read: true, write: false}, + Solarstorage: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Spannung: {type: 'number', role: 'value.voltage', read: true, write: false, unit: 'V'}, + Speed: {type: 'number', role: 'value', read: true, write: false}, + Status: {type: 'number', role: 'value', read: true, write: false}, + Strom: {type: 'number', role: 'value.current', read: true, write: false, unit: 'A'}, + Switch1: {type: 'boolean', role: 'switch', read: true, write: false}, + Switch2: {type: 'boolean', role: 'switch', read: true, write: false}, + Switch3: {type: 'boolean', role: 'switch', read: true, write: false}, + Switch4: {type: 'boolean', role: 'switch', read: true, write: false}, + TVOC: {type: 'number', role: 'value.tvoc', read: true, write: false, unit: 'ppb'}, + Temperatur: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Temperature: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Time: {type: 'string', role: 'state', read: true, write: true}, + Timer1: {type: 'object', role: 'state', read: true, write: true}, + Timer2: {type: 'object', role: 'state', read: true, write: true}, + Timer3: {type: 'object', role: 'state', read: true, write: true}, + Timer4: {type: 'object', role: 'state', read: true, write: true}, + Timer5: {type: 'object', role: 'state', read: true, write: true}, + Timer6: {type: 'object', role: 'state', read: true, write: true}, + Timer7: {type: 'object', role: 'state', read: true, write: true}, + Timer8: {type: 'object', role: 'state', read: true, write: true}, + Timer9: {type: 'object', role: 'state', read: true, write: true}, + Timer10: {type: 'object', role: 'state', read: true, write: true}, + Timer11: {type: 'object', role: 'state', read: true, write: true}, + Timer12: {type: 'object', role: 'state', read: true, write: true}, + Timer13: {type: 'object', role: 'state', read: true, write: true}, + Timer14: {type: 'object', role: 'state', read: true, write: true}, + Timer15: {type: 'object', role: 'state', read: true, write: true}, + Timer16: {type: 'object', role: 'state', read: true, write: true}, + Today: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'kWh'}, + Total: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'kWh'}, + Total_in: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'kWh'}, + Total_out: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'kWh'}, + Uptime: {type: 'string', role: 'state', read: true, write: false}, + UvLevel: {type: 'number', role: 'value', read: true, write: false}, + VCC: {type: 'number', role: 'value.voltage', read: true, write: false, unit: 'V'}, + Vcc: {type: 'number', role: 'value.voltage', read: true, write: false, unit: 'V'}, + Version: {type: 'string', role: 'state', read: true, write: false}, + Voltage: {type: 'number', role: 'value.voltage', read: true, write: false, unit: 'V'}, + Window: {type: 'number', role: 'value', read: true, write: true}, + WW: {type: 'number', role: 'level.color.white', read: true, write: true}, + WW_POWER: {type: 'boolean', role: 'switch.light', read: true, write: true, storeMap: true}, + Wakeup: {type: 'string', role: 'state', read: true, write: true}, + Warmwater: {type: 'number', role: 'value.temperature', read: true, write: false, unit: '°C'}, + Yesterday: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'kWh'}, + eCO2: {type: 'number', role: 'value.eco2', read: true, write: false, unit: 'ppm'}, + gestern: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'kWh'}, + heute: {type: 'number', role: 'value.power.consumption', read: true, write: false, unit: 'kWh'} +}; diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..273520d --- /dev/null +++ b/lib/server.js @@ -0,0 +1,1643 @@ +/* jshint -W097 */ +/* jshint strict:true */ +/* jslint node: true */ +/* jslint esversion: 6 */ +'use strict'; + +const mqtt = require('mqtt-connection'); +const net = require('net'); +const types = require(__dirname + '/datapoints'); +//const memwatch = require('memwatch-next'); +//const heapdump = require('heapdump'); +const hueCalc = true; +//var heapTest = 0; +const FORBIDDEN_CHARS = /[\]\[*,;'"`<>\\?]/g; +const mappingClients = {}; +/* +* HSV to RGB color conversion +* +* H runs from 0 to 360 degrees +* S and V run from 0 to 100 +* +* Ported from the excellent java algorithm by Eugene Vishnevsky at: +* http://www.cs.rit.edu/~ncs/color/t_convert.html +*/ +function hsvToRgb(h, s, v) { + let r, g, b; + let i; + let f, p, q, t; + + // Make sure our arguments stay in-range + h = Math.max(0, Math.min(360, h)); + s = Math.max(0, Math.min(100, s)); + v = Math.max(0, Math.min(100, v)); + + // We accept saturation and value arguments from 0 to 100 because that's + // how Photoshop represents those values. Internally, however, the + // saturation and value are calculated from a range of 0 to 1. We make + // That conversion here. + s /= 100; + v /= 100; + + if (s === 0) { + // Achromatic (grey) + r = g = b = v; + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255) + ]; + } + + h /= 60; // sector 0 to 5 + i = Math.floor(h); + f = h - i; // factorial part of h + p = v * (1 - s); + q = v * (1 - s * f); + t = v * (1 - s * (1 - f)); + + switch (i) { + case 0: + r = v; + g = t; + b = p; + break; + + case 1: + r = q; + g = v; + b = p; + break; + + case 2: + r = p; + g = v; + b = t; + break; + + case 3: + r = p; + g = q; + b = v; + break; + + case 4: + r = t; + g = p; + b = v; + break; + + default: // case 5: + r = v; + g = p; + b = q; + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255) + ]; +} + +function componentToHex(c) { + const hex = c.toString(16); + return hex.length === 1 ? '0' + hex : hex; +} + +function toPaddedHexString(num, len) { + if (len === 2) { + if (num > 255) { + num = 255; + } + } + const str = num.toString(16); + return '0'.repeat(len - str.length) + str; +} + +function MQTTServer(adapter) { + if (!(this instanceof MQTTServer)) return new MQTTServer(adapter); + + const NO_PREFIX = ''; + + let server = new net.Server(); + const clients = {}; + const tasks = []; + let messageId = 1; + let persistentSessions = {}; + let resending = false; + let resendTimer = null; + + const cacheAddedObjects = {}; + const cachedModeExor = {}; + const cachedReadColors = {}; + + this.destroy = cb => { + if (resendTimer) { + clearInterval(resendTimer); + resendTimer = null; + } + + if (server) { + let cnt = 0; + for (let id in clients) { + if (clients.hasOwnProperty(id)) { + cnt++; + adapter.setForeignState(adapter.namespace + '.' + clients[id].iobId + '.alive', false, true, () => { + if (!--cnt) { + // to release all resources + server.close(() => cb && cb()); + server = null; + } + }); + } + } + if (!cnt) { + // to release all resources + server.close(() => cb && cb()); + server = null; + } + } + }; + +/* + memwatch.on('leak', (info) => { + //console.error('Memory leak detected:\n', info); + adapter.log.info('Memory leak detected:\n', info); + var filename='/opt/iobroker/dumps/' + Date.now() + '.heapsnapshot'; + heapdump.writeSnapshot((err,filename) => { + if (err) { + console.error(err); + } + else console.error('Wrote snapshot: ' + filename); + }); + }); + */ +/* + memwatch.on('stats', function(stats) { + adapter.log.info("stats:",stats); + }); +*/ + + function setColor(channelId, val) { + //adapter.log.info('color write: '+ val); + const stateId = 'Color'; + if (clients[channelId]._map && clients[channelId]._map[stateId]) { + setImmediate(sendState2Client, clients[channelId], clients[channelId]._map[stateId] || 'cmnd/sonoff/Color', val, adapter.config.defaultQoS); + } else if (clients[channelId]._fallBackName) { + setImmediate(sendState2Client, clients[channelId], 'cmnd/' + clients[channelId]._fallBackName + '/' + stateId, val, adapter.config.defaultQoS); + } else { + adapter.log.warn('Unknown mapping for "' + stateId + '"'); + } + } + + function setPower(channelId, val) { + const stateId = 'POWER'; + if (clients[channelId]._map && clients[channelId]._map[stateId]) { + setImmediate(sendState2Client, clients[channelId], clients[channelId]._map[stateId] || 'cmnd/sonoff/POWER', val ? 'ON' : 'OFF', adapter.config.defaultQoS); + } else if (clients[channelId]._fallBackName) { + setImmediate(sendState2Client, clients[channelId], 'cmnd/' + clients[channelId]._fallBackName + '/' + stateId, val, adapter.config.defaultQoS); + } else { + adapter.log.warn('Unknown mapping for "' + stateId + '"'); + } + } + + function setStateImmediate(channelId,stateId,val) { + if (clients[channelId]._map && clients[channelId]._map[stateId]) { + setImmediate(sendState2Client, clients[channelId], clients[channelId]._map[stateId] || ('cmnd/sonoff/' + stateId), val, adapter.config.defaultQoS); + } else if (clients[channelId]._fallBackName) { + setImmediate(sendState2Client, clients[channelId], 'cmnd/' + clients[channelId]._fallBackName + '/' + stateId, val, adapter.config.defaultQoS); + } else { + adapter.log.warn('Unknown mapping for "' + stateId + '"'); + } + } + + function _setState(id, val) { + adapter.setForeignState(id, val, true, () => { + setImmediate(processTasks); + }); + } + + function updateState(task, val) { + if (val !== undefined) { + task.cb = _setState; + task.cbArg = val; + } + tasks.push(task); + if (tasks.length === 1) { + setImmediate(processTasks); + } + } + + const specVars = ['Red', 'Green', 'Blue', 'WW', 'CW', 'Color', 'RGB_POWER', 'WW_POWER', 'CW_POWER', 'Hue', 'Saturation']; + function onStateChangedColors(id, state, channelId, stateId) { + if (!channelId) { + let parts = id.split('.'); + stateId = parts.pop(); + + if (stateId === 'level' || stateId === 'state' || stateId === 'red' || stateId === 'blue' || stateId === 'green') { + stateId = parts.pop() + '.' + stateId; + } + + channelId = parts.splice(2, parts.length).join('.'); + } + const ledModeIdExor = adapter.namespace + '.' + channelId + '.modeLedExor'; + if (cachedModeExor[ledModeIdExor] === undefined) { + return adapter.getForeignState(ledModeIdExor, (err, _state) => { + cachedModeExor[ledModeIdExor] = _state ? _state.val || false : true; + setImmediate(() => onStateChangedColors(id, state, channelId, stateId)); + }); + } + + // ledstripe objects + const exorWhiteLeds = cachedModeExor[ledModeIdExor]; // exor for white leds and color leds => if white leds are switched on, color leds are switched off and vice versa (default on) + + // now evaluate ledstripe vars + // adaptions for magichome tasmota + if (stateId.match(/Color\d?/)) { + //adapter.log.info('sending color'); + // id = sonoff.0.DVES_96ABFA.Color + // statid=Color + // state = {"val":"#faadcf","ack":false,"ts":1520146102580,"q":0,"from":"system.adapter.web.0","lc":1520146102580} + + // set white to rgb or rgbww + adapter.getObject(id, (err, obj) => { + if (!obj) { + adapter.log.warn('ill rgbww obj'); + } else { + const role = obj.common.role; + let color; + + //adapter.log.info(state.val); + if (role === 'level.color.rgbww') { + // rgbww + if (state.val.toUpperCase() === '#FFFFFF') { + // transform white to WW + //color='000000FF'; + color = state.val.substring(1) + '00'; + } else { + // strip # char and add ww + color = state.val.substring(1) + '00'; + } + } else if (role === 'level.color.rgbcwww') { + color = state.val.substring(1) + '0000'; + } else { + // rgb, strip # char + color = state.val.substring(1); + } + + //adapter.log.info('color :' + color + ' : ' + role); + // strip # char + //color=state.val.substring(1); + + setColor(channelId, color); + + // set rgb too + const hidE = id.split('.'); + const deviceDesc = hidE[0] + '.' + hidE[1] + '.' + hidE[2]; + let idAlive = deviceDesc + '.Red'; + adapter.setState(idAlive, 100 * parseInt(color.substring(0, 2), 16) / 255, true); + + idAlive = deviceDesc + '.Green'; + adapter.setState(idAlive, 100 * parseInt(color.substring(2, 4), 16) / 255, true); + + idAlive = deviceDesc + '.Blue'; + adapter.setState(idAlive, 100 * parseInt(color.substring(4, 6), 16) / 255, true); + } + }); + } else { + const hidE = id.split('.'); + const deviceDesc = hidE[0] + '.' + hidE[1] + '.' + hidE[2]; + if (stateId.match(/Red\d?/)) { + // set red component + if (state.val > 100) state.val = 100; + const red = toPaddedHexString(Math.floor(255 * state.val / 100), 2); + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + adapter.setState(idAlive, '#000000', false); + return; + } + const color = state.val.substring(1); + // replace red component + const out = red + color.substring(2, 10); + adapter.setState(idAlive, '#' + out, false); + setColor(channelId, out); + }); + } else + if (stateId.match(/Green\d?/)) { + // set green component + if (state.val > 100) state.val = 100; + const green = toPaddedHexString(Math.floor(255 * state.val / 100), 2); + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + adapter.setState(idAlive, '#000000', false); + return; + } + const color = state.val.substring(1); + // replace green component + const out = color.substring(0, 2) + green + color.substring(4, 10); + adapter.setState(idAlive, '#' + out, false); + setColor(channelId, out); + }); + } else + if (stateId.match(/Blue\d?/)) { + // set blue component + if (state.val > 100) state.val = 100; + const blue = toPaddedHexString(Math.floor(255 * state.val / 100), 2); + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + adapter.setState(idAlive, '#000000', false); + return; + } + const color = state.val.substring(1); + // replace blue component + const out = color.substring(0, 4) + blue + color.substring(6, 10); + adapter.setState(idAlive, '#' + out, false); + setColor(channelId, out); + }); + } else + if (stateId.match(/RGB_POWER\d?/)) { + // set ww component + const rgbpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1'; + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + //adapter.log.warn('ill state Color'); + adapter.setState(idAlive, '#000000', false); + return; + } + const color = state.val.substring(1); + let rgb = '000000'; + if (rgbpow === true) { + rgb = 'FFFFFF'; + } + // replace rgb component + let out = rgb + color.substring(6, 10); + + if (rgbpow && exorWhiteLeds) { + //adapter.log.info('reset white'); + out = rgb + '0000'; + let idAlive = deviceDesc + '.WW_POWER'; + adapter.setState(idAlive, false, false); + idAlive = deviceDesc + '.WW'; + adapter.setState(idAlive, 0, false); + idAlive = deviceDesc + '.CW_POWER'; + adapter.setState(idAlive, false, false); + idAlive = deviceDesc + '.CW'; + adapter.setState(idAlive, 0, false); + } + setColor(channelId, out); + adapter.setState(idAlive, '#' + out, false); + if (rgbpow) { + setPower(channelId, true) + } + // if led_mode&1, exor white leds + + }); + } else + // calc hue + saturation params to rgb + if (hueCalc && stateId.match(/Hue\d?/)) { + let hue = state.val; + if (hue > 359) hue = 359; + // recalc color by hue + const idAlive = deviceDesc + '.Dimmer'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + const dim = 100; + adapter.setState(idAlive, dim, true); + //adapter.log.warn('ill state Dimmer'); + } else { + const dim = state.val; + let idAlive = deviceDesc + '.Saturation'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + const sat = 100; + adapter.setState(idAlive, sat, true); + } else { + const sat = state.val; + const rgb = hsvToRgb(hue, sat, dim); + const hexval = componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]); + let idAlive = deviceDesc + '.Color'; + adapter.setState(idAlive, '#' + hexval, false); + } + }); + } + }); + } else + if (hueCalc && stateId.match(/Saturation\d?/)) { + let sat = state.val; + if (sat > 100) sat = 100; + // recalc color by saturation + const idAlive = deviceDesc + '.Dimmer'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + const dim = 100; + adapter.setState(idAlive, dim, true); + //adapter.log.warn('ill state Dimmer'); + } else { + const dim = state.val; + const idAlive = deviceDesc + '.Hue'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + const hue = 100; + adapter.setState(idAlive, hue, true); + } else { + const hue = state.val; + const rgb = hsvToRgb(hue, sat, dim); + const hexval = componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]); + const idAlive = deviceDesc + '.Color'; + adapter.setState(idAlive, '#' + hexval, false); + } + }); + } + }); + } else { + // get color attributes to check other ledstripe vars + const idAlive = deviceDesc + '.Color'; + adapter.getForeignObject(idAlive, (err, obj) => { + if (!obj) { + // no color object + adapter.log.warn(`unknown object: ${id}: ${state}`); + } else { + const role = obj.common.role; + //if (role='level.color.rgb') return; + let wwindex; + if (role === 'level.color.rgbww') { + wwindex = 6; + } else { + wwindex = 8; + } + + if (stateId.match(/WW_POWER\d?/)) { + // set ww component + const wwpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1'; + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + adapter.log.warn('ill state Color'); + return; + } + const color = state.val.substring(1); + let ww = '00'; + if (wwpow) { + ww = 'FF'; + } + // replace ww component + let out = color.substring(0, wwindex) + ww; + if (wwpow && exorWhiteLeds) { + //adapter.log.info('reset white'); + out = '000000' + ww; + let idAlive = deviceDesc + '.RGB_POWER'; + adapter.setState(idAlive,false, false); + } + + let idAlive = deviceDesc + '.Color'; + adapter.setState(idAlive,'#' + out, false); + setColor(channelId, out); + + // set ww channel + idAlive = deviceDesc + '.WW'; + adapter.setState(idAlive, 100 * parseInt(out.substring(6, 8), 16) / 255, true); + + // in case POWER is off, switch it on + wwpow && setPower(channelId, true) + }); + } else + if (stateId.match(/CW_POWER\d?/)) { + // set ww component + const cwpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1'; + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + adapter.log.warn('ill state Color'); + return; + } + const color = state.val.substring(1); + let cw = '00'; + if (cwpow) { + cw = 'FF'; + } + // replace cw component + let out = color.substring(0, 6) + cw + color.substring(8, 10); + if (cwpow && exorWhiteLeds) { + //adapter.log.info('reset white'); + out = '000000' + cw + color.substring(8, 10); + let idAlive = deviceDesc + '.RGB_POWER'; + adapter.setState(idAlive, false, false); + } + + let idAlive = deviceDesc + '.Color'; + adapter.setState(idAlive, '#' + out, false); + setColor(channelId, out); + + // set cw channel + idAlive = deviceDesc + '.CW'; + adapter.setState(idAlive, 100 * parseInt(out.substring(6, 8), 16) / 255, true); + + // in case POWER is off, switch it on + if (cwpow) { + let idAlive = deviceDesc + '.POWER'; + adapter.setState(idAlive, true, false); + } + }); + } else + if (stateId.match(/WW\d?/)) { + // set ww component + const ww = toPaddedHexString(Math.floor(255 * state.val / 100), 2); + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + adapter.setState(idAlive, '#000000', false); + return; + } + const color = state.val.substring(1); + // replace ww component + const out = color.substring(0, wwindex) + ww; + setColor(channelId, out); + }); + } else + if (stateId.match(/CW\d?/)) { + // set ww component + const cw = toPaddedHexString(Math.floor(255 * state.val / 100), 2); + const idAlive = deviceDesc + '.Color'; + adapter.getForeignState(idAlive, (err, state) => { + if (!state) { + adapter.setState(idAlive, '#000000', false); + return; + } + const color = state.val.substring(1); + // replace cw component + const out = color.substring(0, 6) + cw + color.substring(8, 10); + setColor(channelId, out); + }); + } + } + }); + } + } + } + + this.onStateChange = (id, state) => { + adapter.log.debug('onStateChange ' + id + ': ' + JSON.stringify(state)); + if (server && state && !state.ack) { + // find client.id + let parts = id.split('.'); + const stateId = parts.pop(); + const channelId = parts.splice(2, parts.length).join('.'); + if (clients[mappingClients[channelId]]) { + // check for special ledstripe vars + if (specVars.indexOf(stateId) === -1) { + // other objects + adapter.getObject(id, (err, obj) => { + if (!obj) { + adapter.log.warn(`invalid obj ${id}`); + } else { + const type = obj.common.type; + switch (type) { + case 'boolean': + setStateImmediate(mappingClients[channelId], stateId, state.val ? 'ON' : 'OFF'); + break; + case 'number': + setStateImmediate(mappingClients[channelId], stateId, state.val.toString()); + break; + case 'string': + setStateImmediate(mappingClients[channelId], stateId, state.val); + break; + } + } + }); + } else { + onStateChangedColors(id, state, channelId, stateId); + } + } else { + //Client:"DVES_96ABFA : MagicHome" not connected => State: sonoff.0.myState - Value: 0, ack: false, time stamp: 1520369614189, last changed: 1520369614189 + // if (server && state && !state.ack) { + // server = false + // or state = false + // or state.ack = true + // or clients[channelId] = false + adapter.log.warn(`Client "${channelId}" not connected`); + + /* + if (!clients[channelId]) { + var idAlive='sonoff.0.'+channelId+'.INFO.IPAddress'; + adapter.getForeignState(idAlive, function (err, state) { + if (!state) { + adapter.log.warn('Client "' + channelId + '" could not get ip adress'); + } else { + var ip=state.val; + adapter.log.warn('Clients ip "' + ip); + + request('http://'+ip+'/cm?cmnd=Restart 1', function(error, response, body) { + if (error || response.statusCode !== 200) { + log('Fehler beim Neustart von Sonoff: ' + channelId + ' (StatusCode = ' + response.statusCode + ')'); + } + }); + } + }); + }*/ + } + } + }; + + function processTasks() { + if (tasks && tasks.length) { + let task = tasks[0]; + if (task.type === 'addObject') { + if (!cacheAddedObjects[task.id]) { + cacheAddedObjects[task.id] = true; + adapter.getForeignObject(task.id, (err, obj) => { + if (!obj) { + adapter.setForeignObject(task.id, task.data, (/* err */) => { + adapter.log.info('new object created: ' + task.id); + tasks.shift(); + if (task.cb) { + task.cb(task.id, task.cbArg); + } else { + setImmediate(processTasks); + } + }); + } else { + tasks.shift(); + if (task.cb) { + task.cb(task.id, task.cbArg); + } else { + setImmediate(processTasks); + } + } + }); + } else { + tasks.shift(); + if (task.cb) { + task.cb(task.id, task.cbArg); + } else { + setImmediate(processTasks); + } + } + } else if (task.type === 'extendObject') { + adapter.extendObject(task.id, task.data, (/* err */) => { + tasks.shift(); + if (task.cb) { + task.cb(task.id, task.cbArg); + } else { + setImmediate(processTasks); + } + }); + } else if (task.type === 'deleteState') { + adapter.deleteState('', '', task.id, (/* err */) => { + tasks.shift(); + if (task.cb) { + task.cb(task.id, task.cbArg); + } else { + setImmediate(processTasks); + } + }); + } else { + adapter.log.error('Unknown task name: ' + JSON.stringify(task)); + tasks.shift(); + if (task.cb) { + task.cb(task.id, task.cbArg); + } else { + setImmediate(processTasks); + } + } + } + } + + function createClient(client) { + // mqtt.0.cmnd.sonoff.POWER + // mqtt.0.stat.sonoff.POWER + let isStart = !tasks.length; + + let id = adapter.namespace + '.' + client.iobId; + let obj = { + _id: id, + common: { + name: client.id, + desc: '' + }, + native: { + clientId: client.id + }, + type: 'channel' + }; + tasks.push({type: 'addObject', id: obj._id, data: obj}); + + obj = { + _id: id + '.alive', + common: { + type: 'boolean', + role: 'indicator.connected', + read: true, + write: false, + name: client.id + ' alive' + }, + type: 'state' + }; + tasks.push({type: 'addObject', id: obj._id, data: obj}); + if (isStart) { + processTasks(tasks); + } + } + + function updateClients() { + let text = ''; + if (clients) { + for (let id in clients) { + text += (text ? ',' : '') + id; + } + } + + adapter.setState('info.connection', {val: text, ack: true}); + } + + function updateAlive(client, alive) { + let idAlive = adapter.namespace + '.' + client.iobId + '.alive'; + + adapter.getForeignState(idAlive, (err, state) => { + if (!state || state.val !== alive) { + adapter.setForeignState(idAlive, alive, true); + } + }); + } + + function sendState2Client(client, topic, state, qos, retain, cb) { + if (typeof qos === 'function') { + cb = qos; + qos = undefined; + } + if (typeof retain === 'function') { + cb = retain; + retain = undefined; + } + adapter.log.debug('Send to "' + client.id + '": ' + topic + ' = ' + state); + client.publish({topic: topic, payload: state, qos: qos, retain: retain, messageId: messageId++}, cb); + messageId &= 0xFFFFFFFF; + } + + function resendMessages2Client(client, messages, i) { + i = i || 0; + if (messages && i < messages.length) { + try { + messages[i].ts = Date.now(); + messages[i].count++; + adapter.log.debug(`Client [${client.id}] Resend messages on connect: ${messages[i].topic} = ${messages[i].payload}`); + if (messages[i].cmd === 'publish') { + client.publish(messages[i]); + } + } catch (e) { + adapter.log.warn(`Client [${client.id}] Cannot resend message: ${e}`); + } + + if (adapter.config.sendInterval) { + setTimeout(() => resendMessages2Client(client, messages, i + 1), adapter.config.sendInterval); + } else { + setImmediate(() => resendMessages2Client(client, messages, i + 1)); + } + } else { + //return; + } + } + + function addObject(attr, client, prefix, path) { + let replaceAttr = types[attr].replace || attr; + let id = adapter.namespace + '.' + client.iobId + '.' + (prefix ? prefix + '.' : '') + (path.length ? path.join('_') + '_' : '') + replaceAttr.replace(FORBIDDEN_CHARS, '_'); + let obj = { + type: 'addObject', + id: id, + data: { + _id: id, + common: Object.assign({}, types[attr]), + native: {}, + type: 'state' + } + }; + obj.data.common.name = client.id + ' ' + (prefix ? prefix + ' ' : '') + (path.length ? path.join(' ') + ' ' : '') + ' ' + replaceAttr; + return obj; + } + + function checkData(client, topic, prefix, data, unit, path) { + if (!data || typeof data !== 'object') return; + const ledModeReadColorsID = adapter.namespace + '.' + client.iobId + '.modeReadColors'; + if (cachedReadColors[ledModeReadColorsID] === undefined) { + return adapter.getForeignState(ledModeReadColorsID, (err, state) => { + cachedReadColors[ledModeReadColorsID] = (state && state.val) || false; + setImmediate(() => checkData(client, topic, prefix, data, unit, path)); + }); + } + + path = path || []; + prefix = prefix || ''; + + // first get the units + if (data.TempUnit) { + unit = data.TempUnit; + if (unit.indexOf('°') !== 0) { + unit = '°' + unit.replace('°'); + } + } + + for (let attr in data) { + if (!data.hasOwnProperty(attr)) { + adapter.log.warn('[' + client.id + '] attr error: ' + attr + '' + data[attr]); + continue; + } + + if (typeof data[attr] === 'object') { + // check for arrays + if (types[attr]) { + if (types[attr].type === 'array') { + // transform to array of attributes + for (let i = 1; i <= 10; i++) { + let val = data[attr][i - 1]; + if (typeof val === 'undefined') break; + // define new object + let replaceAttr = attr.replace(FORBIDDEN_CHARS, '_') + i.toString(); + let id = adapter.namespace + '.' + client.iobId + '.' + (prefix ? prefix + '.' : '') + (path.length ? path.join('_') + '_' : '') + replaceAttr; + let obj = { + type: 'addObject', + id: id, + data: { + _id: id, + common: Object.assign({}, types[attr]), + native: {}, + type: 'state' + } + }; + obj.data.common.name = client.id + ' ' + (prefix ? prefix + ' ' : '') + (path.length ? path.join(' ') + ' ' : '') + ' ' + replaceAttr; + obj.data.common.type = 'number'; + updateState(obj, val); + } + } else { + let nPath = Object.assign([], path); + nPath.push(attr.replace(FORBIDDEN_CHARS, '_')); + checkData(client, topic, prefix, data[attr], unit, nPath); + nPath = undefined; + } + } else { + let nPath = Object.assign([], path); + nPath.push(attr.replace(FORBIDDEN_CHARS, '_')); + checkData(client, topic, prefix, data[attr], unit, nPath); + nPath = undefined; + } + } else if (types[attr]) { + const allowReadColors = cachedReadColors[ledModeReadColorsID]; // allow for color read from MQTT (default off) + // create object + const obj = addObject(attr, client, prefix, path); + let replaceAttr = types[attr].replace || attr; + + if (attr === 'Temperature') { + obj.data.common.unit = unit || obj.data.common.unit || '°C'; + } + if (attr === 'Humidity') { + obj.data.common.unit = unit || obj.data.common.unit || '%'; + } + if (obj.data.common.storeMap) { + delete obj.data.common.storeMap; + client._map[replaceAttr] = topic.replace(/$\w+\//, 'cmnd/').replace(/\/\w+$/, '/' + replaceAttr); + } + + console.log(attr); + // adaptions for magichome tasmota + if (attr === 'Color') { + // read vars + // if ledFlags bit 2, read color from tasmota, else ignore + + if (data[attr].length === 10) { + obj.data.common.role = 'level.color.rgbcwww'; // RGB + cold white + white??? + } else if (data[attr].length === 8) { + obj.data.common.role = 'level.color.rgbww'; // RGB + White + } else { + obj.data.common.role = 'level.color.rgb'; + } + + if (hueCalc) { + // Create LEDs modes if required + let xObj = addObject('modeReadColors', client, prefix, path); + updateState(xObj); + + xObj = addObject('modeLedExor', client, prefix, path); + updateState(xObj); + + xObj = addObject('Hue', client, prefix, path); + updateState(xObj); + + xObj = addObject('Saturation', client, prefix, path); + updateState(xObj); + + xObj = addObject('Red', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(0, 2), 16) / 255 : undefined); + + xObj = addObject('Green', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(2, 4), 16) / 255 : undefined); + + xObj = addObject('Blue', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(4, 6), 16) / 255 : undefined); + + xObj = addObject('RGB_POWER', client, prefix, path); + xObj.data.common.read = allowReadColors; + let val = parseInt(data[attr].substring(0, 6), 16); + updateState(xObj, allowReadColors ? (val > 0) : undefined); + + if (obj.data.common.role === 'level.color.rgbww') { + // rgbww + xObj = addObject('WW', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(6, 8), 16) / 255 : undefined); + + xObj = addObject('WW_POWER', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? (val > 0) : undefined); + } else + if (obj.data.common.role === 'level.color.rgbcwww') { + // rgbcwww + xObj = addObject('CW', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(6, 8), 16) / 255 : undefined); + + xObj = addObject('CW_POWER', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? (val > 0) : undefined); + + xObj = addObject('WW', client, prefix, path); + xObj.data.common.read = allowReadColors; + updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(8, 10), 16) / 255 : undefined); + + xObj = addObject('WW_POWER', client, prefix, path); + xObj.data.common.read = allowReadColors; + val = parseInt(data[attr].substring(8, 10), 16); + updateState(xObj, allowReadColors ? (val > 0) : undefined); + } + } + } + + let val; + if (obj.data.common.type === 'number') { + val = parseFloat(data[attr]); + } else if (obj.data.common.type === 'boolean') { + val = (data[attr] || '').toUpperCase() === 'ON'; + } else { + if (attr === 'Color') { + // add # char + if (allowReadColors) { + val = '#' + data[attr]; + } + } else { + val = data[attr]; + } + } + updateState(obj, val); + } else { + // not in list, auto insert + //if (client.id=='DVES_008ADB') { + // adapter.log.warn('[' + client.id + '] Received attr not in list: ' + attr + '' + data[attr]); + //} + // tele/sonoff/SENSOR tele/sonoff/STATE => read only + // stat/sonoff/RESULT => read,write + + let parts = topic.split('/'); + // auto generate objects + if ((parts[0] === 'tele' && ( + (adapter.config.TELE_SENSOR && parts[2] === 'SENSOR') || + (adapter.config.TELE_STATE && parts[2] === 'STATE'))) || + (parts[0] === 'stat' && + (adapter.config.STAT_RESULT && parts[2] === 'RESULT')) + ) { + //adapter.log.info('[' + client.id + '] auto insert object: ' + attr + ' ' + data[attr] + ' flags: ' + autoFlags); + //if (data[attr]) { + const xdata = data[attr]; + //adapter.log.info('[' + client.id + '] auto insert object: ' + attr + ' ' + data[attr]); + let attributes; + if (parts[2] === 'RESULT') { + if (xdata.isNaN) { + // string + attributes = {type: 'string', role:'value', read: true, write: true}; + } else { + // number + attributes = {type: 'number', role:'value', read: true, write: true}; + } + } else { + if (xdata.isNaN) { + // string + attributes = {type: 'string', role: 'value', read: true, write: false}; + } else { + // number + attributes = {type: 'number', role: 'value', read: true, write: false}; + } + } + let replaceAttr = attr ; + let id = adapter.namespace + '.' + client.iobId + '.' + (prefix ? prefix + '.' : '') + (path.length ? path.join('_') + '_' : '') + replaceAttr; + let obj = { + type: 'addObject', + id: id, + data: { + _id: id, + common: Object.assign({}, attributes), + native: {}, + type: 'state' + } + }; + obj.data.common.name = client.id + ' ' + (prefix ? prefix + ' ' : '') + (path.length ? path.join(' ') + ' ' : '') + ' ' + replaceAttr; + updateState(obj, xdata); + } + } + } + } + + function receivedTopic(packet, client) { + client.states = client.states || {}; + client.states[packet.topic] = { + message: packet.payload, + retain: packet.retain, + qos: packet.qos + }; + + // update alive state + updateAlive(client, true); + + if (client._will && client._will.topic && packet.topic === client._will.topic) { + client._will.payload = packet.payload; + return; + } + + let val = packet.payload.toString('utf8'); + adapter.log.debug('[' + client.id + '] Received: ' + packet.topic + ' = ' + val); + + /* + adapter.getForeignState('system.adapter.sonoff.0.memRss', (err, state) => { + adapter.log.info('mem: ' + state.val + ' MB'); + if (heapTest==0 || (heapTest==1 && state.val>37.5)) { + heapTest+=1; + heapdump.writeSnapshot('/opt/iobroker/' + Date.now() + '.heapsnapshot'); + adapter.log.info('Wrote snapshot: '); + + var filename='/opt/iobroker/' + Date.now() + '.heapsnapshot'; + heapdump.writeSnapshot((err,filename) => { + if (err) { + adapter.log.info('heap: ' + err); + } else { + adapter.log.info('Wrote snapshot: ' + filename); + } + }); + + } + }); + */ + + // [DVES_BD3B4D] Received: tele/sonoff2/STATE = { + // "Time":"2017-10-01T12:37:18", + // "Uptime":0, + // "Vcc":3.224, + // "POWER":"ON", + // "POWER1":"OFF", + // "POWER2":"ON" + // "Wifi":{ + // "AP":1, + // "SSId":"FuckOff", + // "RSSI":62, + // "APMac":"E0:28:6D:EC:21:EA" + // } + // } + // [DVES_BD3B4D] Received: tele/sonoff2/SENSOR = { + // "Time":"2017-10-01T12:37:18", + // "Switch1":"ON", + // "DS18B20":{"Temperature":20.6}, + // "TempUnit":"C" + // } + client._map = client._map || {}; + + const parts = packet.topic.split('/'); + + if (!client._fallBackName) { + client._fallBackName = parts[1]; + } + + const stateId = parts.pop(); + + if (stateId === 'LWT') { + return; + } + + if (stateId === 'RESULT') { + // ignore: stat/Sonoff/RESULT = {"POWER":"ON"} + // testserver.js reports error, so reject above cmd + const str = val.replace(/\s+/g, ''); + if (str.startsWith('{"POWER":"ON"}')) return; + if (str.startsWith('{"POWER":"OFF"}')) return; + + if (parts[0] === 'stat') { + try { + checkData(client, packet.topic, NO_PREFIX, JSON.parse(val)); + } catch (e) { + adapter.log.warn('Cannot parse data "' + stateId + '": _' + val + '_ - ' + e); + } + return; + } + if (parts[0] === 'tele') { + try { + checkData(client, packet.topic, NO_PREFIX, JSON.parse(val)); + } catch (e) { + adapter.log.warn('Cannot parse data "' + stateId + '": _' + val + '_ - ' + e); + } + } + return; + } + + + // tele/sonoff_4ch/STATE = {"Time":"2017-10-02T19:26:06", "Uptime":0, "Vcc":3.226, "POWER1":"OFF", "POWER2":"OFF", "POWER3":"OFF", "POWER4":"OFF", "Wifi":{"AP":1, "SSId":"AAA", "RSSI": 15}} + // tele/sonoff/SENSOR = {"Time":"2017-10-05T17:43:19", "DS18x20":{"DS1":{"Type":"DS18B20", "Address":"28FF9A9876815022A", "Temperature":12.2}}, "TempUnit":"C"} + // tele/sonoff5/SENSOR = {"Time":"2017-10-03T14:02:25", "AM2301-14":{"Temperature":21.6, "Humidity":54.7}, "TempUnit":"C"} + // tele/sonoff/SENSOR = {"Time":"2018-02-23T17:36:59", "Analog0":298} + if (parts[0] === 'tele' && stateId.match(/^(STATE|SENSOR|WAKEUP)\d?$/)) { + try { + checkData(client, packet.topic, NO_PREFIX, JSON.parse(val)); + //adapter.log.warn('log sensor parse"' + stateId + '": _' + val); + } catch (e) { + adapter.log.warn('Cannot parse data "' + stateId + '": _' + val + '_ - ' + e); + } + } else if (parts[0] === 'tele' && stateId.match(/^INFO\d?$/)) { + // tele/SonoffPOW/INFO1 = {"Module":"Sonoff Pow", "Version":"5.8.0", "FallbackTopic":"SonoffPOW", "GroupTopic":"sonoffs"} + // tele/SonoffPOW/INFO2 = {"WebServerMode":"Admin", "Hostname":"Sonoffpow", "IPAddress":"192.168.2.182"} + // tele/SonoffPOW/INFO3 = {"RestartReason":"Software/System restart"} + try { + checkData(client, packet.topic, 'INFO', JSON.parse(val)); + } catch (e) { + adapter.log.warn('Cannot parse data"' + stateId + '": _' + val + '_ - ' + e); + } + } else if (parts[0] === 'tele' && stateId.match(/^(ENERGY)\d?$/)) { + // tele/sonoff_4ch/ENERGY = {"Time":"2017-10-02T19:24:32", "Total":1.753, "Yesterday":0.308, "Today":0.205, "Period":0, "Power":3, "Factor":0.12, "Voltage":221, "Current":0.097} + try { + checkData(client, packet.topic, 'ENERGY', JSON.parse(val)); + } catch (e) { + adapter.log.warn('Cannot parse data"' + stateId + '": _' + val + '_ - ' + e); + } + } else if (types[stateId]) { + // /ESP_BOX/BM280/Pressure = 1010.09 + // /ESP_BOX/BM280/Humidity = 42.39 + // /ESP_BOX/BM280/Temperature = 25.86 + // /ESP_BOX/BM280/Approx. Altitude = 24 + + // cmnd/sonoff/POWER + // stat/sonoff/POWER + + if (types[stateId]) { + let id = adapter.namespace + '.' + client.iobId + '.' + stateId.replace(/[-.+\s]+/g, '_'); + let obj = { + type: 'addObject', + id: id, + data: { + _id: id, + common: JSON.parse(JSON.stringify(types[stateId])), + native: {}, + type: 'state' + } + }; + obj.data.common.name = client.id + ' ' + stateId; + + // push only new objects + updateState(obj); + + if (parts[0] === 'cmnd') { + + // Set Object fix + if (obj.data.common.type === 'number') { + adapter.setState(id, parseFloat(val), true); + } else if (obj.data.common.type === 'boolean') { + adapter.setState(id, val === 'ON' || val === '1' || val === 'true' || val === 'on', true); + } else { + adapter.setState(id, val, true); + } + + // remember POWER topic + client._map[stateId] = packet.topic; + } else { + if (obj.data.common.type === 'number') { + adapter.setState(id, parseFloat(val), true); + } else if (obj.data.common.type === 'boolean') { + adapter.setState(id, val === 'ON' || val === '1' || val === 'true' || val === 'on', true); + } else { + adapter.setState(id, val, true); + } + } + } else { + adapter.log.debug('Cannot process: ' + packet.topic); + } + } + } + + function clientClose(client, reason) { + if (!client) return; + + if (persistentSessions[client.id]) { + persistentSessions[client.id].connected = false; + } + + if (client._resendonStart) { + clearTimeout(client._resendonStart); + client._resendonStart = null; + } + + try { + if (clients[client.id] && (client.__secret === clients[client.id].__secret)) { + adapter.log.info(`Client [${client.id}] connection closed: ${reason}`); + delete clients[client.id]; + updateClients(); + if (client._will) { + receivedTopic(client._will, client, () => client.destroy()); + } else { + client.destroy(); + } + } else { + client.destroy(); + } + } catch (e) { + adapter.log.warn(`Client [${client.id}] Cannot close client: ${e}`); + } + } + + function checkResends() { + const now = Date.now(); + resending = true; + for (const clientId in clients) { + if (clients.hasOwnProperty(clientId) && clients[clientId] && clients[clientId]._messages) { + for (let m = clients[clientId]._messages.length - 1; m >= 0; m--) { + const message = clients[clientId]._messages[m]; + if (now - message.ts >= adapter.config.retransmitInterval) { + if (message.count > adapter.config.retransmitCount) { + adapter.log.warn(`Client [${clientId}] Message ${message.messageId} deleted after ${message.count} retries`); + clients[clientId]._messages.splice(m, 1); + continue; + } + + // resend this message + message.count++; + message.ts = now; + try { + adapter.log.debug(`Client [${clientId}] Resend message topic: ${message.topic}, payload: ${message.payload}`); + if (message.cmd === 'publish') { + clients[clientId].publish(message); + } + } catch (e) { + adapter.log.warn(`Client [${clientId}] Cannot publish message: ${e}`); + } + + if (adapter.config.sendInterval) { + setTimeout(checkResends, adapter.config.sendInterval); + } else { + setImmediate(checkResends); + } + return; + } + } + } + } + + // delete old sessions + if (adapter.config.storeClientsTime !== -1) { + for (const id in persistentSessions) { + if (persistentSessions.hasOwnProperty(id)) { + if (now - persistentSessions[id].lastSeen > adapter.config.storeClientsTime * 60000) { + delete persistentSessions[id]; + } + } + } + } + + resending = false; + } + + (function _constructor(config) { + if (config.timeout === undefined) { + config.timeout = 300; + } else { + config.timeout = parseInt(config.timeout, 10); + } + + server.on('connection', stream => { + let client = mqtt(stream); + // Store unique connection identifier + client.__secret = Date.now() + '_' + Math.round(Math.random() * 10000); + + // client connected + client.on('connect', options => { + // acknowledge the connect packet + client.id = options.clientId; + client.iobId = client.id.replace(FORBIDDEN_CHARS, '_'); + mappingClients[client.iobId] = client.id; + + // get possible old client + let oldClient = clients[client.id]; + + if (config.user) { + if (config.user !== options.username || + config.pass !== options.password.toString()) { + adapter.log.warn(`Client [${client.id}] has invalid password(${options.password}) or username(${options.username})`); + client.connack({returnCode: 4}); + if (oldClient) { + // delete existing client + delete clients[client.id]; + updateAlive(oldClient, false); + updateClients(); + oldClient.destroy(); + } + client.destroy(); + return; + } + } + + if (oldClient) { + adapter.log.info(`Client [${client.id}] reconnected. Old secret ${clients[client.id].__secret}. New secret ${client.__secret}`); + // need to destroy the old client + + if (client.__secret !== clients[client.id].__secret) { + // it is another socket!! + + // It was following situation: + // - old connection was active + // - new connection is on the same TCP + // Just forget him + // oldClient.destroy(); + } + } else { + adapter.log.info(`Client [${client.id}] connected with secret ${client.__secret}`); + } + + let sessionPresent = false; + + if (!client.cleanSession && adapter.config.storeClientsTime !== 0) { + if (persistentSessions[client.id]) { + sessionPresent = true; + persistentSessions[client.id].lastSeen = Date.now(); + } else { + persistentSessions[client.id] = { + _subsID: {}, + _subs: {}, + messages: [], + lastSeen: Date.now() + }; + } + client._messages = persistentSessions[client.id].messages; + persistentSessions[client.id].connected = true; + } else if (client.cleanSession && persistentSessions[client.id]) { + delete persistentSessions[client.id]; + } + client._messages = client._messages || []; + + client.connack({returnCode: 0, sessionPresent}); + clients[client.id] = client; + updateClients(); + + client._will = options.will; + createClient(client); + + if (persistentSessions[client.id]) { + client._subsID = persistentSessions[client.id]._subsID; + client._subs = persistentSessions[client.id]._subs; + if (persistentSessions[client.id].messages.length) { + // give to the client a little bit time + client._resendonStart = setTimeout(clientId => { + client._resendonStart = null; + resendMessages2Client(client, persistentSessions[clientId].messages); + }, 100, client.id); + } + } + }); + + // timeout idle streams after 5 minutes + if (config.timeout) { + stream.setTimeout(config.timeout * 1000); + } + + // connection error handling + client.on('close', had_error => clientClose(client, had_error ? 'closed because of error' : 'closed')); + client.on('error', e => clientClose(client, e)); + client.on('disconnect', () => clientClose(client, 'disconnected')); + // stream timeout + stream.on('timeout', () => clientClose(client, 'timeout')); + + client.on('publish', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends publish. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + if (packet.qos === 1) { + // send PUBACK to client + client.puback({ + messageId: packet.messageId + }); + } else if (packet.qos === 2) { + const pack = client._messages.find(e => { + return e.messageId === packet.messageId; + }); + if (pack) { + // duplicate message => ignore + adapter.log.warn(`Client [${client.id}] Ignored duplicate message with ID: ${packet.messageId}`); + return; + } else { + packet.ts = Date.now(); + packet.cmd = 'pubrel'; + packet.count = 0; + client._messages.push(packet); + + client.pubrec({messageId: packet.messageId}); + return; + } + } + + + receivedTopic(packet, client) + }); + + // response for QoS2 + client.on('pubrec', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pubrec. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== -1) { + client.pubrel({ + messageId: packet.messageId + }); + } else { + adapter.log.warn(`Client [${client.id}] Received pubrec on ${client.id} for unknown messageId ${packet.messageId}`); + } + }); + + // response for QoS2 + client.on('pubcomp', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pubcomp. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== null) { + client._messages.splice(pos, 1); + } else { + adapter.log.warn(`Client [${client.id}] Received pubcomp for unknown message ID: ${packet.messageId}`); + } + }); + + // response for QoS2 + client.on('pubrel', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pubrel. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== -1) { + client.pubcomp({ + messageId: packet.messageId + }); + receivedTopic(client._messages[pos], client); + } else { + adapter.log.warn(`Client [${client.id}] Received pubrel on ${client.id} for unknown messageId ${packet.messageId}`); + } + }); + + // response for QoS1 + client.on('puback', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends puback. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + // remove this message from queue + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== null) { + adapter.log.debug(`Client [${client.id}] Received puback for ${client.id} message ID: ${packet.messageId}`); + client._messages.splice(pos, 1); + } else { + adapter.log.warn(`Client [${client.id}] Received puback for unknown message ID: ${packet.messageId}`); + } + }); + + client.on('unsubscribe', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends unsubscribe. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + client.unsuback({messageId: packet.messageId}); + }); + + client.on('subscribe', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends unsubscribe. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + let granted = []; + // just confirm the request. + // we expect subscribe for 'cmnd.sonoff.#' + for (let i = 0; i < packet.subscriptions.length; i++) { + granted.push(packet.subscriptions[i].qos); + } + + client.suback({granted: granted, messageId: packet.messageId}); + }); + + client.on('pingreq', (/*packet*/) => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pingreq. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + adapter.log.debug(`Client [${client.id}] pingreq`); + client.pingresp(); + }); + }); + + config.port = parseInt(config.port, 10) || 1883; + + config.retransmitInterval = config.retransmitInterval || 2000; + config.retransmitCount = config.retransmitCount || 10; + if (config.storeClientsTime === undefined) { + config.storeClientsTime = 1440; + } else { + config.storeClientsTime = parseInt(config.storeClientsTime, 10) || 0; + } + + config.defaultQoS = parseInt(config.defaultQoS, 10) || 0; + + // Update connection state + updateClients(); + + // to start + server.listen(config.port, config.bind, () => { + adapter.log.info('Starting MQTT ' + (config.user ? 'authenticated ' : '') + ' server on port ' + config.port); + + resendTimer = setInterval(() => { + if (!resending) { + checkResends(); + } + }, adapter.config.retransmitInterval || 2000); + }); + })(adapter.config); + + return this; +} + +module.exports = MQTTServer; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..e649d00 --- /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) { + 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/main.js b/main.js new file mode 100644 index 0000000..1b205a9 --- /dev/null +++ b/main.js @@ -0,0 +1,99 @@ +/* jshint -W097 */ +/* jshint strict:true */ +/* jslint node: true */ +/* jslint esversion: 6 */ + +/** + * + * ioBroker sonoff Adapter + * + * (c) 2017-2019 bluefox + * + * MIT License + * + */ +'use strict'; + +const utils = require(__dirname + '/lib/utils'); // Get common adapter utils +let adapter; +const adapterName = require('./package.json').name.split('.').pop(); +const Server = require(__dirname + '/lib/server'); +let server = null; + +function decrypt(key, value) { + let result = ''; + for (let i = 0; i < value.length; ++i) { + result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + return result; +} + +function startAdapter(options) { + options = options || {}; + Object.assign(options, {name: adapterName}); + + adapter = new utils.Adapter(options); + + adapter.on('ready', () => { + // it must be like this + + adapter.getForeignObject('system.config', (err, obj) => { + if (obj && obj.native && obj.native.secret) { + //noinspection JSUnresolvedVariable + adapter.config.pass = decrypt(obj.native.secret, adapter.config.pass); + } else { + //noinspection JSUnresolvedVariable + adapter.config.pass = decrypt('Zgfr56gFe87jJOM', adapter.config.pass); + } + main(); + }); + }); + + adapter.on('unload', cb => { + if (server) { + server.destroy(cb); + } else if (cb) { + cb(); + } + }); + + // is called if a subscribed state changes + adapter.on('stateChange', (id, state) => { + adapter.log.debug('stateChange ' + id + ': ' + JSON.stringify(state)); + // you can use the ack flag to detect if state is desired or acknowledged + if (state && !state.ack && server) { + server.onStateChange(id, state); + } + }); + return adapter; +} + +function main() { + adapter.config.TELE_SENSOR = adapter.config.TELE_SENSOR === true || adapter.config.TELE_SENSOR === 'true'; + adapter.config.TELE_STATE = adapter.config.TELE_STATE === true || adapter.config.TELE_STATE === 'true'; + adapter.config.STAT_RESULT = adapter.config.STAT_RESULT === true || adapter.config.STAT_RESULT === 'true'; + + // subscribe for all variables + adapter.subscribeStates('*'); + + // read all states and set alive to false + adapter.getStatesOf('', '', (err, states) => { + if (states && states.length) { + states.forEach(state => { + if (state._id.match(/\.alive$/)) { + adapter.setForeignState(state._id, false, true); + } + }); + } + }); + + server = new Server(adapter); +} + +// If started as allInOne/compact mode => return function to create instance +if (module && module.parent) { + module.exports = startAdapter; +} else { + // or start the instance directly + startAdapter(); +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..a4bf2d1 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "iobroker.sonoff", + "description": "The adapter allows to send and receive MQTT messages from ioBroker and to be a broker.", + "version": "2.2.3", + "author": "bluefox ", + "homepage": "https://github.com/ioBroker/ioBroker.sonoff", + "repository": { + "type": "git", + "url": "https://github.com/ioBroker/ioBroker.sonoff" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/ioBroker/ioBroker.sonoff/blob/master/LICENSE" + } + ], + "keywords": [ + "iobroker", + "sonoff", + "MQTT", + "tasmota", + "esp" + ], + "devDependencies ": { + "mqtt": "^2.18.1" + }, + "dependencies": { + "mqtt-connection": "^4.0.0" + }, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^5.2.0", + "chai": "^4.1.2" + }, + "bugs": { + "url": "https://github.com/ioBroker/ioBroker.sonoff/issues" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "engines": { + "node": ">=4.0.0" + }, + "license": "MIT" +} diff --git a/test/lib/mqttClient.js b/test/lib/mqttClient.js new file mode 100644 index 0000000..419cf77 --- /dev/null +++ b/test/lib/mqttClient.js @@ -0,0 +1,94 @@ +'use strict'; +const mqtt = require('mqtt'); + +function Client(cbConnected, cbChanged, config) { + let that = this; + if (typeof config === 'string') config = {name: config}; + config = config || {}; + config.url = config.url || 'localhost'; + this.client = mqtt.connect('mqtt://' + (config.user ? (config.user + ':' + config.pass + '@') : '') + config.url + (config.name ? '?clientId=' + config.name : ''), config); + + this.client.on('connect', () => { + console.log((new Date()) + ' test client connected to localhost'); + + /*that.client.publish('mqtt/0/test', 'Roger1'); + client.publish('test/out/testMessage1', 'Roger1'); + client.publish('test/out/testMessage2', 'Roger2'); + client.publish('test/in/testMessage3', 'Roger3'); + client.publish('test/in/testMessage4', 'Roger4');*/ + + /*client.publish('arduino/kitchen/out/temperature', '10.1'); + client.publish('arduino/kitchen/out/humidity', '56'); + // Current light state + client.publish('arduino/kitchen/in/lightActor', 'false'); + + client.subscribe('arduino/kitchen/in/#');*/ + //client.subscribe('arduino/kitchen/in/updateInterval'); + that.client.subscribe('#'); + if (cbConnected) cbConnected(true); + }); + + this.client.on('message', (topic, message, packet) => { + // message is Buffer + if (cbChanged) { + cbChanged(topic, message, packet); + } else { + console.log('Test MQTT Client received "' + topic + '": ' + message.toString()); + } + }); + this.client.on('close', err => { + if (err) console.error('Connection closed: ' + err); + // message is Buffer + if (cbConnected) { + cbConnected(false); + } else { + console.log('Test MQTT Client closed'); + } + }); + + this.client.on('error', error => { + console.error('Test MQTT Client error: ' + error); + }); + + this.publish = (topic, message, qos, retain, cb) => { + if (typeof qos === 'function') { + cb = qos; + qos = undefined; + } + if (typeof retain === 'function') { + cb = retain; + retain = undefined; + } + const opts = { + retain: retain || false, + qos: qos || 0 + }; + that.client.publish(topic, message, opts, cb); + }; + this.subscribe = (topic, opts, cb) => { + if (typeof opts === 'function') { + cb = opts; + opts = null; + } + that.client.subscribe(topic, opts, cb); + }; + this.unsubscribe = (topic, cb) => { + that.client.unsubscribe(topic, cb); + }; + this.destroy = () => { + if (that.client) { + that.client.end(); + that.client = null; + } + }; + + this.stop = this.destroy; + + return this; +} + +if (typeof module !== 'undefined' && module.parent) { + module.exports = Client; +} else { + new Client(); +} diff --git a/test/lib/objects.js b/test/lib/objects.js new file mode 100644 index 0000000..273a0a7 --- /dev/null +++ b/test/lib/objects.js @@ -0,0 +1,1411 @@ +'use strict'; +const path = require('path'); +const rootDir = path.normalize(__dirname + '/../../'); +let adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; + +const logger = { + info: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } +}; + +function Objects(cb) { + if (!(this instanceof Objects)) return new Objects(cb); + + const _Objects = require(rootDir + 'tmp/node_modules/iobroker.js-controller/lib/objects'); + this.connected = false; + const that = this; + + that.namespace = 'test'; + + this.objects = new _Objects({ + connection: { + type : 'file', + host : '127.0.0.1', + port : 19001, + user : '', + pass : '', + noFileCache: false, + connectTimeout: 2000 + }, + logger: logger, + connected: () => { + this.connected = true; + if (typeof cb === 'function') cb(); + }, + disconnected: () => { + this.connected = false; + }, + change: (id, obj) => { + if (!id) { + logger.error(that.namespace + ' change ID is empty: ' + JSON.stringify(obj)); + return; + } + + if (id.slice(that.namespace.length) === that.namespace) { + if (typeof options.objectChange === 'function') options.objectChange(id.slice(that.namespace.length + 1), obj); + + // emit 'objectChange' event instantly + setImmediate(() => that.emit('objectChange', id.slice(that.namespace.length + 1), obj)); + } else { + if (typeof options.objectChange === 'function') options.objectChange(id, obj); + + // emit 'objectChange' event instantly + setImmediate(() => that.emit('objectChange', id, obj)); + } + }, + connectTimeout: error => { + if (logger) logger.error(that.namespace + ' no connection to objects DB'); + if (typeof cb === 'function') cb('Connect timeout'); + } + }); + + that._namespaceRegExp = new RegExp('^' + that.namespace); // cache the regex object 'adapter.0' + + that._fixId = function _fixId(id) { + let result = ''; + // If id is an object + if (typeof id === 'object') { + // Add namespace + device + channel + result = that.namespace + '.' + (id.device ? id.device + '.' : '') + (id.channel ? id.channel + '.' : '') + id.state; + } else { + result = id; + if (!that._namespaceRegExp.test(id)) result = that.namespace + '.' + id; + } + return result; + }; + + that.setObject = function setObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!id) { + logger.error(that.namespace + ' setObject id missing!!'); + return; + } + + if (!obj) { + logger.error(that.namespace + ' setObject ' + id + ' object missing!'); + return; + } + + if (obj.hasOwnProperty('type')) { + if (!obj.hasOwnProperty('native')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property native missing!'); + obj.native = {}; + } + // Check property 'common' + if (!obj.hasOwnProperty('common')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common missing!'); + obj.common = {}; + } else if (obj.type === 'state') { + // Try to extend the model for type='state' + // Check property 'role' by 'state' + if (obj.common.hasOwnProperty('role') && defaultObjs[obj.common.role]) { + obj.common = extend(true, defaultObjs[obj.common.role], obj.common); + } else if (!obj.common.hasOwnProperty('role')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.role missing!'); + } + if (!obj.common.hasOwnProperty('type')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.type missing!'); + } + } + + if (!obj.common.hasOwnProperty('name')) { + obj.common.name = id; + logger.debug(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.name missing, using id as name'); + } + + id = that._fixId(id, obj.type); + + if (obj.children || obj.parent) { + logger.warn('Do not use parent or children for ' + id); + } + that.objects.setObject(id, obj, options, callback); + + } else { + logger.error(that.namespace + ' setObject ' + id + ' mandatory property type missing!'); + } + }; + + that.extendObject = function extendObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + id = that._fixId(id, obj.type); + + if (obj.children || obj.parent) { + logger.warn('Do not use parent or children for ' + id); + } + // delete arrays if they should be changed + if (obj && ( + (obj.common && obj.common.members) || + (obj.native && obj.native.repositories) || + (obj.native && obj.native.certificates) || + (obj.native && obj.native.devices)) + ) { + // Read whole object + that.objects.getObject(id, options, (err, oldObj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!oldObj) { + logger.error('Object ' + id + ' not exist!'); + oldObj = {}; + } + if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { + oldObj.native.repositories = []; + } + if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { + oldObj.common.members = []; + } + if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { + oldObj.native.certificates = []; + } + if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { + oldObj.native.devices = []; + } + obj = extend(true, oldObj, obj); + + that.objects.setObject(id, obj, options, callback); + }); + } else { + that.objects.extendObject(id, obj, options, callback); + } + }; + + that.setForeignObject = function setForeignObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.setObject(id, obj, options, callback); + }; + + that.extendForeignObject = function extendForeignObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + // delete arrays if they should be changed + if (obj && ((obj.native && (obj.native.repositories || obj.native.certificates || obj.native.devices)) || + (obj.common && obj.common.members))) { + // Read whole object + that.objects.getObject(id, options, (err, oldObj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!oldObj) { + logger.error('Object ' + id + ' not exist!'); + oldObj = {}; + } + if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { + oldObj.native.repositories = []; + } + if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { + oldObj.common.members = []; + } + if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { + oldObj.native.certificates = []; + } + if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { + oldObj.native.devices = []; + } + obj = extend(true, oldObj, obj); + + that.objects.setObject(id, obj, callback); + }); + } else { + that.objects.extendObject(id, obj, options, callback); + } + }; + + that.getObject = function getObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(that._fixId(id), options, callback); + }; + + // Get the enum tree + that.getEnum = function getEnum(_enum, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!_enum.match('^enum.')) _enum = 'enum.' + _enum; + const result = {}; + + that.objects.getObjectView('system', 'enum', {startkey: _enum + '.', endkey: _enum + '.\u9999'}, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + // Read all + let count = 0; + + for (let t = 0; t < res.rows.length; t++) { + count++; + that.objects.getObject(res.rows[t].id, options, (err, _obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + + if (!err && _obj) result[_obj._id] = _obj; + if (!--count && callback) callback (err, result, _enum); + }); + } + if (!count && callback) callback(err, result); + }); + }; + + // read for given enums the members of them + that.getEnums = function getEnums(_enumList, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + const _enums = {}; + if (_enumList) { + if (typeof _enumList === 'string') _enumList = [_enumList]; + let count = 0; + for (let t = 0; t < _enumList.length; t++) { + count++; + that.getEnum(_enumList[t], options, (list, _enum) => { + _enums[_enum] = list; + if (!--count && callback) callback(_enums); + }); + } + } else { + // Read all enums + that.objects.getObjectView('system', 'enum', {startkey: 'enum.', endkey: 'enum.\u9999'}, options, (err, res) => { + if (err) { + callback(err); + return; + } + const result = {}; + for (let i = 0; i < res.rows.length; i++) { + const parts = res.rows[i].id.split('.', 3); + if (!parts[2]) continue; + if (!result[parts[0] + '.' + parts[1]]) result[parts[0] + '.' + parts[1]] = {}; + result[parts[0] + '.' + parts[1]][res.rows[i].id] = res.rows[i].value; + } + + if (callback) callback(err, result); + }); + } + }; + + that.getForeignObjects = function getForeignObjects(pattern, type, enums, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + let params = {}; + if (pattern && pattern !== '*') { + params = { + startkey: pattern.replace('*', ''), + endkey: pattern.replace('*', '\u9999') + }; + } + if (typeof enums === 'function') { + callback = enums; + enums = null; + } + if (typeof type === 'function') { + callback = type; + type = null; + } + that.objects.getObjectView('system', type || 'state', params, options, (err, res) => { + if (err) { + callback(err); + return; + } + + that.getEnums(enums, (_enums) => { + const list = {}; + for (let i = 0; i < res.rows.length; i++) { + list[res.rows[i].id] = res.rows[i].value; + + if (_enums) { + // get device or channel of this state and check it too + const parts = res.rows[i].id.split('.'); + parts.splice(parts.length - 1, 1); + const channel = parts.join('.'); + parts.splice(parts.length - 1, 1); + const device = parts.join('.'); + + list[res.rows[i].id].enums = {}; + for (const es in _enums) { + if (!_enums.hasOwnProperty(es)) continue; + for (const e in _enums[es]) { + if (!_enums[es].hasOwnProperty(e)) continue; + if (!_enums[es][e] || !_enums[es][e].common || !_enums[es][e].common.members) continue; + if (_enums[es][e].common.members.indexOf(res.rows[i].id) !== -1 || + _enums[es][e].common.members.indexOf(channel) !== -1 || + _enums[es][e].common.members.indexOf(device) !== -1) { + list[res.rows[i].id].enums[e] = _enums[es][e].common.name; + } + } + } + } + } + callback(null, list); + }); + }); + + }; + + that.findForeignObject = function findForeignObject(id, type, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.findObject(id, type, options, callback); + }; + + that.getForeignObject = function getForeignObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(id, options, callback); + }; + + that.delObject = function delObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + id = that._fixId(id); + that.objects.delObject(id, options, callback); + }; + + that.delForeignObject = function delForeignObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.delObject(id, options, callback); + }; + + that.subscribeObjects = function subscribeObjects(pattern, options) { + if (pattern === '*') { + that.objects.subscribe(that.namespace + '.*'); + } else { + pattern = that._fixId(pattern); + that.objects.subscribe(pattern, options); + } + }; + + that.subscribeForeignObjects = function subscribeObjects(pattern, options) { + that.objects.subscribe(pattern, options); + }; + + that.unsubscribeForeignObjects = function unsubscribeForeignObjects(pattern, options) { + if (!pattern) pattern = '*'; + that.objects.unsubscribe(pattern, options); + }; + + that.unsubscribeObjects = function unsubscribeObjects(pattern, options) { + if (pattern === '*') { + that.objects.unsubscribe(that.namespace + '.*', options); + } else { + pattern = that._fixId(pattern); + that.objects.unsubscribe(pattern); + } + }; + + that.setObjectNotExists = function setObjectNotExists(id, object, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + id = that._fixId(id); + + if (object.children || object.parent) { + logger.warn('Do not use parent or children for ' + id); + } + + that.objects.getObject(id, options, (err, obj) => { + if (!obj) { + that.objects.setObject(id, object, callback); + } + }); + }; + + that.setForeignObjectNotExists = function setForeignObjectNotExists(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(id, options, (err, obj) => { + if (!obj) { + that.objects.setObject(id, obj, callback); + } + }); + }; + + that._DCS2ID = function (device, channel, stateOrPoint) { + let id = ''; + if (device) id += device; + if (channel) id += ((id) ? '.' : '') + channel; + + if (stateOrPoint !== true && stateOrPoint !== false) { + if (stateOrPoint) id += ((id) ? '.' : '') + stateOrPoint; + } else if (stateOrPoint === true) { + if (id) id += '.'; + } + return id; + }; + + that.createDevice = function createDevice(deviceName, common, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!deviceName) { + that.log.error('Try to create device with empty name!'); + return; + } + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + if (typeof common === 'function') { + callback = common; + common = {}; + } + common = common || {}; + common.name = common.name || deviceName; + + deviceName = deviceName.replace(/[.\s]+/g, '_'); + _native = _native || {}; + + that.setObjectNotExists(deviceName, { + 'type': 'device', + 'common': common, + 'native': _native + }, options, callback); + }; + + // name of channel must be in format 'channel' + that.createChannel = function createChannel(parentDevice, channelName, roleOrCommon, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!channelName) throw 'Try to create channel without name!'; + + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + + if (typeof roleOrCommon === 'function') { + callback = roleOrCommon; + roleOrCommon = undefined; + } + + let common = {}; + if (typeof roleOrCommon === 'string') { + common = { + role: roleOrCommon + }; + } else if (typeof roleOrCommon === 'object') { + common = roleOrCommon; + } + common.name = common.name || channelName; + + if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + channelName = channelName.replace(/[.\s]+/g, '_'); + channelName = that._DCS2ID(parentDevice, channelName); + + _native = _native || {}; + + const obj = { + 'type': 'channel', + 'common': common, + 'native': _native + }; + + that.setObject(channelName, obj, options, callback); + }; + + that.createState = function createState(parentDevice, parentChannel, stateName, roleOrCommon, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!stateName) throw 'Empty name is not allowed!'; + + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + + if (typeof roleOrCommon === 'function') { + callback = roleOrCommon; + roleOrCommon = undefined; + } + + let common = {}; + if (typeof roleOrCommon === 'string') { + common = { + role: roleOrCommon + }; + } else if (typeof roleOrCommon === 'object') { + common = roleOrCommon; + } + + common.name = common.name || stateName; + _native = _native || {}; + + common.read = (common.read === undefined) ? true : common.read; + common.write = (common.write === undefined) ? false : common.write; + + if (!common.role) { + logger.error('Try to create state ' + (parentDevice ? (parentDevice + '.') : '') + parentChannel + '.' + stateName + ' without role'); + return; + } + + if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + if (parentChannel) parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + stateName = stateName.replace(/[.\s]+/g, '_'); + const id = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}); + + that.setObjectNotExists(id, { + type: 'state', + common: common, + native: _native + }, options, callback); + + if (common.def !== undefined) { + that.setState(id, common.def, options); + } else { + that.setState(id, null, false, options); + } + }; + + that.deleteDevice = function deleteDevice(deviceName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + deviceName = deviceName.replace(/[.\s]+/g, '_'); + if (!that._namespaceRegExp.test(deviceName)) deviceName = that.namespace + '.' + deviceName; + + that.objects.getObjectView('system', 'device', {startkey: deviceName, endkey: deviceName}, options, (err, res) => { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + let cnt = 0; + if (res.rows.length > 1) that.log.warn('Found more than one device ' + deviceName); + + for (let t = 0; t < res.rows.length; t++) { + cnt++; + that.delObject(res.rows[t].id, options, err => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + + if (!--cnt) { + cnt = 0; // just to better understand + that.objects.getObjectView('system', 'channel', {startkey: deviceName + '.', endkey: deviceName + '.\u9999'}, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + for (let k = 0; k < res.rows.length; k++) { + cnt++; + that.deleteChannel(deviceName, res.rows[k].id, options, err => { + if (!(--cnt) && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + if (!cnt && callback) callback(); + }); + } + }); + } + if (!cnt && callback) callback(); + }); + }; + + that.addChannelToEnum = function addChannelToEnum(enumName, addTo, parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(channelName)) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName.replace(/[.\s]+/g, '_'); + + const objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + if (addTo.match(/^enum\./)) { + that.objects.getObject(addTo, options, (err, obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!err && obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, options, err => { + if (callback) callback(err); + }); + } + } + }); + } else { + if (enumName.match(/^enum\./)) enumName = enumName.substring(5); + + that.objects.getObject('enum.' + enumName + '.' + addTo, options, (err, obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + if (obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, options, callback); + } else { + if (callback) callback(); + } + } else { + // Create enum + that.objects.setObject('enum.' + enumName + '.' + addTo, { + common: { + name: addTo, + members: [objId] + }, + type: 'enum' + }, options, callback); + } + }); + } + }; + + that.deleteChannelFromEnum = function deleteChannelFromEnum(enumName, parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (parentDevice.substring(0, that.namespace.length) === that.namespace) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (channelName && channelName.substring(0, that.namespace.length) === that.namespace) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName || ''; + channelName = channelName.replace(/[.\s]+/g, '_'); + + const objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + if (enumName) { + enumName = 'enum.' + enumName + '.'; + } else { + enumName = 'enum.'; + } + + that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (res) { + let count = 0; + for (let i = 0; i < res.rows.length; i++) { + count++; + that.objects.getObject(res.rows[i].id, options, (err, obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + if (!err && obj && obj.common && obj.common.members) { + const pos = obj.common.members.indexOf(objId); + if (pos !== -1) { + obj.common.members.splice(pos, 1); + count++; + that.objects.setObject(obj._id, obj, options, err => { + if (!(--count) && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + } + count--; + if (!count && callback) callback(err); + }); + } + } else if (callback) { + callback (err); + } + }); + }; + + that.deleteChannel = function deleteChannel(parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof channelName === 'function') { + callback = channelName; + channelName = parentDevice; + parentDevice = ''; + } + if (parentDevice && !channelName) { + channelName = parentDevice; + parentDevice = ''; + } else if (parentDevice && typeof channelName === 'function') { + callback = channelName; + channelName = parentDevice; + parentDevice = ''; + } + if (!parentDevice) parentDevice = ''; + that.deleteChannelFromEnum('', parentDevice, channelName); + const _parentDevice = parentDevice; + const _channelName = channelName; + + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (channelName && that._namespaceRegExp.test(channelName)) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName || ''; + channelName = channelName.replace(/[.\s]+/g, '_'); + + channelName = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + logger.info('Delete channel ' + channelName); + + that.objects.getObjectView('system', 'channel', {startkey: channelName, endkey: channelName}, options, (err, res) => { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + let cnt = 0; + if (res.rows.length > 1) { + that.log.warn('Found more than one channel ' + channelName); + } + + for (let t = 0; t < res.rows.length; t++) { + cnt++; + that.delObject(res.rows[t].id, options, err => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + cnt--; + if (!cnt) { + that.objects.getObjectView('system', 'state', {startkey: channelName + '.', endkey: channelName + '.\u9999'}, options, (err, res) => { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + for (let k = 0; k < res.rows.length; k++) { + that.deleteState(_parentDevice, _channelName, res.rows[k].id, options, err => { + if (!(--cnt) && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + if (!cnt && callback) callback(); + }); + } + }); + } + if (!cnt && callback) callback(); + }); + }; + + that.deleteState = function deleteState(parentDevice, parentChannel, stateName, options, callback) { + if (typeof parentChannel === 'function' && stateName === undefined) { + stateName = parentDevice; + callback = parentChannel; + parentChannel = ''; + parentDevice = ''; + } else + if (parentChannel === undefined && stateName === undefined) { + stateName = parentDevice; + parentDevice = ''; + parentChannel = ''; + } else { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof stateName === 'function') { + callback = stateName; + stateName = parentChannel; + parentChannel = parentDevice; + parentDevice = ''; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + stateName = parentDevice; + parentChannel = ''; + parentDevice = ''; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + stateName = parentDevice; + parentChannel = ''; + parentDevice = ''; + } + } + + that.deleteStateFromEnum('', parentDevice, parentChannel, stateName, options); + + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName || ''; + stateName = stateName.replace(/[.\s]+/g, '_'); + + const _name = that._DCS2ID(parentDevice, parentChannel, stateName); + that.delState(_name, options, function () { + that.delObject(_name, options, callback); + }); + }; + + that.getDevices = function getDevices(callback, options) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObjectView('system', 'device', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999'}, options, (err, obj) => { + if (callback) { + if (obj.rows.length) { + const res = []; + for (let i = 0; i < obj.rows.length; i++) { + res.push(obj.rows[i].value); + } + callback(null, res); + } else { + callback(err, []); + } + } + }); + }; + + that.getChannelsOf = function getChannelsOf(parentDevice, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof parentDevice === 'function') { + callback = parentDevice; + parentDevice = null; + } + if (!parentDevice) parentDevice = ''; + + if (parentDevice && that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + parentDevice = that.namespace + (parentDevice ? ('.' + parentDevice) : ''); + that.objects.getObjectView('system', 'channel', {startkey: parentDevice + '.', endkey: parentDevice + '.\u9999'}, options, (err, obj) => { + if (callback) { + if (obj.rows.length) { + const res = []; + for (let i = 0; i < obj.rows.length; i++) { + res.push(obj.rows[i].value); + } + callback(null, res); + } else { + callback(err, []); + } + } + }); + }; + + that.getChannels = that.getChannelsOf; + + that.getStatesOf = function getStatesOf(parentDevice, parentChannel, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof parentDevice === 'function') { + callback = parentDevice; + parentDevice = null; + parentChannel = null; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + parentChannel = null; + } + + if (!parentDevice) { + parentDevice = ''; + } else { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (!parentChannel) { + parentChannel = ''; + } else if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + + if (parentDevice && parentChannel && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + + const id = that.namespace + '.' + that._DCS2ID(parentDevice, parentChannel, true); + + that.objects.getObjectView('system', 'state', {startkey: id, endkey: id + '\u9999'}, options, (err, obj) => { + if (callback) { + const res = []; + if (obj.rows.length) { + let read = 0; + for (let i = 0; i < obj.rows.length; i++) { + read++; + that.objects.getObject(obj.rows[i].id, (err, subObj) => { + if (subObj) res.push(subObj); + + if (!--read) callback(null, res); + }); + } + } else { + callback(null, res); + } + } + }); + }; + + that.addStateToEnum = function addStateToEnum(enumName, addTo, parentDevice, parentChannel, stateName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName.replace(/[.\s]+/g, '_'); + + const objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}); + + if (addTo.match(/^enum\./)) { + that.objects.getObject(addTo, options, (err, obj) => { + if (!err && obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, options, err => { + if (callback) callback(err); + }); + } + } + }); + } else { + if (enumName.match(/^enum\./)) enumName = enumName.substring(5); + + that.objects.getObject('enum.' + enumName + '.' + addTo, options, (err, obj) => { + if (!err && obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, callback); + } else { + if (callback) callback(); + } + } else { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + // Create enum + that.objects.setObject('enum.' + enumName + '.' + addTo, { + common: { + name: addTo, + members: [objId] + }, + type: 'enum' + }, options, callback); + } + }); + } + }; + + that.deleteStateFromEnum = function deleteStateFromEnum(enumName, parentDevice, parentChannel, stateName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName.replace(/[.\s]+/g, '_'); + + const objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}, 'state'); + + if (enumName) { + enumName = 'enum.' + enumName + '.'; + } else { + enumName = 'enum.'; + } + + that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, (err, res) => { + if (!err && res) { + let count = 0; + for (let i = 0; i < res.rows.length; i++) { + count++; + that.objects.getObject(res.rows[i].id, options, (err, obj) => { + if (err) { + if (callback) callback(err); + callback = null; + return; + } + + if (!err && obj && obj.common && obj.common.members) { + const pos = obj.common.members.indexOf(objId); + if (pos !== -1) { + obj.common.members.splice(pos, 1); + count++; + that.objects.setObject(obj._id, obj, err => { + if (!--count && callback) callback(err); + }); + } + } + if (!--count && callback) callback(err); + }); + } + } else if (callback) { + callback (err); + } + }); + }; + + that.chmodFile = function readDir(adapter, path, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.chmodFile(adapter, path, options, callback); + }; + + that.readDir = function readDir(adapter, path, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.readDir(adapter, path, options, callback); + }; + + that.unlink = function unlink(adapter, name, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.unlink(adapter, name, options, callback); + }; + + that.rename = function rename(adapter, oldName, newName, options, callback) { + if (adapter === null) adapter = that.name; + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.rename(adapter, oldName, newName, options, callback); + }; + + that.mkdir = function mkdir(adapter, dirname, options, callback) { + if (adapter === null) adapter = that.name; + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.mkdir(adapter, dirname, options, callback); + }; + + that.readFile = function readFile(adapter, filename, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.readFile(adapter, filename, options, callback); + }; + + that.writeFile = function writeFile(adapter, filename, data, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.writeFile(adapter, filename, data, options, callback); + }; + + that.formatDate = function formatDate(dateObj, isSeconds, _format) { + if (typeof isSeconds !== 'boolean') { + _format = isSeconds; + isSeconds = false; + } + + let format = _format || that.dateFormat || 'DD.MM.YYYY'; + + if (!dateObj) return ''; + const text = typeof dateObj; + if (text === 'string') { + const pos = dateObj.indexOf('.'); + if (pos !== -1) dateObj = dateObj.substring(0, pos); + return dateObj; + } + if (text !== 'object') dateObj = isSeconds ? new Date(dateObj * 1000) : new Date(dateObj); + + let v; + + // Year + if (format.indexOf('YYYY') !== -1 || format.indexOf('JJJJ') !== -1 || format.indexOf('ГГГГ') !== -1) { + v = dateObj.getFullYear(); + format = format.replace('YYYY', v); + format = format.replace('JJJJ', v); + format = format.replace('ГГГГ', v); + } else if (format.indexOf('YY') !== -1 || format.indexOf('JJ') !== -1 || format.indexOf('ГГ') !== -1) { + v = dateObj.getFullYear() % 100; + format = format.replace('YY', v); + format = format.replace('JJ', v); + format = format.replace('ГГ', v); + } + // Month + if (format.indexOf('MM') !== -1 || format.indexOf('ММ') !== -1) { + v = dateObj.getMonth() + 1; + if (v < 10) v = '0' + v; + format = format.replace('MM', v); + format = format.replace('ММ', v); + } else if (format.indexOf('M') !== -1 || format.indexOf('М') !== -1) { + v = dateObj.getMonth() + 1; + format = format.replace('M', v); + format = format.replace('М', v); + } + + // Day + if (format.indexOf('DD') !== -1 || format.indexOf('TT') !== -1 || format.indexOf('ДД') !== -1) { + v = dateObj.getDate(); + if (v < 10) v = '0' + v; + format = format.replace('DD', v); + format = format.replace('TT', v); + format = format.replace('ДД', v); + } else if (format.indexOf('D') !== -1 || format.indexOf('TT') !== -1 || format.indexOf('Д') !== -1) { + v = dateObj.getDate(); + format = format.replace('D', v); + format = format.replace('T', v); + format = format.replace('Д', v); + } + + // hours + if (format.indexOf('hh') !== -1 || format.indexOf('SS') !== -1 || format.indexOf('чч') !== -1) { + v = dateObj.getHours(); + if (v < 10) v = '0' + v; + format = format.replace('hh', v); + format = format.replace('SS', v); + format = format.replace('чч', v); + } else if (format.indexOf('h') !== -1 || format.indexOf('S') !== -1 || format.indexOf('ч') !== -1) { + v = dateObj.getHours(); + format = format.replace('h', v); + format = format.replace('S', v); + format = format.replace('ч', v); + } + + // minutes + if (format.indexOf('mm') !== -1 || format.indexOf('мм') !== -1) { + v = dateObj.getMinutes(); + if (v < 10) v = '0' + v; + format = format.replace('mm', v); + format = format.replace('мм', v); + } else if (format.indexOf('m') !== -1 || format.indexOf('м') !== -1) { + v = dateObj.getMinutes(); + format = format.replace('m', v); + format = format.replace('v', v); + } + + // seconds + if (format.indexOf('ss') !== -1 || format.indexOf('сс') !== -1) { + v = dateObj.getSeconds(); + if (v < 10) v = '0' + v; + v = v.toString(); + format = format.replace('ss', v); + format = format.replace('cc', v); + } else if (format.indexOf('s') !== -1 || format.indexOf('с') !== -1) { + v = dateObj.getHours().toString(); + format = format.replace('s', v); + format = format.replace('с', v); + } + return format; + }; + + return this; +} + +module.exports = Objects; \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..69be60b --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,718 @@ +'use strict'; +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +const fs = require('fs'); +const path = require('path'); +const child_process = require('child_process'); +const rootDir = path.normalize(__dirname + '/../../'); +const pkg = require(rootDir + 'package.json'); +const debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +let adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +let adapterStarted = false; + +function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +const appName = getAppName().toLowerCase(); + +let objects; +let states; + +let pid = null; + +function copyFileSync(source, target) { + + let 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) { + let files = []; + + let base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + const 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; + } + + const curSource = path.join(source, file); + const 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...'); + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + let f = fs.readFileSync(dataDir + 'objects.json'); + const 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...'); + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + let 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; + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + const f = fs.readFileSync(dataDir + 'objects.json'); + const objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(() => 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(() => checkIsAdapterInstalled(cb, counter + 1), 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + const f = fs.readFileSync(dataDir + 'objects.json'); + const objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(() => 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(() => 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...'); + const 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(error => { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + const _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, () => { + checkIsAdapterInstalled(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', (code, signal) => { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', (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...'); + let _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, () => { + // 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...'); + let __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, () => { + checkIsControllerInstalled(() => { + // change ports for object and state DBs + const 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(() => { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + const client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', () => { + 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(() => { + 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...'); + 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(() => { + let _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, () => { + // change ports for object and state DBs + const 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(() => { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setImmediate(() => { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }); + } +} + +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() { + const dirPath = rootDir + 'tmp/log'; + let 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 (let i = 0; i < files.length; i++) { + const filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + const dirPath = rootDir + 'tmp/iobroker-data/sqlite'; + let 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 (let i = 0; i < files.length; i++) { + const filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(isInited => { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + let 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; + let isObjectConnected; + let isStatesConnected; + + const 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: () => { + 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 + const 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: () => { + 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) { + setImmediate(() => cb(false)); + } + } else { + adapterStarted = false; + pid.on('exit', (code, signal) => { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', (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) { + let timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(() => { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(() => { + 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) { + const objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + const 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) { + const objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + const 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/lib/states.js b/test/lib/states.js new file mode 100644 index 0000000..5ccb11e --- /dev/null +++ b/test/lib/states.js @@ -0,0 +1,708 @@ +'use strict'; +const path = require('path'); +const rootDir = path.normalize(__dirname + '/../../'); +let adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; + +const logger = { + info: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } +}; + +function States(cb, stateChange) { + const that = this; + const _States = require(rootDir + 'tmp/node_modules/iobroker.js-controller/lib/states'); + let callbackId = 0; + + const options = { + stateChange: (id, state) => stateChange && stateChange(id, state) + }; + + that.namespace = 'test'; + + that.states = new _States({ + connection: { + type : 'file', + host : '127.0.0.1', + port : 19000, + options : { + auth_pass : null, + retry_max_delay : 15000 + } + }, + logger: logger, + change: (id, state) => { + if (!id || typeof id !== 'string') { + console.log('Something is wrong! ' + JSON.stringify(id)); + return; + } + + // Clear cache if accidentally got the message about change (Will work for admin and javascript) + if (id.match(/^system\.user\./) || id.match(/^system\.group\./)) { + that.users = []; + } + + // If someone want to have log messages + if (that.logList && id.match(/\.logging$/)) { + that.logRedirect(state ? state.val : false, id.substring(0, id.length - '.logging'.length)); + } else + if (id === 'log.system.adapter.' + that.namespace) { + that.processLog(state); + } else + // If this is messagebox + if (id === 'messagebox.system.adapter.' + that.namespace && state) { + // Read it from fifo list + that.states.delMessage('system.adapter.' + that.namespace, state._id); + const obj = state; + if (obj) { + // If callback stored for this request + if (obj.callback && + obj.callback.ack && + obj.callback.id && + that.callbacks && + that.callbacks['_' + obj.callback.id]) { + // Call callback function + if (that.callbacks['_' + obj.callback.id].cb) { + that.callbacks['_' + obj.callback.id].cb(obj.message); + delete that.callbacks['_' + obj.callback.id]; + } + // delete too old callbacks IDs, like garbage collector + const now = Date.now(); + for (const _id in that.callbacks) { + if (that.callbacks.hasOwnProperty(_id) && now - that.callbacks[_id].time > 3600000) delete that.callbacks[_id]; + } + + } else { + if (options.message) { + // Else inform about new message the adapter + options.message(obj); + } + that.emit('message', obj); + } + } + } else { + if (id.slice(that.namespace.length) === that.namespace) { + if (typeof options.stateChange === 'function') options.stateChange(id.slice(that.namespace.length + 1), state); + // emit 'stateChange' event instantly + setImmediate(() => that.emit('stateChange', id.slice(that.namespace.length + 1), state)); + + } else { + if (typeof options.stateChange === 'function') options.stateChange(id, state); + if (id.substring(0, 4) === 'log.') { + console.log("LOG"); + } + if (that.emit) { + // emit 'stateChange' event instantly + setImmediate(() => that.emit('stateChange', id, state)); + } + } + } + }, + connectTimeout: (error) => { + if (logger) logger.error(that.namespace + ' no connection to states DB'); + if (cb) cb('Timeout'); + } + }); + + // Send message to other adapter instance or all instances of adapter + that.sendTo = function sendTo(objName, command, message, callback) { + if (typeof message === 'undefined') { + message = command; + command = 'send'; + } + const obj = {command: command, message: message, from: 'system.adapter.' + that.namespace}; + + if (!objName.match(/^system\.adapter\./)) objName = 'system.adapter.' + objName; + + that.log.info('sendTo "' + command + '" to ' + objName + ' from system.adapter.' + that.namespace + ': ' + JSON.stringify(message)); + + // If not specific instance + if (!objName.match(/\.[0-9]+$/)) { + // Send to all instances of adapter + that.objects.getObjectView('system', 'instance', {startkey: objName + '.', endkey: objName + '.\u9999'}, (err, _obj) => { + if (_obj) { + for (let i = 0; i < _obj.rows.length; i++) { + that.states.pushMessage(_obj.rows[i].id, obj); + } + } + }); + } else { + if (callback) { + if (typeof callback === 'function') { + // force subscribe even no messagebox enabled + if (!that.common.messagebox && !that.mboxSubscribed) { + that.mboxSubscribed = true; + that.states.subscribeMessage('system.adapter.' + that.namespace); + } + + obj.callback = { + message: message, + id: callbackId++, + ack: false, + time: Date.now() + }; + if (callbackId >= 0xFFFFFFFF) { + callbackId = 1; + } + if (!that.callbacks) that.callbacks = {}; + that.callbacks['_' + obj.callback.id] = {cb: callback}; + + // delete too old callbacks IDs + const now = Date.now(); + for (const _id in that.callbacks) { + if (that.callbacks.hasOwnProperty(_id) && now - that.callbacks[_id].time > 3600000) { + delete that.callbacks[_id]; + } + } + } else { + obj.callback = callback; + obj.callback.ack = true; + } + } + + that.states.pushMessage(objName, obj); + } + }; + + // Send message to specific host or to all hosts + that.sendToHost = function sendToHost(objName, command, message, callback) { + if (typeof message === 'undefined') { + message = command; + command = 'send'; + } + const obj = {command: command, message: message, from: 'system.adapter.' + that.namespace}; + + if (objName && objName.substring(0, 'system.host.'.length) !== 'system.host.') objName = 'system.host.' + objName; + + if (!objName) { + // Send to all hosts + that.objects.getObjectList({startkey: 'system.host.', endkey: 'system.host.' + '\u9999'}, null, (err, res) => { + if (!err && res.rows.length) { + for (let i = 0; i < res.rows.length; i++) { + const parts = res.rows[i].id.split('.'); + // ignore system.host.name.alive and so on + if (parts.length === 3) { + that.states.pushMessage(res.rows[i].id, obj); + } + } + } + }); + } else { + if (callback) { + if (typeof callback === 'function') { + // force subscribe even no messagebox enabled + if (!that.common.messagebox && !that.mboxSubscribed) { + that.mboxSubscribed = true; + that.states.subscribeMessage('system.adapter.' + that.namespace); + } + + obj.callback = { + message: message, + id: callbackId++, + ack: false, + time: Date.now() + }; + if (callbackId >= 0xFFFFFFFF) callbackId = 1; + if (!that.callbacks) that.callbacks = {}; + that.callbacks['_' + obj.callback.id] = {cb: callback}; + } else { + obj.callback = callback; + obj.callback.ack = true; + } + } + + that.states.pushMessage(objName, obj); + } + }; + + that.setState = function setState(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id, 'state'); + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.setState(id, state, callback); + } + }); + } else { + that.states.setState(id, state, callback); + } + }; + + that.setForeignState = function setForeignState(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.setState(id, state, callback); + } + }); + } else { + that.states.setState(id, state, callback); + } + }; + + that.getState = function getState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id, 'state'); + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'getState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.getState(id, callback); + } + }); + } else { + that.states.getState(id, callback); + } + }; + + that.getStateHistory = function getStateHistory(id, start, end, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id, 'state'); + that.getForeignStateHistory(id, start, end, options, callback); + }; + + that.getForeignStateHistory = function getForeignStateHistory(id, start, end, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (typeof start === 'function') { + callback = start; + start = undefined; + end = undefined; + } else if (typeof end === 'function') { + callback = end; + end = undefined; + } + + start = start || Math.round((new Date()).getTime() / 1000) - 31536000; // - 1 year + end = end || Math.round((new Date()).getTime() / 1000) + 5000; + + const history = []; + const docs = []; + + // get data from states + that.log.debug('get states history ' + id + ' ' + start + ' ' + end); + that.getFifo(id, (err, res) => { + if (!err && res) { + let iProblemCount = 0; + for (let i = 0; i < res.length; i++) { + if (!res[i]) { + iProblemCount++; + continue; + } + if (res[i].ts < start) { + continue; + } else if (res[i].ts > end) { + break; + } + history.push(res[i]); + } + if (iProblemCount) that.log.warn('got null states ' + iProblemCount + ' times for ' + id); + + that.log.debug('got ' + res.length + ' datapoints for ' + id); + } else { + if (err !== 'Not exists') { + that.log.error(err); + } else { + that.log.debug('datapoints for ' + id + ' do not yet exist'); + } + } + + // fetch a history document from objectDB + function getObjectsLog(cid, callback) { + that.log.info('getObjectLog ' + cid); + that.getForeignObject(cid, options, (err, res) => { + if (!err && res.common.data) { + for (let i = 0; i < res.common.data.length; i++) { + if (res.common.data[i].ts < start) { + continue; + } else if (res.common.data[i].ts > end) { + break; + } + history.push(res.common.data[i]); + } + } else { + that.log.warn(cid + ' not found'); + } + callback(err); + }); + } + + // queue objects history documents fetching + function queue(ts) { + if (ts < start) { + callback(null, history); + return; + } + const cid = 'history.' + id + '.' + ts2day(ts); + if (docs.indexOf(cid) !== -1) { + getObjectsLog(cid, err => queue(ts - 86400)); // - 1 day + } else { + queue(ts - 86400); // - 1 day + } + } + + // get list of available history documents + that.objects.getObjectList({startkey: 'history.' + id, endkey: 'history.' + id + '\u9999'}, options, (err, res) => { + if (!err && res.rows.length) { + for (let i = 0; i < res.rows.length; i++) { + docs.push(res.rows[i].id); + } + queue(end); + } else { + callback(null, history); + } + }); + }); + }; + + // normally only foreign history has interest, so there is no getHistory and getForeignHistory + that.getHistory = function getHistory(id, options, callback) { + options = options || {}; + options.end = options.end || Math.round((new Date()).getTime() / 1000) + 5000; + if (!options.count && !options.start) { + options.start = options.start || Math.round((new Date()).getTime() / 1000) - 604800; // - 1 week + } + + if (!options.instance) { + if (!that.defaultHistory) { + // read default history instance from system.config + return getDefaultHistory(() => that.getHistory(id, options, callback)); + } else { + options.instance = that.defaultHistory; + } + } + + that.sendTo(options.instance || 'history.0', 'getHistory', {id: id, options: options}, res => { + setImmediate(() => callback(res.error, res.result, res.step)); + }); + }; + + // Convert ID adapter.instance.device.channel.state + // Convert ID to {device: D, channel: C, state: S} + that.idToDCS = function idToDCS(id) { + if (!id) return null; + const parts = id.split('.'); + if (parts[0] + '.' + parts[1] !== that.namespace) { + that.log.warn("Try to decode id not from this adapter"); + return null; + } + return {device: parts[2], channel: parts[3], state: parts[4]}; + }; + + that.getForeignState = function getForeignState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'getState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.getState(id, callback); + } + }); + } else { + that.states.getState(id, callback); + } + }; + + that.delForeignState = function delForeignState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'delState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.delState(id, callback); + } + }); + } else { + that.states.delState(id, callback); + } + + }; + + that.delState = function delState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id); + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'delState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.delState(id, callback); + } + }); + } else { + that.states.delState(id, callback); + } + }; + + that.getStates = function getStates(pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + pattern = that._fixId(pattern, 'state'); + that.getForeignStates(pattern, options, callback); + }; + + that.getForeignStates = function getForeignStates(pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + const list = {}; + if (typeof pattern === 'function') { + callback = pattern; + pattern = '*'; + } + + if (typeof callback !== 'function') { + logger.error('getForeignStates invalid callback for ' + pattern); + return; + } + + if (typeof pattern === 'object') { + that.states.getStates(pattern, (err, arr) => { + if (err) { + callback(err); + return; + } + for (let i = 0; i < pattern.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[pattern[i]] = arr[i] || {}; + } + callback(null, list); + }); + return; + } + const keys = []; + let params = {}; + if (pattern && pattern !== '*') { + params = { + startkey: pattern.replace('*', ''), + endkey: pattern.replace('*', '\u9999') + }; + } + that.objects.getObjectView('system', 'state', params, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + for (let i = 0; i < res.rows.length; i++) { + keys.push(res.rows[i].id); + } + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(keys, options, 'getState', (err, keys) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + that.states.getStates(keys, function (err, arr) { + if (err) { + callback(err); + return; + } + for (let i = 0; i < res.rows.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[keys[i]] = arr[i] || {}; + } + if (typeof callback === 'function') callback(null, list); + }); + }); + } else { + that.states.getStates(keys, function (err, arr) { + if (err) { + callback(err); + return; + } + for (let i = 0; i < res.rows.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[keys[i]] = arr[i] || {}; + } + if (typeof callback === 'function') callback(null, list); + }); + } + }); + }; + + that.subscribeForeignStates = function subscribeForeignStates(pattern, options) { + if (!pattern) pattern = '*'; + that.states.subscribe(pattern, options); + }; + + that.unsubscribeForeignStates = function unsubscribeForeignStates(pattern, options) { + if (!pattern) pattern = '*'; + that.states.unsubscribe(pattern, options); + }; + + that.subscribeStates = function subscribeStates(pattern, options) { + // Exception. Threat the '*' case automatically + if (!pattern || pattern === '*') { + that.states.subscribe(that.namespace + '.*', options); + } else { + pattern = that._fixId(pattern, 'state'); + that.states.subscribe(pattern, options); + } + }; + + that.unsubscribeStates = function unsubscribeStates(pattern, options) { + if (!pattern || pattern === '*') { + that.states.unsubscribe(that.namespace + '.*', options); + } else { + pattern = that._fixId(pattern, 'state'); + that.states.unsubscribe(pattern, options); + } + }; + + that.pushFifo = function pushFifo(id, state, callback) { + that.states.pushFifo(id, state, callback); + }; + + that.trimFifo = function trimFifo(id, start, end, callback) { + that.states.trimFifo(id, start, end, callback); + }; + + that.getFifoRange = function getFifoRange(id, start, end, callback) { + that.states.getFifoRange(id, start, end, callback); + }; + + that.getFifo = function getFifo(id, callback) { + that.states.getFifo(id, callback); + }; + + that.lenFifo = function lenFifo(id, callback) { + that.states.lenFifo(id, callback); + }; + + that.subscribeFifo = function subscribeFifo(pattern) { + that.states.subscribeFifo(pattern); + }; + + that.getSession = function getSession(id, callback) { + that.states.getSession(id, callback); + }; + that.setSession = function setSession(id, ttl, data, callback) { + that.states.setSession(id, ttl, data, callback); + }; + that.destroySession = function destroySession(id, callback) { + that.states.destroySession(id, callback); + }; + + that.getMessage = function getMessage(callback) { + that.states.getMessage('system.adapter.' + that.namespace, callback); + }; + + that.lenMessage = function lenMessage(callback) { + that.states.lenMessage('system.adapter.' + that.namespace, callback); + }; + + // Write binary block into redis, e.g image + that.setBinaryState = function setBinaryState(id, binary, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + that.states.setBinaryState(id, binary, callback); + }; + + // Read binary block fromredis, e.g. image + that.getBinaryState = function getBinaryState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + that.states.getBinaryState(id, callback); + }; + + logger.debug(that.namespace + ' statesDB connected'); + + if (typeof cb === 'function') { + setImmediate(() => cb(), 0); + } + + return this; +} + +module.exports = States; \ No newline at end of file diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..8b4e595 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,92 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +'use strict'; +const expect = require('chai').expect; +const fs = require('fs'); + +describe('Test package.json and io-package.json', () => { + it('Test package files', function (done) { + console.log(); + + const fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + const ioPackage = JSON.parse(fileContentIOPackage); + + const fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + const 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; + } + } + + const licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + const 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/testServer.js b/test/testServer.js new file mode 100644 index 0000000..0bad4b7 --- /dev/null +++ b/test/testServer.js @@ -0,0 +1,264 @@ +/* jshint -W097 */ +// jshint strict:true +/*jslint node: true */ +/*jslint esversion: 6 */ +'use strict'; +let expect = require('chai').expect; +let setup = require(__dirname + '/lib/setup'); + +let objects = null; +let states = null; +let mqttClientEmitter = null; +let mqttClientDetector = null; +let connected = false; +let lastReceivedTopic1; +let lastReceivedMessage1; +let lastReceivedTopic2; +let lastReceivedMessage2; + +let clientConnected1 = false; +let clientConnected2 = false; +let brokerStarted = false; + +let rules = { + 'tele/sonoff_4ch/STATE': {send: '{"Time":"2017-10-02T19:26:06", "Uptime":0, "Vcc":3.226, "POWER1":"OFF", "POWER2":"OFF", "POWER3":"OFF", "POWER4":"OFF", "Wifi":{"AP":1, "SSId":"AAA", "RSSI": 15}}', expect: {Vcc: 3.226, Wifi_RSSI: 15}}, + 'tele/sonoff/SENSOR': {send: '{"Time":"2017-10-05T17:43:19", "DS18x20":{"DS1":{"Type":"DS18B20", "Address":"28FF9A9876815022A", "Temperature":12.2}}, "TempUnit":"C"}', expect: {DS18x20_DS1_Temperature: 12.2}}, + 'tele/sonoff5/SENSOR': {send: '{"Time":"2017-10-03T14:02:25", "AM2301-14":{"Temperature":21.6, "Humidity":54.7}, "TempUnit":"C"}', expect: {'AM2301-14_Temperature': 21.6, 'AM2301-14_Humidity': 54.7}}, + 'tele/SonoffPOW/INFO1': {send: '{"Module":"Sonoff Pow", "Version":"5.8.0", "FallbackTopic":"SonoffPOW", "GroupTopic":"sonoffs"}', expect: {'INFO.Module': 'Sonoff Pow', 'INFO.Version': '5.8.0'}}, + 'tele/SonoffPOW/INFO2': {send: '{"WebServerMode":"Admin", "Hostname":"Sonoffpow", "IPAddress":"192.168.2.182"}', expect: {'INFO.Hostname': 'Sonoffpow', 'INFO.IPAddress': '192.168.2.182'}}, + 'tele/SonoffPOW/INFO3': {send: '{"RestartReason":"Software/System restart"}', expect: {'INFO.RestartReason': 'Software/System restart'}}, + 'tele/sonoff_4ch/ENERGY': {send: '{"Time":"2017-10-02T19:24:32", "Total":1.753, "Yesterday":0.308, "Today":0.205, "Period":0, "Power":3, "Factor":0.12, "Voltage":221, "Current":0.097}', expect: {'ENERGY.Total': 1.753, 'ENERGY.Current': 0.097}}, + 'tele/sonoff_4ch/ENERGY1': {send: '"Time":"2017-10-02T19:24:32", "Total":1.753, "Yesterday":0.308, "Today":0.205, "Period":0, "Power":3, "Factor":0.12, "Voltage":221, "Current":0.097}', expect: {}}, + 'tele/sonoff_1ch/STATE': {send: '{"Time":"2017-10-02T19:24:32", "Color": "112233"}', expect: {}}, + 'tele/sonoff/STATE': {send: '{"Time":"2018-06-19T06:39:33","Uptime":"0T23:47:32","Vcc":3.482,"POWER":"OFF","Dimmer":100,"Color":"000000FF","HSBColor":"0,0,0","Channel":[0,0,0,100],"Scheme":0,"Fade":"OFF","Speed":4,"LedTable":"OFF","Wifi":{"AP":1,"SSId":"WLAN-7490","RSSI":50,"APMac":"34:31:C4:C6:EB:0F"}}', + expect:{}}, + 'tele/sonoff1/SENSOR': {send: '{"Time":"2018-06-15T10:03:24","DS18B20":{"Temperature":0.0},"TempUnit":"C"}', expect: {'DS18B20_Temperature': 0}}, + '/ESP_BOX/BM280/Pressure': {send: '1010.09', expect: {'Pressure': 1010.09}}, + '/ESP_BOX/BM280/Humidity': {send: '42.39', expect: {'Humidity': 42.39}}, + '/ESP_BOX/BM280/Temperature': {send: '25.86', expect: {'Temperature': 25.86}}, + '/ESP_BOX/BM280/Approx. Altitude': {send: '24', expect: {'Approx_Altitude': 24}}, + 'stat/sonoff/POWER': {send: 'ON', expect: {'POWER': true}}, + 'cmnd/sonoff/POWER': {send: '', expect: {}}, + 'stat/sonoff/RESULT': {send: '{"POWER": "ON"}', expect: {'RESULT': null}}, + 'stat/sonoff/LWT': {send: 'someTopic', expect: {'LWT': null}}, + 'stat/sonoff/ABC': {send: 'text', expect: {'ABC': null}} +}; +function decrypt(key, value) { + let result = ''; + for (let i = 0; i < value.length; ++i) { + result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); + } + return result; +} + +function startClients(_done) { + // start mqtt client + const MqttClient = require(__dirname + '/lib/mqttClient.js'); + + // Start client to emit topics + mqttClientEmitter = new MqttClient(connected => { + // on connected + if (connected) { + console.log('Test MQTT Emitter is connected to MQTT broker'); + clientConnected1 = true; + if (_done && brokerStarted && clientConnected1 && clientConnected2) { + _done(); + _done = null; + } + } + }, (topic, message) => { + console.log(Date.now() + ' emitter received ' + topic + ': ' + message.toString()); + // on receive + lastReceivedTopic1 = topic; + lastReceivedMessage1 = message ? message.toString() : null; + }, {name: 'Emitter*1', user: 'user', pass: 'pass1'}); + + // Start client to receive topics + mqttClientDetector = new MqttClient(connected => { + // on connected + if (connected) { + console.log('Test MQTT Detector is connected to MQTT broker'); + clientConnected2 = true; + if (_done && brokerStarted && clientConnected1 && clientConnected2) { + _done(); + _done = null; + } + } + }, (topic, message) => { + console.log(Date.now() + ' detector received ' + topic + ': ' + message.toString()); + // on receive + lastReceivedTopic2 = topic; + lastReceivedMessage2 = message ? message.toString() : null; + console.log(JSON.stringify(lastReceivedMessage2)); + }, {name: 'Detector-1', user: 'user', pass: 'pass1'}); +} + +function checkMqtt2Adapter(id, task, _it, _done) { + _it.timeout(1000); + + lastReceivedMessage1 = null; + lastReceivedTopic1 = null; + lastReceivedTopic2 = null; + lastReceivedMessage2 = null; + + console.log(`[${new Date().toISOString()}] Publish ${id}: ${task.send}`); + mqttClientEmitter.publish(id, task.send, err => { + expect(err).to.be.undefined; + + setTimeout(() => { + let count = 0; + for (let e in task.expect) { + if (! task.expect.hasOwnProperty(e)) continue; + count++; + (function (_id, _val) { + objects.getObject('sonoff.0.Emitter_1.' + _id, (err, obj) => { + if (_val !== null) { + if (!obj) console.error('Object sonoff.0.Emitter_1.' + _id + ' not found'); + expect(obj).to.be.not.null.and.not.undefined; + expect(obj._id).to.be.equal('sonoff.0.Emitter_1.' + _id); + expect(obj.type).to.be.equal('state'); + + states.getState(obj._id, function (err, state) { + expect(state).to.be.not.null.and.not.undefined; + expect(state.val).to.be.equal(_val); + expect(state.ack).to.be.true; + if (!--count) _done(); + }); + } else { + expect(obj).to.be.undefined; + + states.getState('sonoff.0.Emitter_1.' + _id, (err, state) => { + expect(state).to.be.undefined; + if (!--count) _done(); + }); + } + }); + })(e, task.expect[e]); + } + if (!count) _done(); + }, 200); + }); +} + +function checkAdapter2Mqtt(id, mqttid, value, _done) { + console.log(new Date().toISOString() + ' Send ' + id + ' with value '+ value); + + lastReceivedTopic1 = null; + lastReceivedMessage1 = null; + lastReceivedTopic2 = null; + lastReceivedMessage2 = null; + + states.setState(id, { + val: value, + ack: false + }, (err, id) => { + setTimeout(() => { + if (!lastReceivedTopic1) { + setTimeout(() => { + expect(lastReceivedTopic1).to.be.equal(mqttid); + expect(lastReceivedMessage1).to.be.equal(value ? 'ON' : 'OFF'); + _done(); + }, 200); + } else { + expect(lastReceivedTopic1).to.be.equal(mqttid); + expect(lastReceivedMessage1).to.be.equal(value ? 'ON' : 'OFF'); + _done(); + } + }, 400); + }); +} + +function checkConnection(value, done, counter) { + counter = counter || 0; + if (counter > 20) { + done && done('Cannot check ' + value); + return; + } + + states.getState('sonoff.0.info.connection', (err, state) => { + if (err) console.error(err); + if (state && typeof state.val === 'string' && ((value && state.val.indexOf(',') !== -1) || (!value && state.val.indexOf(',') === -1))) { + connected = value; + done(); + } else { + setTimeout(() => { + checkConnection(value, done, counter + 1); + }, 1000); + } + }); +} + +describe('Sonoff server: Test mqtt server', () => { + before('Sonoff server: Start js-controller', function (_done) { // + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(systemConfig => { + let config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.user = 'user'; + config.native.pass = decrypt(systemConfig.native.secret, 'pass1'); + + setup.setAdapterConfig(config.common, config.native); + + setup.startController((_objects, _states) => { + objects = _objects; + states = _states; + brokerStarted = true; + if (_done && brokerStarted && clientConnected1 && clientConnected2) { + _done(); + _done = null; + } + }); + }); + + startClients(_done); + }); + + it('Sonoff Server: Check if connected to MQTT broker', done => { + if (!connected) { + checkConnection(true, done); + } else { + done(); + } + }).timeout(2000); + + for (let r in rules) { + (function(id, task) { + it('Sonoff Server: Check receive ' + id, function (done) { // let FUNCTION here + checkMqtt2Adapter(id, task, this, done); + }); + })(r, rules[r]); + } + + // give time to client to receive all messages + it('wait', done => { + setTimeout(() => done(), 1000); + }).timeout(3000); + + it('Sonoff server: detector must receive cmnd/sonoff/POWER', done => { + checkAdapter2Mqtt('sonoff.0.Emitter_1.POWER', 'cmnd/sonoff/POWER', false, done); + }).timeout(2000); + + it('Sonoff Server: check reconnection', done => { + mqttClientEmitter.stop(); + mqttClientDetector.stop(); + checkConnection(false, error => { + expect(error).to.be.not.ok; + startClients(); + checkConnection(true, error => { + expect(error).to.be.not.ok; + done(); + }); + }); + }).timeout(10000); + + after('Sonoff Server: Stop js-controller', function (_done) { // let FUNCTION and not => here + this.timeout(5000); + mqttClientEmitter.stop(); + mqttClientDetector.stop(); + setup.stopController(() => _done()); + }); +});