Initial commit

This commit is contained in:
2019-01-16 09:07:10 +08:00
commit 0cc08f136d
32 changed files with 7021 additions and 0 deletions

7
.gitignore vendored Normal file
View File

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

12
.npmignore Normal file
View File

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

24
.travis.yml Normal file
View File

@@ -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

22
LICENSE Normal file
View File

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

194
README.md Executable file
View File

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

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

177
admin/index.html Normal file
View File

@@ -0,0 +1,177 @@
<html>
<head>
<!-- these 4 files always have to be included -->
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
<!-- these three files have to be always included -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<!-- you have to define 2 functions in the global scope: -->
<script type="text/javascript">
var onChange;
var secret;
function encrypt(key, value) {
var result = '';
for(var i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
function decrypt(key, value) {
var result = '';
for(var i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
function setValue(id, value) {
var $value = $('#' + id + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', value).change(function() {
onChange();
});
} else {
$value.val(value).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
}
function loadHelper(settings, param, subparam) {
if (!settings) return;
if (settings.useChunkPatch === undefined) settings.useChunkPatch = false;
if (settings.timeout === undefined) settings.timeout = 300;
if (subparam && param) {
for (var key in settings[param][subparam]) {
if (typeof settings[param][subparam][key] !== 'object') {
setValue(param + '_' + subparam + '_' + key, settings[param][subparam][key]);
} else {
alert('4th level is not supported');
}
}
} else if (param) {
for (var key in settings[param]) {
if (typeof settings[param][key] !== 'object') {
setValue(param + '_' + key, settings[param][key]);
} else {
loadHelper(settings, param, key);
}
}
} else {
for (var key in settings) {
if (typeof settings[key] !== 'object') {
if (key === 'pass') {
settings[key] = decrypt(secret, settings[key]);
}
setValue(key, settings[key]);
} else {
loadHelper(settings, key);
}
}
}
}
// the function loadSettings has to exist ...
function load(settings, _onChange) {
$('#tabs').tabs();
onChange = _onChange;
socket.emit('getObject', 'system.config', function (err, obj) {
secret = (obj.native ? obj.native.secret : '') || 'Zgfr56gFe87jJOM';
loadHelper(settings);
});
onChange(false);
}
function saveHelper(obj, id, value) {
var ids = id.split('_');
if (ids.length === 1) {
if (ids[0] === 'pass') value = encrypt(secret, value);
obj[id] = value;
} else if (ids.length === 2) {
if (!obj[ids[0]]) obj[ids[0]] = {};
obj[ids[0]][ids[1]] = value;
} else if (ids.length === 3) {
if (!obj[ids[0]]) obj[ids[0]] = {};
if (!obj[ids[0]][ids[1]]) obj[ids[0]][ids[1]] = {};
obj[ids[0]][ids[1]][ids[2]] = value;
}
return obj;
}
function getSettings() {
var obj = {};
$('.value').each(function () {
var $this = $(this);
var id = $this.attr('id');
if ($this.attr('type') === 'checkbox') {
obj = saveHelper(obj, id, $this.prop('checked'))
} else {
obj = saveHelper(obj, id, $this.val())
}
});
return obj;
}
function save(callback) {
if ($('#pass').val() !== $('#pass_confirm').val()) {
showMessage(_('Password confirmation is not equal with password'));
return;
}
callback(getSettings());
}
</script>
</head>
<body>
<!-- you have to put your config page in a div with id adapter-container -->
<div id="adapter-container">
<table><tr>
<td><img src="sonoff.png"></td>
<td style="padding-top: 20px;padding-left: 10px"><h3 class="translate">Sonoff adapter settings</h3></td>
</tr></table>
<table>
<tr>
<td colspan='2'><h4 class="translate">Connection settings</h4></td>
</tr>
<tr>
<td><label class="translate" for="port">Port:</label></td>
<td><input id="port" type="number" min="1" max="65565" class="value"/></td>
</tr>
<tr>
<td><label class="translate" for="timeout">Client timeout (sec):</label></td>
<td><input id="timeout" type="number" min="1" max="65565" class="value"/></td>
</tr>
<tr>
<td colspan='2'><h4 class="translate">Authentication settings</h4></td>
</tr>
<tr>
<td><label class="translate" for="user">User:</label></td>
<td><input id="user" size="17" class="value"/></td>
</tr>
<tr>
<td><label class="translate" for="pass">Password:</label></td>
<td><input id="pass" type="password" size="17" class="value"/></td>
</tr>
<tr>
<td><label class="translate" for="pass_confirm">Password confirmation:</label></td>
<td><input id="pass_confirm" type="password" size="17"/></td>
</tr>
</table>
</div>
</body>
</html>

183
admin/index_m.html Normal file
View File

@@ -0,0 +1,183 @@
<html>
<head>
<!-- Materialze style -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
<script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../lib/js/materialize.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<script type="text/javascript">
var onChange;
var secret;
function encrypt(key, value) {
var result = '';
for(var i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
function decrypt(key, value) {
var result = '';
for(var i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
function setValue(id, value) {
var $value = $('#' + id + '.value');
if ($value.attr('type') === 'checkbox') {
$value.prop('checked', value).on('change', function () {
onChange();
});
} else {
$value.val(value).on('change', function () {
onChange();
}).keyup(function () {
onChange();
});
}
}
function loadHelper(settings, param, subparam) {
if (!settings) return;
if (settings.useChunkPatch === undefined) settings.useChunkPatch = false;
if (settings.timeout === undefined) settings.timeout = 300;
for (var key in settings) {
if (!settings.hasOwnProperty(key)) continue;
if (key === 'pass') {
settings[key] = decrypt(secret, settings[key]);
}
setValue(key, settings[key]);
}
}
// the function loadSettings has to exist ...
function load(settings, _onChange) {
onChange = _onChange;
socket.emit('getObject', 'system.config', function (err, obj) {
secret = (obj.native ? obj.native.secret : '') || 'Zgfr56gFe87jJOM';
loadHelper(settings);
M.updateTextFields();
});
onChange(false);
}
function getSettings() {
var obj = {};
$('.value').each(function () {
var $this = $(this);
var id = $this.attr('id');
if ($this.attr('type') === 'checkbox') {
obj[id] = $this.prop('checked');
} else {
var value = $this.val();
if (id === 'pass') {
value = encrypt(secret, value);
}
obj[id] = value;
}
});
return obj;
}
function save(callback) {
if ($('#pass').val() !== $('#pass_confirm').val()) {
showMessage(_('Password confirmation is not equal with password'));
return;
}
callback(getSettings());
}
</script>
<style>
.adapter-container>div {
margin-bottom: 0 !important;
}
.page {
height: calc(100% - 50px) !important;
}
.marg{
margin-top: 5em;
}
</style>
</head>
<body>
<div class="m adapter-container">
<div class="row">
<div class="col s12">
<div class="row">
<div class="col s6">
<img src="sonoff.png" class="logo">
</div>
</div>
<div class="row">
<div class="col s12">
<h6 class="title translate">Connection settings</h6>
</div>
</div>
<div class="row">
<div class="input-field col s6 m4 l2">
<input id="port" type="number" min="1" max="65565" class="value"/>
<label class="translate" for="port">Port:</label>
</div>
<div class="input-field col s6 m4 l2">
<input id="timeout" type="number" min="1" max="99999" class="value"/>
<label class="translate" for="timeout">Client timeout (sec):</label>
</div>
</div>
<div class="row">
<div class="col s12">
<h6 class="title translate">Authentication settings</h6>
</div>
</div>
<div class="row">
<div class="input-field col s6 m4">
<input id="user" type="text" class="value"/>
<label class="translate" for="user">User:</label>
</div>
<div class="input-field col s6 m4">
<input id="pass" type="password" class="value"/>
<label class="translate" for="pass">Password:</label>
</div>
<div class="input-field col s6 m4">
<input id="pass_confirm" type="password" />
<label class="translate" for="pass_confirm">Password confirmation:</label>
</div>
</div>
<div class="row">
<div class="col s12">
<h6 class="title translate">Auto creation of states</h6>
</div>
</div>
<div class="row">
<div class="input-field col s6 m4">
<input id="TELE_SENSOR" type="checkbox" class="value"/>
<span class="translate" for="user">For tele/SENSOR</span>
</div>
<div class="input-field col s6 m4">
<input id="TELE_STATE" type="checkbox" class="value"/>
<span class="translate" for="TELE_STATE">For tele/STATE</span>
</div>
<div class="input-field col s6 m4">
<input id="STAT_RESULT" type="checkbox" class="value"/>
<span class="translate" for="STAT_RESULT">For stat/RESULT</span>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

BIN
admin/sonoff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

53
admin/words.js Normal file
View File

@@ -0,0 +1,53 @@
// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n
/*global systemDictionary:true */
'use strict';
systemDictionary = {
"Sonoff adapter settings": { "en": "Sonoff adapter settings", "de": "Sonoff Adapter-Einstellungen", "ru": "Настройки драйвера Sonoff", "pt": "Configurações do adaptador Sonoff", "nl": "Configurações do adaptador Sonoff", "fr": "Paramètres de l'adaptateur Sonoff", "it": "Impostazioni dell'adattatore Sonoff", "es": "Configuraciones del adaptador Sonoff", "pl": "Ustawienia adaptera Sonoff"},
"Type:": { "en": "Type", "de": "Typ", "ru": "Тип", "pt": "Tipo", "nl": "Tipo", "fr": "Type", "it": "genere", "es": "Tipo", "pl": "Rodzaj"},
"Port:": { "en": "Port", "de": "Port", "ru": "Порт", "pt": "Porta", "nl": "Porta", "fr": "Port", "it": "Porta", "es": "Puerto", "pl": "Port"},
"User:": { "en": "User", "de": "Username", "ru": "Имя пользователя", "pt": "Do utilizador", "nl": "Do utilizador", "fr": "Utilisateur", "it": "Utente", "es": "Usuario", "pl": "Użytkownik"},
"Password:": { "en": "Password", "de": "Kennwort", "ru": "Пароль", "pt": "Senha", "nl": "Senha", "fr": "Mot de passe", "it": "Parola d'ordine", "es": "Contraseña", "pl": "Hasło"},
"Password confirmation:": { "en": "Password confirmation", "de": "Kennwort-Wiederholung", "ru": "Подтверждение пароля", "pt": "ConfirmaÇão Da Senha", "nl": "ConfirmaÇão Da Senha", "fr": "Confirmation mot de passe", "it": "conferma password", "es": "confirmación de contraseña", "pl": "Potwierdzenie hasła"},
"URL:": { "en": "URL", "de": "URL", "ru": "URL", "pt": "URL", "nl": "URL", "fr": "URL", "it": "URL", "es": "URL", "pl": "URL"},
"Secure:": { "en": "Secure", "de": "SSL", "ru": "SSL", "pt": "Seguro", "nl": "Seguro", "fr": "Garantir", "it": "Sicuro", "es": "Seguro", "pl": "Bezpieczne"},
"Public certificate:": { "en": "Public certificate", "de": "Publikzertifikat", "ru": "'Public' сертификат", "pt": "Certificado público", "nl": "Certificado público", "fr": "Certificat public", "it": "Certificato pubblico", "es": "Certificado público", "pl": "Certyfikat publiczny"},
"Private certificate:": { "en": "Private certificate", "de": "Privatzertifikat", "ru": "'Private' сертификат", "pt": "Certificado privado", "nl": "Certificado privado", "fr": "Certificat privé", "it": "Certificato privato", "es": "Certificado privado", "pl": "Prywatny certyfikat"},
"Chained certificate:": { "en": "Chained certificate", "de": "Kettenzertifikat", "ru": "'Chained' сертификат", "pt": "Certificado acorrentado", "nl": "Certificado acorrentado", "fr": "Certificat chaîné", "it": "Certificato incatenato", "es": "Certificado encadenado", "pl": "Przykuty certyfikat"},
"Patterns:": { "en": "Subscribe patterns", "de": "Subscribe patterns", "ru": "Patterns", "pt": "Assinar padrões", "nl": "Assinar padrões", "fr": "Modèles de souscription", "it": "Iscriviti modelli", "es": "Suscribir patrones", "pl": "Wzorce subskrypcji"},
"Use WebSockets:": { "en": "Use WebSockets too", "de": "Benutze auch WebSockets", "ru": "Сервер WebSockets тоже", "pt": "Use também WebSockets", "nl": "Use também WebSockets", "fr": "Utilisez aussi WebSockets", "it": "Utilizza anche WebSockets", "es": "Use WebSockets también", "pl": "Użyj także WebSockets"},
"Connection": { "en": "Connection", "de": "Verbindung", "ru": "Соединение", "pt": "Conexão", "nl": "Conexão", "fr": "Connexion", "it": "Connessione", "es": "Conexión", "pl": "Połączenie"},
"Sonoff Settings": { "en": "Sonoff Settings", "de": "Sonoff Einstellungen", "ru": "Настройки Sonoff", "pt": "Configurações de Sonoff", "nl": "Configurações de Sonoff", "fr": "Paramètres Sonoff", "it": "Impostazioni Sonoff", "es": "Configuraciones Sonoff", "pl": "Ustawienia Sonoff"},
"Client ID:": { "en": "Client ID", "de": "Client ID", "ru": "ID Клиента", "pt": "ID do Cliente", "nl": "ID do Cliente", "fr": "identité du client", "it": "Identificativo cliente", "es": "Identificación del cliente", "pl": "Identyfikator klienta"},
"chars": { "en": "chars", "de": "Symbolen", "ru": "символов", "pt": "caracteres", "nl": "caracteres", "fr": "chars", "it": "caratteri", "es": "caracteres", "pl": "znaki"},
"ms": { "en": "ms", "de": "ms", "ru": "мс", "pt": "Senhora", "nl": "Senhora", "fr": "Mme", "it": "Signorina", "es": "Sra", "pl": "ms"},
"Interval before send topics by connection:": { "en": "Interval before send topics by connection", "de": "Interval vom Senden von allen Topics bei der verbindung", "ru": "Интервал перед отсылкой всей топиков после соединения", "pt": "Intervalo antes de enviar tópicos por conexão", "nl": "Intervalo antes de enviar tópicos por conexão", "fr": "Intervalle avant d'envoyer des sujets par connexion", "it": "Intervallo prima di inviare argomenti per connessione", "es": "Intervalo antes de enviar temas por conexión", "pl": "Interwał przed wysłaniem tematów według połączenia"},
"Send interval:": { "en": "Send interval", "de": "Sendeintervall", "ru": "Интервал между пакетами", "pt": "Intervalo de envio", "nl": "Intervalo de envio", "fr": "Envoyer un intervalle", "it": "Invia intervallo", "es": "Intervalo de envío", "pl": "Interwał wysyłania"},
"Use chunk patch:": { "en": "Use chunk patch", "de": "Benutze Patch für Chunking", "ru": "Использовать заплатку для Chunking", "pt": "Use patch de pedaços", "nl": "Use patch de pedaços", "fr": "Utiliser un patch de morceau", "it": "Usa patch pezzo", "es": "Use el fragmento de fragmento", "pl": "Użyj poprawki kawałka"},
"Divided by comma": { "en": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'", "de": "Mit Komma getrennt, z.B 'mqtt/0/#,javascript/#'", "ru": "Использовать запятую, как разделитеть. Например 'mqtt/0/#,javascript/#'", "pt": "Dividido por vírgula, e. 'mqtt / 0 / #, javascript / #'", "nl": "Dividido por vírgula, e. 'mqtt/0/#, javascript/#'", "fr": "Divisé par une virgule, par ex. 'mqtt/0/#, javascript/#'", "it": "Diviso in virgola, ad es. 'MQTT/0/#, JavaScript/#'", "es": "Dividido por coma, p. 'mqtt/0/#, javascript/#'", "pl": "Podzielone przecinkiem, np. \"mqtt / 0 / #, javascript / # '"},
"Mask to publish": { "en": "e.g. 'mqtt.0.*,javascript.*'", "de": "z.B 'mqtt.0.*,javascript.*'", "ru": "Использовать запятую, как разделитеть. Например 'mqtt.0.*,javascript.*'", "pt": "por exemplo. 'mqtt.0. *, javascript. *'", "nl": "por exemplo. 'mqtt.0.*, javascript.*'", "fr": "par exemple. 'mqtt.0.*, javascript.*'", "it": "per esempio. 'Mqtt.0. *, Javascript. *'", "es": "p.ej. 'mqtt.0.*, javascript.*'", "pl": "na przykład \"mqtt.0. *, javascript. *\""},
"Store only on change:": { "en": "Publish only on change", "de": "Publish nur bei Änderung", "ru": "Отсылать только изменения", "pt": "Publique apenas na mudança", "nl": "Publique apenas na mudança", "fr": "Publier uniquement en cas de modification", "it": "Pubblica solo sul cambiamento", "es": "Publicar solo en el cambio", "pl": "Opublikuj tylko w przypadku zmiany"},
"Trace output for every message:": { "en": "Trace output for every message", "de": "Trace Ausgabe für jede Meldung", "ru": "Вывод лога для каждого изменения", "pt": "Saída de rastreamento para cada mensagem", "nl": "Saída de rastreamento para cada mensagem", "fr": "Trace de sortie pour chaque message", "it": "Traccia l'output per ogni messaggio", "es": "Salida de seguimiento para cada mensaje", "pl": "Dane wyjściowe śledzenia dla każdej wiadomości"},
"Test connection": { "en": "Test connection with Server", "de": "Teste Verbindung zum Server", "ru": "Проверить настройки", "pt": "Teste a conexão com o Servidor", "nl": "Teste a conexão com o Servidor", "fr": "Tester la connexion avec le serveur", "it": "Verifica la connessione con il server", "es": "Prueba de conexión con el servidor", "pl": "Sprawdź połączenie z serwerem"},
"Result: ": { "en": "Result ", "de": "Ergebnis ", "ru": "Результат ", "pt": "Resultado", "nl": "Resultado", "fr": "Résultat", "it": "Risultato", "es": "Resultado", "pl": "Wynik"},
"connected": { "en": "connected", "de": "verbunden", "ru": "успешно", "pt": "conectado", "nl": "conectado", "fr": "connecté", "it": "collegato", "es": "conectado", "pl": "połączony"},
"Main settings": { "en": "Main settings", "de": "Allgemeine Einstellungen", "ru": "Основные настройки", "pt": "Configurações principais", "nl": "Configurações principais", "fr": "Réglages principaux", "it": "Impostazioni principali", "es": "Ajustes principales", "pl": "Ustawienia główne"},
"Connection settings": { "en": "Connection settings", "de": "Verbindungseinstellungen", "ru": "Настройки соединения", "pt": "Configurações de conexão", "nl": "Configurações de conexão", "fr": "Paramètres de connexion", "it": "Impostazioni di connessione", "es": "Configuración de conexión", "pl": "Ustawienia połączenia"},
"Authentication settings": { "en": "Authentication settings", "de": "Authentication Einstellungen", "ru": "Настройки аутентификации", "pt": "Configurações de autenticação", "nl": "Configurações de autenticação", "fr": "Paramètres d'authentification", "it": "Impostazioni di autenticazione", "es": "Configuraciones de autenticación", "pl": "Ustawienia uwierzytelniania"},
"Adapter settings": { "en": "Sonoff settings", "de": "Sonoff Einstellungen", "ru": "Настройки Sonoff", "pt": "Configurações de Sonoff", "nl": "Configurações de Sonoff", "fr": "Paramètres Sonoff", "it": "Impostazioni Sonoff", "es": "Configuraciones Sonoff", "pl": "Ustawienia Sonoff"},
"Mask to publish own states:": { "en": "Mask to publish own states", "de": "Maske für Bekanntgeben von eigenen States", "ru": "Маска для собственных значений", "pt": "Máscara para publicar estados próprios", "nl": "Máscara para publicar estados próprios", "fr": "Masque pour publier ses propres états", "it": "Maschera per pubblicare i propri stati", "es": "Máscara para publicar estados propios", "pl": "Maskuj, aby publikować własne stany"},
"Send states (ack=true) too:": { "en": "Send states (ack=true) too", "de": "Sende auch Zustände (ack=true)", "ru": "Посылать не только команды, но и состояния (ack=true)", "pt": "Enviar estados (ack = true) também", "nl": "Enviar estados (ack = true) também", "fr": "Envoyer des états (ack = true) aussi", "it": "Invia stati (ack = true) anche", "es": "Enviar estados (ack = true) también", "pl": "Wyślij także stany (ack = true)"},
"Publish all states at start:": { "en": "Publish own states on connect", "de": "Bekanntgeben eigene States beim Verbinden", "ru": "Выдавать собственные значения при старте", "pt": "Publique seus próprios estados na conexão", "nl": "Publique seus próprios estados na conexão", "fr": "Publier ses propres états sur se connecter", "it": "Pubblica i tuoi stati sulla connessione", "es": "Publicar estados propios al conectar", "pl": "Opublikuj własne stany przy połączeniu"},
"Use different topic names for set and get:": { "en": "Use different topic names for set and get", "de": "Unterschiedliche Namen für setzten und lesen", "ru": "Использовать разные имена для чтения и записи", "pt": "Use nomes de tópicos diferentes para definir e obter", "nl": "Use nomes de tópicos diferentes para definir e obter", "fr": "Utilisez différents noms de sujet pour définir et obtenir", "it": "Usa diversi nomi di argomenti per impostare e ottenere", "es": "Use diferentes nombres de tema para establecer y obtener", "pl": "Używaj różnych nazw tematów do ustawiania i pobierania"},
"Publish states on subscribe:": { "en": "Publish states on subscribe", "de": "Bekanntgeben von States bei Subscribe", "ru": "Публиковать состояния при подписке", "pt": "Publique estados na assinatura", "nl": "Publique estados na assinatura", "fr": "Publier les états sur l'abonnement", "it": "Pubblica stati su iscriviti", "es": "Publicar estados al suscribirse", "pl": "Opublikuj stany w subskrypcji"},
"Set certificates or load it first in the system settings (right top).": {"en": "Set certificates or load it first in the system settings (right top).", "de": "Setze Zertificate oder lade die erst unter System/Einstellungen (oben rechts).", "ru": "Нужно выбрать сертификаты или сначала загрузить их в системных настройках (вверху справа).", "pt": "Defina certificados ou carregue primeiro nas configurações do sistema (parte superior direita).", "nl": "Defina certificados ou carregue primeiro nas configurações do sistema (parte superior direita).", "fr": "Définissez des certificats ou chargez-les d'abord dans les paramètres du système (en haut à droite).", "it": "Imposta i certificati o caricali prima nelle impostazioni di sistema (in alto a destra).", "es": "Establezca certificados o cárguelos primero en la configuración del sistema (arriba a la derecha).", "pl": "Ustaw certyfikaty lub załaduj najpierw w ustawieniach systemu (prawy górny)."},
"Prefix for topics:": { "en": "Prefix for all topics", "de": "Prefix für alle Topics", "ru": "Префикс для всех значений", "pt": "Prefix para todos os tópicos", "nl": "Prefix para todos os tópicos", "fr": "Préfixe pour tous les sujets", "it": "Prefisso per tutti gli argomenti", "es": "Prefijo para todos los temas", "pl": "Prefiks dla wszystkich tematów"},
"Max topic length:": { "en": "Max topic name length", "de": "Maximale Topicnamelänge", "ru": "Максимальная длина имени топика", "pt": "Nome máximo do nome do tópico", "nl": "Nome máximo do nome do tópico", "fr": "Longueur maximale du nom de la rubrique", "it": "Lunghezza massima del nome dell'argomento", "es": "Longitud máxima del nombre del tema", "pl": "Maksymalna długość nazwy tematu"},
"Server": { "en": "Server/broker", "de": "Server/broker", "ru": "Сервер/брокер", "pt": "Servidor / corretor", "nl": "Servidor/corretor", "fr": "Serveur/courtier", "it": "Server/mediatore", "es": "Servidor/intermediario", "pl": "Serwer / broker"},
"Client": { "en": "Client/subscriber", "de": "Client/subscriber", "ru": "Клиент/подписчик", "pt": "Cliente / assinante", "nl": "Cliente/assinante", "fr": "Client/abonné", "it": "Client/abbonato", "es": "Cliente/suscriptor", "pl": "Klient / subskrybent"},
"For tele/SENSOR": { "en": "For tele/SENSOR", "de": "Für tele/SENSOR", "ru": "Для tele/SENSOR", "pt": "Para tele/SENSOR", "nl": "Voor tele/SENSOR", "fr": "Pour tele/SENSOR", "it": "Per tele/SENSOR", "es": "Por tele/SENSOR", "pl": "Dla tele/SENSOR"},
"For tele/STATE": { "en": "For tele/STATE", "de": "Für tele/STATE", "ru": "Для tele/STATE", "pt": "Para tele/STATE", "nl": "Voor tele/STATE", "fr": "Pour tele/STATE", "it": "Per tele/STATE", "es": "Por tele/STATE", "pl": "Dla tele/STATE"},
"For stat/RESULT": { "en": "For stat/RESULT", "de": "Für stat/RESULT", "ru": "Для stat/RESULT", "pt": "Para stat/RESULT", "nl": "Voor stat/RESULT", "fr": "Pour stat/RESULT", "it": "Per stat/RESULT", "es": "Por stat/RESULT", "pl": "Dla stat/RESULT"},
"Client timeout (sec):": { "en": "Client timeout (sec)", "de": "Client-Zeitüberschreitung (Sek.)", "ru": "Client timeout (sec):", "pt": "Tempo limite do cliente (seg)", "nl": "Clienttimeout (sec)", "fr": "Délai d'expiration du client (sec)", "it": "Timeout del client (sec)", "es": "Tiempo de espera del cliente (seg)", "pl": "Limit czasu klienta (s)"},
"Auto creation of states": { "en": "Auto creation of states", "de": "Automatische Erstellung von Zuständen", "ru": "Auto creation of states", "pt": "Criação automática de estados", "nl": "Automatisch creëren van staten", "fr": "Création automatique d'états", "it": "Creazione automatica di stati", "es": "Creación automática de estados", "pl": "Automatyczne tworzenie stanów"}
};

26
appveyor.yml Normal file
View File

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

401
gulpfile.js Normal file
View File

@@ -0,0 +1,401 @@
'use strict';
var gulp = require('gulp');
var fs = require('fs');
var pkg = require('./package.json');
var iopackage = require('./io-package.json');
var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
/*var appName = getAppName();
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 1].split('.')[0].toLowerCase();
}
*/
const fileName = 'words.js';
var languages = {
en: {},
de: {},
ru: {},
pt: {},
nl: {},
fr: {},
it: {},
es: {},
pl: {}
};
function lang2data(lang, isFlat) {
var str = isFlat ? '' : '{\n';
var count = 0;
for (var w in lang) {
if (lang.hasOwnProperty(w)) {
count++;
if (isFlat) {
str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n';
} else {
var key = ' "' + w.replace(/"/g, '\\"') + '": ';
str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n';
}
}
}
if (!count) return isFlat ? '' : '{\n}';
if (isFlat) {
return str;
} else {
return str.substring(0, str.length - 2) + '\n}';
}
}
function readWordJs(src) {
try {
var words;
if (fs.existsSync(src + 'js/' + fileName)) {
words = fs.readFileSync(src + 'js/' + fileName).toString();
} else {
words = fs.readFileSync(src + fileName).toString();
}
var lines = words.split(/\r\n|\r|\n/g);
var i = 0;
while (!lines[i].match(/^systemDictionary = {/)) {
i++;
}
lines.splice(0, i);
// remove last empty lines
i = lines.length - 1;
while (!lines[i]) {
i--;
}
if (i < lines.length - 1) {
lines.splice(i + 1);
}
lines[0] = lines[0].replace('systemDictionary = ', '');
lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}');
words = lines.join('\n');
var resultFunc = new Function('return ' + words + ';');
return resultFunc();
} catch (e) {
return null;
}
}
function padRight(text, totalLength) {
return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : '');
}
function writeWordJs(data, src) {
var text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n';
text += '/*global systemDictionary:true */\n';
text += '\'use strict\';\n\n';
text += 'systemDictionary = {\n';
for (var word in data) {
if (data.hasOwnProperty(word)) {
text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50);
var line = '';
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' ';
}
}
if (line) {
line = line.trim();
line = line.substring(0, line.length - 1);
}
text += line + '},\n';
}
}
text = text.replace(/},\n$/, '}\n');
text += '};';
if (fs.existsSync(src + 'js/' + fileName)) {
fs.writeFileSync(src + 'js/' + fileName, text);
} else {
fs.writeFileSync(src + '' + fileName, text);
}
}
const EMPTY = '';
function words2languages(src) {
var langs = Object.assign({}, languages);
var data = readWordJs(src);
if (data) {
for (var word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var keys = Object.keys(langs[l]);
//keys.sort();
var obj = {};
for (var k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
if (!fs.existsSync(src + 'i18n/' + l)) {
fs.mkdirSync(src + 'i18n/' + l);
}
fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj));
}
} else {
console.error('Cannot read or parse ' + fileName);
}
}
function words2languagesFlat(src) {
var langs = Object.assign({}, languages);
var data = readWordJs(src);
if (data) {
for (var word in data) {
if (data.hasOwnProperty(word)) {
for (var lang in data[word]) {
if (data[word].hasOwnProperty(lang)) {
langs[lang][word] = data[word][lang];
// pre-fill all other languages
for (var j in langs) {
if (langs.hasOwnProperty(j)) {
langs[j][word] = langs[j][word] || EMPTY;
}
}
}
}
}
}
var keys = Object.keys(langs.en);
//keys.sort();
for (var l in langs) {
if (!langs.hasOwnProperty(l)) continue;
var obj = {};
for (var k = 0; k < keys.length; k++) {
obj[keys[k]] = langs[l][keys[k]];
}
langs[l] = obj;
}
if (!fs.existsSync(src + 'i18n/')) {
fs.mkdirSync(src + 'i18n/');
}
for (var ll in langs) {
if (!langs.hasOwnProperty(ll)) continue;
if (!fs.existsSync(src + 'i18n/' + ll)) {
fs.mkdirSync(src + 'i18n/' + ll);
}
fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en));
}
fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n'));
} else {
console.error('Cannot read or parse ' + fileName);
}
}
function languagesFlat2words(src) {
var dirs = fs.readdirSync(src + 'i18n/');
var langs = {};
var bigOne = {};
var order = Object.keys(languages);
dirs.sort(function (a, b) {
var posA = order.indexOf(a);
var posB = order.indexOf(b);
if (posA === -1 && posB === -1) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
} else if (posA === -1) {
return -1;
} else if (posB === -1) {
return 1;
} else {
if (posA > posB) return 1;
if (posA < posB) return -1;
return 0;
}
});
var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n');
for (var l = 0; l < dirs.length; l++) {
if (dirs[l] === 'flat.txt') continue;
var lang = dirs[l];
var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n');
langs[lang] = {};
keys.forEach(function (word, i) {
langs[lang][word] = values[i].replace(/<\/ i>/g, '</i>').replace(/<\/ b>/g, '</b>').replace(/<\/ span>/g, '</span>').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']);

184
io-package.json Executable file
View File

@@ -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 <dogafox@gmail.com>"
],
"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": {}
}
]
}

139
lib/datapoints.js Normal file
View File

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

1643
lib/server.js Normal file

File diff suppressed because it is too large Load Diff

83
lib/utils.js Normal file
View File

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

99
main.js Normal file
View File

@@ -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();
}

46
package.json Executable file
View File

@@ -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 <dogafox@gmail.com>",
"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"
}

94
test/lib/mqttClient.js Normal file
View File

@@ -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();
}

1411
test/lib/objects.js Normal file

File diff suppressed because it is too large Load Diff

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

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

708
test/lib/states.js Normal file
View File

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

92
test/testPackageFiles.js Normal file
View File

@@ -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 <my@email.com>');
}
}
else {
expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
console.log();
}
expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
console.log('WARNING: titleLang is not existing in io-package.json. Please add');
console.log();
}
if (
ioPackage.common.title.indexOf('iobroker') !== -1 ||
ioPackage.common.title.indexOf('ioBroker') !== -1 ||
ioPackage.common.title.indexOf('adapter') !== -1 ||
ioPackage.common.title.indexOf('Adapter') !== -1
) {
console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.');
console.log();
}
if (ioPackage.common.name.indexOf('vis-') !== 0) {
if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
console.log('WARNING: Admin3 support is missing! Please add it');
console.log();
}
if (ioPackage.common.materialize) {
expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
}
}
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();
});
});

264
test/testServer.js Normal file
View File

@@ -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());
});
});