commit 398ec83597dd981090c506c1e5a867cc74d02986 Author: zhongjin Date: Tue Dec 25 22:05:19 2018 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4a6c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.idea +tmp +admin/i18n/flat.txt +admin/i18n/*/flat.txt +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f2378af --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +gulpfile.js +tasks +tmp +test +.travis.yml +appveyor.yml +admin/i18n +iob_npm.done +package-lock.json +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2e82b2d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +os: + - linux + - osx +language: node_js +node_js: + - '6' + - '8' + - '10' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install winston@2.3.1 + - 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9499093 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2018 bluefox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ca11bd --- /dev/null +++ b/README.md @@ -0,0 +1,313 @@ +![Logo](admin/mqtt.png) +# yunkong2 MQTT +============== + +[![NPM version](http://img.shields.io/npm/v/yunkong2.mqtt.svg)](https://www.npmjs.com/package/yunkong2.mqtt) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.mqtt.svg)](https://www.npmjs.com/package/yunkong2.mqtt) +[![Tests](https://travis-ci.org/yunkong2/yunkong2.mqtt.svg?branch=master)](https://travis-ci.org/yunkong2/yunkong2.mqtt) + +[![NPM](https://nodei.co/npm/yunkong2.mqtt.png?downloads=true)](https://nodei.co/npm/yunkong2.mqtt/) + +Requires node.js **6.0** or higher. + +# MQ Telemetry Transport for yunkong2 (MQTT). + +MQTT (formerly Message Queue Telemetry Transport) is a publish-subscribe based "light weight" messaging protocol for use on top of the TCP/IP protocol. +It is designed for connections with remote locations where a "small code footprint" is required and/or network bandwidth is limited. +The Publish-Subscribe messaging pattern requires a message broker. The broker is responsible for distributing messages to interested clients based on the topic of a message. +Historically, the 'MQ' in 'MQTT' came from IBM's MQ message queuing product line. + +This adapter uses the MQTT.js library from https://github.com/adamvr/MQTT.js/ + +## Configuration +- **Type** - Select "Client" (If you want to receive and send messages to other broker) or "Server" if you want create own MQTT broker. + +### Server settings +- **WebSockets** - if parallel to TCP Server, the WebSocket MQTT Server should run. +- **Port** - Port where the server will run (Default 1883). **WebSockets** will always run on port+1 (Default 1884) +- **SSL** - If TCP and WebSockets should run as secure server. +- **Authentication/User name** - If authentication required, you can specify username. It is suggested to always use SSL with authentication to not send passwords over unsequre connection. +- **Authentication/Password** - Password for user. +- **Mask to publish own states** - Pattern to filter yunkong2 states, which will be sent to clients. You can use wildcards to specify group of messages, e.g "*.memRss, mqtt.0.*" to get all memory states of all adapters and all states of adapter mqtt.0 +- **Publish only on change** - New messages will be sent to client only if the state value changes. Every message sent by client will be accepted, even if the value does not changed. +- **Publish own states on connect** - by every client connection the all known states will be sent to client (defined by state mask), to say him which states has the yunkong2. +- **Prefix for all topics** - if set, every sent topic will be prepended with this prefix, e.g if prefix "yunkong2/" all states will have names like "**yunkong2**/mqtt/0/connected" +- **Trace output for every message** - Debug outputs. +- **Send states (ack=true) too** - Normally only the states/commands with ack=false will be sent to partner. If this flag is set every state independent from ack will be sent to partner. +- **Use different topic names for set and get** - if active, so every state will have two topics: ```adapter/instance/stateName``` and ```adapter/instance/stateName/set```. In this case topic with "/set" will be used to send non acknowledged commands (ack: false) and topic without "/set" to receive state updates (with ack: true). The client will receive sent messages back in this mode. +- **Interval before send topics by connection** - Pause between connection and when all topics will be sent to client (if activated). +- **Send interval** - Interval between packets by sending all topics (if activated). Used only by once after the connection establishment. +- **Use chunk patch** - There is a problem with last update of mqtt-packet, that frames will be sent directly to client and not first completely built and then sent to client. Some old clients do not like such a packets and do not work with new library. To fix it you can activate this flag. + +### Client settings +- **URL** - name or ip address of the broker/server. Like "localhost". +- **Port** - Port of the MQTT broker. By default 1883 +- **Secure** - If secure (SSL) connection must be used. +- **User** - if broker required authentication, define here the user name. +- **Password** - if user name is not empty the password must be set. It can be empty. +- **Password confirmation** - repeat here the password. +- **Subscribe Patterns** - Subscribe pattern. See chapter "Examples of using wildcards" to define the pattern. '#' to subscribe for all topics. 'mqtt/0/#,javascript/#' to subscribe for states of mqtt.0 and javascript +- **Publish only on change** - Store incoming messages only if payload is differ from actual stored. +- **Mask to publish own states** - Mask for states, that must be published to broker. '*' - to publish all states. 'io.yr.*,io.hm-rpc.0.*' to publish states of "yr" and "hm-rpc" adapter. +- **Publish all states at start** - Publish all states (defined by state mask) every time by connection establishment to announce own available states and their values. +- **Prefix for topics** - The prefix can be defined for own states. Like "/var/yunkong2/". Name of topics will be for example published with the name "/var/yunkong2/ping/192-168-1-5". +- **Test connection** - Press the button to check the connection to broker. Adapter must be enabled before. +- **Send states (ack=true) too** - Normally only the states/commands with ack=false will be sent to partner. If this flag is set every state independent from ack will be sent to partner. +- **Use different topic names for set and get** - if active, so every state will have two topics: ```adapter/instance/stateName``` and ```adapter/instance/stateName/set```. In this case topic with "/set" will be used to send non acknowledged commands (ack: false) and topic without "/set" to receive state updates (with ack: true). + +## Install + +```node yunkong2.js add mqtt``` + +## Usage + +### How to test mqtt client: +- Set type to "Client". +- Leave port on 1883. +- Set URL as "broker.mqttdashboard.com" +- To get absolutely all topics(messages) set pattern to "#". +- To receive all topics for "/4MS" set pattern to "/4MS/#" +- To receive all topics for "/MS and "/floorish" set pattern to "/4MS/#, /floorish/#" + +### Examples of using wildcards +The following examples on the use of wildcards, builds on the example provided in topic strings. + +- "Sport" +- "Sport/Tennis" +- "Sport/Basketball" +- "Sport/Swimming" +- "Sport/Tennis/Finals" +- "Sport/Basketball/Finals" +- "Sport/Swimming/Finals" + +If you want to subscribe to all Tennis topics, you can use the number sign '#', or the plus sign '+'. + +- "Sport/Tennis/#" (this will receive "Sport/Tennis" and "Sport/Tennis/Finals") +- "Sport/Tennis/+" (this will receive "Sport/Tennis/Finals" but not "Sport/Tennis") + +For JMS topics, if you want to subscribe to all Finals topics, you can use the number sign '#', or the plus sign '+'. + +- "Sport/#/Finals" +- "Sport/+/Finals" + +For MQTT topics, if you want to subscribe to all Finals topics, you can use the plus sign '+' . + +"Sport/+/Finals" + +### Tests +The broker was tested with following clients: + +- http://mitsuruog.github.io/what-mqtt/ +- http://mqttfx.jfx4ee.org/ +- http://www.eclipse.org/paho/clients/tool/ + +## Todo +* Implement resend of "QoS 2" messages after a while. + Whenever a packet gets lost on the way, the sender is responsible for resending the last message after a reasonable amount of time. This is true when the sender is a MQTT client and also when a MQTT broker sends a message. + +* queue packets with "QoS 1/2" for the offline clients with persistent session. + [Read here](https://www.hivemq.com/blog/mqtt-essentials-part-7-persistent-session-queuing-messages) + +## Changelog +### 2.0.4 (2018-12-01) +* (Apollon77) Subscribe to topics after connect + +### 2.0.3 (2018-08-11) +* (bluefox) Prefix in server was corrected + +### 2.0.2 (2018-08-09) +* (bluefox) Behaviour of "set" topics was changed + +### 2.0.1 (2018-07-06) +* (bluefox) Double prefix by client was fixed + +### 2.0.0 (2018-03-05) +* (bluefox) broke node.js 4 support +* (bluefox) remove mqtt-stream-server +* (bluefox) partial mqtt5 support + +### 1.5.0 (2018-03-05) +* (bluefox) The patch for wifi-iot removed +* (bluefox) the mqtt library updated +* (bluefox) implement QoS>0 + +### 1.4.2 (2018-01-30) +* (bluefox) Admin3 settings are corrected + +### 1.4.1 (2018-01-13) +* (bluefox) Convert error is caught +* (bluefox) Ready for admin3 + +### 1.3.3 (2017-10-15) +* (bluefox) Fix sending of QOS=2 if server + +### 1.3.2 (2017-02-08) +* (bluefox) Fix convert of UTF8 payloads +* (bluefox) optional fix for chunking problem + +### 1.3.1 (2017-02-02) +* (bluefox) Update mqtt packages +* (bluefox) add Interval before send topics by connection ans send interval settings +* (bluefox) reorganise configuration dialog + +### 1.3.0 (2017-01-07) +* (bluefox) Update mqtt packages +* (bluefox) configurable client ID + +### 1.2.5 (2016-11-24) +* (bluefox) Fix server publishing + +### 1.2.4 (2016-11-13) +* (bluefox) additional debug output + +### 1.2.1 (2016-11-06) +* (bluefox) fix publish on start + +### 1.2.0 (2016-09-27) +* (bluefox) implementation of LWT for server +* (bluefox) update mqtt package version + +### 1.1.2 (2016-09-13) +* (bluefox) fix authentication in server + +### 1.1.1 (2016-09-12) +* (bluefox) do not parse JSON states, that do not have attribute "val" to support other systems + +### 1.1.0 (2016-07-23) +* (bluefox) add new setting: Use different topic names for set and get + +### 1.0.4 (2016-07-19) +* (bluefox) convert values like "+58,890" into numbers too + +### 1.0.3 (2016-05-14) +* (cdjm) change client protocolID + +### 1.0.2 (2016-04-26) +* (bluefox) update mqtt module + +### 1.0.1 (2016-04-25) +* (bluefox) Fix translations in admin + +### 1.0.0 (2016-04-22) +* (bluefox) Fix error with direct publish in server + +### 0.5.0 (2016-03-15) +* (bluefox) fix web sockets +* (bluefox) fix SSL + +### 0.4.2 (2016-02-10) +* (bluefox) create object "info.connection" +* (bluefox) add reconnection tests + +### 0.4.1 (2016-02-04) +* (bluefox) fix error with states creation + +### 0.4.0 (2016-01-27) +* (bluefox) add tests +* (bluefox) client and server run + +### 0.3.1 (2016-01-14) +* (bluefox) change creation of states by client + +### 0.3.0 (2016-01-13) +* (bluefox) try to fix event emitter + +### 0.2.15 (2015-11-23) +* (Pmant) fix publish on subscribe + +### 0.2.14 (2015-11-21) +* (bluefox) fix error with wrong variable names + +### 0.2.13 (2015-11-20) +* (Pmant) fix error with wrong variable name + +### 0.2.12 (2015-11-14) +* (Pmant) send last known value on subscription (server) + +### 0.2.11 (2015-10-17) +* (bluefox) set maximal length of topic name +* (bluefox) convert "true" and "false" to boolean values + +### 0.2.10 (2015-09-16) +* (bluefox) protect against empty topics + +### 0.2.8 (2015-05-17) +* (bluefox) do not ty to parse JSON objects + +### 0.2.7 (2015-05-16) +* (bluefox) fix test button + +### 0.2.6 (2015-05-16) +* (bluefox) fix names if from mqtt adapter + +### 0.2.5 (2015-05-15) +* (bluefox) subscribe to all states if no mask defined + +### 0.2.4 (2015-05-14) +* (bluefox) add state "clients" to server with the list of clients + +### 0.2.3 (2015-05-14) +* (bluefox) fix some errors + +### 0.2.2 (2015-05-13) +* (bluefox) fix some errors with sendOnStart and fix flag sendAckToo + +### 0.2.0 (2015-05-13) +* (bluefox) translations and rename config sendNoAck=>sendAckToo +* (bluefox) lets create server not only on localhost + +### 0.1.8 (2015-05-13) +* (bluefox) fix topic names in server mode +* (bluefox) implement subscribe +* (bluefox) update mqtt package + +### 0.1.7 (2015-03-24) +* (bluefox) create objects if new state received +* (bluefox) update mqtt library + +### 0.1.6 (2015-03-04) +* (bluefox) just update index.html + +### 0.1.5 (2015-01-02) +* (bluefox) fix error if state deleted + +### 0.1.4 (2015-01-02) +* (bluefox) support of npm install + +### 0.1.2 (2014-11-28) +* (bluefox) support of npm install + +### 0.1.1 (2014-11-22) +* (bluefox) support of new naming concept + +### 0.1.0 (2014-10-23) +* (bluefox) Update readme +* (bluefox) Support of authentication for server and client +* (bluefox) Support of prefix for own topics + +### 0.0.2 (2014-10-19) +* (bluefox) support of server (actual no authentication) + +## License + +The MIT License (MIT) + +Copyright (c) 2014-2018, bluefox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/admin/i18n/cn/translations.json b/admin/i18n/cn/translations.json new file mode 100644 index 0000000..8277265 --- /dev/null +++ b/admin/i18n/cn/translations.json @@ -0,0 +1,46 @@ +{ + "MQTT adapter settings": "MQTT 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", + "MQTT Settings": "MQTT 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": "MQTT 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", + "Enable first the adapter to test client.": "Enable first the adapter to test client.", + "First save the adapter": "First save the adapter" +} \ No newline at end of file diff --git a/admin/i18n/en/translations.json b/admin/i18n/en/translations.json new file mode 100644 index 0000000..8277265 --- /dev/null +++ b/admin/i18n/en/translations.json @@ -0,0 +1,46 @@ +{ + "MQTT adapter settings": "MQTT 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", + "MQTT Settings": "MQTT 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": "MQTT 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", + "Enable first the adapter to test client.": "Enable first the adapter to test client.", + "First save the adapter": "First save the adapter" +} \ No newline at end of file diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..402b4bb --- /dev/null +++ b/admin/index.html @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + +
+ + + +

MQTT adapter settings

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

Main settings

Connection settings

Authentication settings

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

Adapter settings

Divided by comma
Mask to publish
chars
ms
ms
+
+
+
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..5359dfd --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,406 @@ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+
+
+
+ + +
+
+ + Use WebSockets: +
+
+
Connection settings
+
+
+ + +
+
+ + +
+
+
+
+ + Secure: +
+
+ + +
+
+ + +
+
+ + +
+
+
Authentication settings
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+ +
+
+
+
+ + ( + Divided by comma) +
+
+ + +
+
+ + ( + Mask to publish) +
+
+
+
+ + Store only on change: +
+
+ + Publish all states at start: +
+
+ + Publish states on subscribe: +
+
+ + Trace output for every message: +
+
+ + Send states (ack=true) too: +
+
+ + Use different topic names for set and get: +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + Default retain flag: +
+
+
+
+
+ + diff --git a/admin/mqtt.png b/admin/mqtt.png new file mode 100644 index 0000000..7a82b4d Binary files /dev/null and b/admin/mqtt.png differ diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..40bc97b --- /dev/null +++ b/admin/words.js @@ -0,0 +1,50 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n +/*global systemDictionary:true */ +'use strict'; + +systemDictionary = { + "MQTT adapter settings": { "en": "MQTT adapter settings", "de": "MQTT Adapter-Einstellungen", "ru": "Настройки драйвера MQTT", "pt": "MQTT adapter settings", "nl": "MQTT adapter settings", "fr": "MQTT adapter settings", "it": "MQTT adapter settings", "es": "MQTT adapter settings", "pl": "MQTT adapter settings"}, + "Type:": { "en": "Type", "de": "Typ", "ru": "Тип", "pt": "Type", "nl": "Type", "fr": "Type", "it": "Type", "es": "Type", "pl": "Type"}, + "Port:": { "en": "Port", "de": "Port", "ru": "Порт", "pt": "Port", "nl": "Port", "fr": "Port", "it": "Port", "es": "Port", "pl": "Port"}, + "User:": { "en": "User", "de": "Username", "ru": "Имя пользователя", "pt": "User", "nl": "User", "fr": "User", "it": "User", "es": "User", "pl": "User"}, + "Password:": { "en": "Password", "de": "Kennwort", "ru": "Пароль", "pt": "Password", "nl": "Password", "fr": "Password", "it": "Password", "es": "Password", "pl": "Password"}, + "Password confirmation:": { "en": "Password confirmation", "de": "Kennwort-Wiederholung", "ru": "Подтверждение пароля", "pt": "Password confirmation", "nl": "Password confirmation", "fr": "Password confirmation", "it": "Password confirmation", "es": "Password confirmation", "pl": "Password confirmation"}, + "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": "Secure", "nl": "Secure", "fr": "Secure", "it": "Secure", "es": "Secure", "pl": "Secure"}, + "Public certificate:": { "en": "Public certificate", "de": "Publikzertifikat", "ru": "'Public' сертификат", "pt": "Public certificate", "nl": "Public certificate", "fr": "Public certificate", "it": "Public certificate", "es": "Public certificate", "pl": "Public certificate"}, + "Private certificate:": { "en": "Private certificate", "de": "Privatzertifikat", "ru": "'Private' сертификат", "pt": "Private certificate", "nl": "Private certificate", "fr": "Private certificate", "it": "Private certificate", "es": "Private certificate", "pl": "Private certificate"}, + "Chained certificate:": { "en": "Chained certificate", "de": "Kettenzertifikat", "ru": "'Chained' сертификат", "pt": "Chained certificate", "nl": "Chained certificate", "fr": "Chained certificate", "it": "Chained certificate", "es": "Chained certificate", "pl": "Chained certificate"}, + "Patterns:": { "en": "Subscribe patterns", "de": "Subscribe patterns", "ru": "Patterns", "pt": "Subscribe patterns", "nl": "Subscribe patterns", "fr": "Subscribe patterns", "it": "Subscribe patterns", "es": "Subscribe patterns", "pl": "Subscribe patterns"}, + "Use WebSockets:": { "en": "Use WebSockets too", "de": "Benutze auch WebSockets", "ru": "Сервер WebSockets тоже", "pt": "Use WebSockets too", "nl": "Use WebSockets too", "fr": "Use WebSockets too", "it": "Use WebSockets too", "es": "Use WebSockets too", "pl": "Use WebSockets too"}, + "Connection": { "en": "Connection", "de": "Verbindung", "ru": "Соединение", "pt": "Connection", "nl": "Connection", "fr": "Connection", "it": "Connection", "es": "Connection", "pl": "Connection"}, + "MQTT Settings": { "en": "MQTT Settings", "de": "MQTT Einstellungen", "ru": "Настройки MQTT", "pt": "MQTT Settings", "nl": "MQTT Settings", "fr": "MQTT Settings", "it": "MQTT Settings", "es": "MQTT Settings", "pl": "MQTT Settings"}, + "Client ID:": { "en": "Client ID", "de": "Client ID", "ru": "ID Клиента", "pt": "Client ID", "nl": "Client ID", "fr": "Client ID", "it": "Client ID", "es": "Client ID", "pl": "Client ID"}, + "chars": { "en": "chars", "de": "Symbolen", "ru": "символов", "pt": "chars", "nl": "chars", "fr": "chars", "it": "chars", "es": "chars", "pl": "chars"}, + "ms": { "en": "ms", "de": "ms", "ru": "мс", "pt": "ms", "nl": "ms", "fr": "ms", "it": "ms", "es": "ms", "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": "Interval before send topics by connection", "nl": "Interval before send topics by connection", "fr": "Interval before send topics by connection", "it": "Interval before send topics by connection", "es": "Interval before send topics by connection", "pl": "Interval before send topics by connection"}, + "Send interval:": { "en": "Send interval", "de": "Sendeintervall", "ru": "Интервал между пакетами", "pt": "Send interval", "nl": "Send interval", "fr": "Send interval", "it": "Send interval", "es": "Send interval", "pl": "Send interval"}, + "Use chunk patch:": { "en": "Use chunk patch", "de": "Benutze Patch für Chunking", "ru": "Использовать заплатку для Chunking", "pt": "Use chunk patch", "nl": "Use chunk patch", "fr": "Use chunk patch", "it": "Use chunk patch", "es": "Use chunk patch", "pl": "Use chunk patch"}, + "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": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'", "nl": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'", "fr": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'", "it": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'", "es": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'", "pl": "Divided by comma, e.g. 'mqtt/0/#,javascript/#'"}, + "Mask to publish": { "en": "e.g. 'mqtt.0.*,javascript.*'", "de": "z.B 'mqtt.0.*,javascript.*'", "ru": "Использовать запятую, как разделитеть. Например 'mqtt.0.*,javascript.*'", "pt": "e.g. 'mqtt.0.*,javascript.*'", "nl": "e.g. 'mqtt.0.*,javascript.*'", "fr": "e.g. 'mqtt.0.*,javascript.*'", "it": "e.g. 'mqtt.0.*,javascript.*'", "es": "e.g. 'mqtt.0.*,javascript.*'", "pl": "e.g. 'mqtt.0.*,javascript.*'"}, + "Store only on change:": { "en": "Publish only on change", "de": "Publish nur bei Änderung", "ru": "Отсылать только изменения", "pt": "Publish only on change", "nl": "Publish only on change", "fr": "Publish only on change", "it": "Publish only on change", "es": "Publish only on change", "pl": "Publish only on change"}, + "Trace output for every message:": { "en": "Trace output for every message", "de": "Trace Ausgabe für jede Meldung", "ru": "Вывод лога для каждого изменения", "pt": "Trace output for every message", "nl": "Trace output for every message", "fr": "Trace output for every message", "it": "Trace output for every message", "es": "Trace output for every message", "pl": "Trace output for every message"}, + "Test connection": { "en": "Test connection with Server", "de": "Teste Verbindung zum Server", "ru": "Проверить настройки", "pt": "Test connection with Server", "nl": "Test connection with Server", "fr": "Test connection with Server", "it": "Test connection with Server", "es": "Test connection with Server", "pl": "Test connection with Server"}, + "Result: ": { "en": "Result: ", "de": "Ergebnis: ", "ru": "Результат: ", "pt": "Result: ", "nl": "Result: ", "fr": "Result: ", "it": "Result: ", "es": "Result: ", "pl": "Result: "}, + "connected": { "en": "connected", "de": "verbunden", "ru": "успешно", "pt": "connected", "nl": "connected", "fr": "connected", "it": "connected", "es": "connected", "pl": "connected"}, + "Main settings": { "en": "Main settings", "de": "Allgemeine Einstellungen", "ru": "Основные настройки", "pt": "Main settings", "nl": "Main settings", "fr": "Main settings", "it": "Main settings", "es": "Main settings", "pl": "Main settings"}, + "Connection settings": { "en": "Connection settings", "de": "Verbindungseinstellungen", "ru": "Настройки соединения", "pt": "Connection settings", "nl": "Connection settings", "fr": "Connection settings", "it": "Connection settings", "es": "Connection settings", "pl": "Connection settings"}, + "Authentication settings": { "en": "Authentication settings", "de": "Authentication Einstellungen", "ru": "Настройки аутентификации", "pt": "Authentication settings", "nl": "Authentication settings", "fr": "Authentication settings", "it": "Authentication settings", "es": "Authentication settings", "pl": "Authentication settings"}, + "Adapter settings": { "en": "MQTT settings", "de": "MQTT Einstellungen", "ru": "Настройки MQTT", "pt": "MQTT settings", "nl": "MQTT settings", "fr": "MQTT settings", "it": "MQTT settings", "es": "MQTT settings", "pl": "MQTT settings"}, + "Mask to publish own states:": { "en": "Mask to publish own states", "de": "Maske für Bekanntgeben von eigenen States", "ru": "Маска для собственных значений", "pt": "Mask to publish own states", "nl": "Mask to publish own states", "fr": "Mask to publish own states", "it": "Mask to publish own states", "es": "Mask to publish own states", "pl": "Mask to publish own states"}, + "Send states (ack=true) too:": { "en": "Send states (ack=true) too", "de": "Sende auch Zustände (ack=true)", "ru": "Посылать не только команды, но и состояния (ack=true)", "pt": "Send states (ack=true) too", "nl": "Send states (ack=true) too", "fr": "Send states (ack=true) too", "it": "Send states (ack=true) too", "es": "Send states (ack=true) too", "pl": "Send states (ack=true) too"}, + "Publish all states at start:": { "en": "Publish own states on connect", "de": "Bekanntgeben eigene States beim Verbinden", "ru": "Выдавать собственные значения при старте", "pt": "Publish own states on connect", "nl": "Publish own states on connect", "fr": "Publish own states on connect", "it": "Publish own states on connect", "es": "Publish own states on connect", "pl": "Publish own states on connect"}, + "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 different topic names for set and get", "nl": "Use different topic names for set and get", "fr": "Use different topic names for set and get", "it": "Use different topic names for set and get", "es": "Use different topic names for set and get", "pl": "Use different topic names for set and get"}, + "Publish states on subscribe:": { "en": "Publish states on subscribe", "de": "Bekanntgeben von States bei Subscribe", "ru": "Публиковать состояния при подписке", "pt": "Publish states on subscribe", "nl": "Publish states on subscribe", "fr": "Publish states on subscribe", "it": "Publish states on subscribe", "es": "Publish states on subscribe", "pl": "Publish states on subscribe"}, + "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": "Set certificates or load it first in the system settings (right top).", "nl": "Set certificates or load it first in the system settings (right top).", "fr": "Set certificates or load it first in the system settings (right top).", "it": "Set certificates or load it first in the system settings (right top).", "es": "Set certificates or load it first in the system settings (right top).", "pl": "Set certificates or load it first in the system settings (right top)."}, + "Prefix for topics:": { "en": "Prefix for all topics", "de": "Prefix für alle Topics", "ru": "Префикс для всех значений", "pt": "Prefix for all topics", "nl": "Prefix for all topics", "fr": "Prefix for all topics", "it": "Prefix for all topics", "es": "Prefix for all topics", "pl": "Prefix for all topics"}, + "Max topic length:": { "en": "Max topic name length", "de": "Maximale Topicnamelänge", "ru": "Максимальная длина имени топика", "pt": "Max topic name length", "nl": "Max topic name length", "fr": "Max topic name length", "it": "Max topic name length", "es": "Max topic name length", "pl": "Max topic name length"}, + "Server": { "en": "Server/broker", "de": "Server/broker", "ru": "Сервер/брокер", "pt": "Server/broker", "nl": "Server/broker", "fr": "Server/broker", "it": "Server/broker", "es": "Server/broker", "pl": "Server/broker"}, + "Client": { "en": "Client/subscriber", "de": "Client/subscriber", "ru": "Клиент/подписчик", "pt": "Client/subscriber", "nl": "Client/subscriber", "fr": "Client/subscriber", "it": "Client/subscriber", "es": "Client/subscriber", "pl": "Client/subscriber"}, + "Enable first the adapter to test client.": { "en": "Enable first the adapter to test client.", "de": "Aktivieren Sie zuerst den Adapter, um den Client zu testen.", "ru": "Включите сначала адаптер для тестирования клиента.", "pt": "Ative primeiro o adaptador para testar o cliente.", "nl": "Schakel eerst de adapter in om de client te testen.", "fr": "Activez d'abord l'adaptateur pour tester le client.", "it": "Abilitare prima l'adattatore per testare il client.", "es": "Primero habilite el adaptador para probar el cliente.", "pl": "Najpierw włącz adapter, aby przetestować klienta."}, + "First save the adapter": { "en": "First save the adapter", "de": "Speichern Sie zuerst den Adapter", "ru": "Сначала сохраните адаптер", "pt": "Primeiro salve o adaptador", "nl": "Sla eerst de adapter op", "fr": "D'abord enregistrer l'adaptateur", "it": "Per prima cosa salva l'adattatore", "es": "Primero guarde el adaptador", "pl": "Najpierw zapisz adapter"}, +}; \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..0df2afa --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,24 @@ +version: 'test-{build}' +environment: + matrix: + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - ps: '$NpmVersion = (npm -v).Substring(0,1)' + - ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }' + - ps: npm --version + - npm install + - npm install winston@2.3.1 + - 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - npm test +build: 'off' diff --git a/docs/de/mqtt.md b/docs/de/mqtt.md new file mode 100644 index 0000000..a05240b --- /dev/null +++ b/docs/de/mqtt.md @@ -0,0 +1,695 @@ +# Beschreibung + +[MQTT](http://mqtt.org/) (Message Queue Telemetry Transport) ist ein schlankes Protokoll für die Kommunikation zwischen verschiedenen Geräten (M2M - machine-to-machine). Es benutzt das publisher-subscriber Modell um Nachrichten über das TCP / IP Protokoll zu senden. Die zentrale Stelle des Protokolls ist der MQTT-Server oder Broker Der Zugriff auf den publisher und den subscriber besitzt. Dieses Protokoll ist sehr simpel: ein kurzer Header ohne Integrität (deshalb setzt die Übermittlung auf TCP auf), legt der Struktur keinerlei Beschränkungen beim Code oder einem Datenbankschema auf. Die einzige Bedingung ist dass jedes Datenpaket eine Information zur Identifikation beinhalten muss. Diese Identifikationsinformation heißt Topic Name. + +Das MQTT Protokoll benötigt einen Datenbroker. Dieses ist die zentrale Idee dieser Technologie. Alle Geräte senden ihre Daten nur zu diesem Broker und erhalten ihre Informationen auch nur von ihm. Nach dem Empfang des Paketes sendet der Broker es zu allen Geräten in dem Netzwerk, die es abonniert haben. Wenn ein Gerät etwas von dem Broker möchte, muss er das entsprechende Topic abonnieren. Topics entstehen dynamisch bei Abonnement oder beim Empfang eines Paketes mit diesem Topic. Nach dem Abonnement eines Topics braucht man nichts mehr zu tun. Deswegen sind Topics sehr bequem um verschiedenen Beziehungen zu organisieren: one-to-many, many-to-one and many-to-many. + +**Wichtig:** + +* Die Geräte stellen selber die Kommunikation mit dem Broker her. Sie können hniter einer NAT liegen und  müssen keine feste IP besitzen, +* Man kann den Traffic mit SSL absichern, +* MQTT Broker ermöglichen es diese über ein Websocket Protokoll auf Port 80 zu erreichen, +* Mehrere Broker können miteinander verbunden werden und abonnieren dann die Nachrichte gegenseitig. + +# Installation + +Die Installation findet im Admin im Reiter _**Adapter**_ statt. In der Gruppe Kommunikation befindet sich eine Zeile **_MQTT Adapter_**, dort wird über das (+)-Icon ganz rechts eine neue Instanz angelegt. + +[![](http://www.yunkong2.net/wp-content/uploads//1-1024x342.png)](http://www.yunkong2.net/wp-content/uploads//1.png) + +Ein pop-up Fenster erscheint mit den Installationsinformationen und schließt nach der Installation eigenständig. + +[![](http://www.yunkong2.net/wp-content/uploads//2-300x153.png)](http://www.yunkong2.net/wp-content/uploads//2.png) + +Wenn alles klappt befindet sich anschließend unter dem Reiter _**Instanzen**_ die neu installierte **mqtt.0** Instanz. + +[![](http://www.yunkong2.net/wp-content/uploads//3-300x156.png)](http://www.yunkong2.net/wp-content/uploads//3.png)   + +# Konfiguration + +Wie bereits oben gesagt, besteht ein MQTT-System aus einem Broker und Clients.  Der yunkong2 Server kann als Broker oder als Client arbeiten.  Entsprechend des gewünschten Modus wird der Typ auf _**server/broker**_ oder **_Client/subscriber_** eingestellt. Hier sollte die Einstellung gut überlegt werden. + +## yunkong2 als MQTT-Broker + +Die Grundeinstellungen um den Adapter als Server/Broker zu verwenden sind in der Abbildung gezeigt: + +[![](http://www.yunkong2.net/wp-content/uploads/yunkong2_Adapter_MQTT_Konfig_Server.jpg)](http://www.yunkong2.net/wp-content/uploads//yunkong2_Adapter_MQTT_Konfig_Server.jpg) + +### Allgemeine Einstellungen + +* **Typ** - Entsprechend der gewünschten Verwendung wird der Typ auf _**server/broker**_ oder **_Client/subscriber_** eingestellt +* **Use WebSockets** - Wenn man Websockets für die Verbindung benötigt, muss diese Checkbox aktiviert werden. Dann läuft der TCP-Server parallel zum WebSocket Server, +* **Port** - Der Port um mit TCP zu verbinden (default: 1883),  ein WebSocket Server (siehe oben) läuft einen Port höher (default: 1884), + +### Verbindungseinstellungen + +* **SSL** - Diese Option wird benötigt um den gesamten Datenverkehr zu verschlüsseln (TCP und WebSocket), deshalb muss man in den jetzt zur Verfügung stehenden Feldern die zu verwendenden Zertifikate angeben. In den Pulldowns kann man diese aus den in den Systemeinstellungen angelegten Zertifikaten auswählen, + +![](yunkong2_Adapter_MQTT_Konfig_Server_SSH.jpg) + +### Authentication Einstellungen + +* **Username** und **Passwort** - Wenn gewünscht kann hier ein Username und ein Passwort vergeben werden. Dies muss auf jeden Fall mit SSH Verschlüsselung benutzt werden, damit Passworte nicht unverschlüsselt übertragen werden. + +### MQTT Einstellungen + +* **Maske für Bekanntgeben von eigenen States** - Diese Maske (oder mehrere durch Komma getrennt)dienen dazu die Variablen zu filtern, die an den Client geschickt werden sollen. Man kann Sonderzeichen angeben um eine Gruppe von Nachrichten zu definieren (z.B.  `memRSS, mqtt.0` - sendet alle Variablen zum memory status aller Adapter und alle variablen der **mqtt.0 Adapter** Instanz), +* **Publish nur bei Änderungen** - sending data to the client will be made only in case of change of a variable (if the state simply update - the value is not changed, the customer message will not be sent) from the client will be accepted any message, even if the value has not changed, +* **To give private values at startup** - for each successful client connection will be transferred to all known states (defined by the mask state) – in order to tell the client about the current state of the yunkong2, +* **Post status subscribes** - immediately after the subscription will be sent to the customer value of the variable on which it is signed (at the first start or restart the client will receive the values of variables on which it is signed, can be used to initialize variables), +* **The prefix for all values** - if the specified value, it will be added as a prefix to every sent topic, for example, if you specify yunkong2/, then all topics sent along the following lines: `yunkong2/mqtt/0/connected`, +* **Output log for each change** - in the log file will display debugging information for each change, +* **To send not only commands, but also the state (ack=true)** - if this option is not active, the client will only send variables/commands with ack=false, if the flag is set, then variables will be transferred regardless of the state of ack (false / true), +* **The maximum length of the name of a topic** - the maximum number of characters for the description of the topic, including service. + +As an example, consider the exchange of data between the client based on the [arduino board](https://www.arduino.cc/) and the broker is an instance of mqtt.0 driver system yunkong2. + +* - the client – the fee for developing [arduino UNO](https://www.arduino.cc/en/Main/ArduinoBoardUno) + [ethernet shield](https://store.arduino.cc/product/A000072) based on W5100 chip, +* - to work with the ethernet board uses the standard [library](https://www.arduino.cc/en/Reference/Ethernet) for working with MQTT library [Pubsubclient](https://github.com/knolleary/pubsubclient), +* - the AM2302 sensor (temperature and humidity) connected to pin_8 for the survey used library with DHTlib with [DHTlib](https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib) resource github.com, +* - led **led_green** is connected to pin_9, control in discrete mode on/off +* - broker – yunkong2 system driver mqtt. + +Format topics of data exchange: + +* `example1/send_interval` - client signed to change the transmission interval of the temperature readings and humidity (int value in seconds), +* `example1/temp` - client publishes a specified temperature interval with DHT22 sensor (float type), +* `example1/hum` - client publishes a specified humidity value intervals with DHT22 sensor (float type), +* `example1/led` - the client is subscribed to the state change of the led (the text on/off or 0/1 or true/false). + +Driver settings will be as follows: + +[![](http://www.yunkong2.net/wp-content/uploads//5-283x300.png)](http://www.yunkong2.net/wp-content/uploads//5.png) + +Connecting via TCP (WebSocket is not necessary), default port 1883\. The client within the local network, so to encrypt traffic and authenticate the user is not necessary. We will send only the changes since the client signed on the send interval indications and led state to obtain information about the update (without changing the value) to a variable makes no sense. To publish the subscription - note this option, as when you first connect (or connected after disconnection) of the client, he must know the state of the variables on which it is signed (a current interval of sending and whether the LED is to be turned on). Setting to send variables ack = true or false is also worth noting, as a variable (which signed the client) can change any driver / script / VIS and any changes should be sent to the client. The complete code for the arduino board will look like this: + +
// Connecting libraries
+#include
+#include
+#include //https://github.com/knolleary/pubsubclient
+#include //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+//Settings of a network
+byte mac[] = {
+  0xAB,
+  0xBC,
+  0xCD,
+  0xDE,
+  0xEF,
+  0x31
+};
+byte ip[] = {
+  192,
+  168,
+  69,
+  31
+}; //arduino board IP address
+byte mqttserver[] = {
+  192,
+  168,
+  69,
+  51
+}; // yunkong2 server IP address
+EthernetClient ethClient;
+void callback(char * topic, byte * payload, unsigned int length);
+PubSubClient client(mqttserver, 1883, callback, ethClient);
+//Global variables
+#define LED_pin 9
+unsigned int send_interval = 10; // the sending interval of indications to the server, by default 10 seconds
+unsigned long last_time = 0; // the current time for the timer
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+// The processing function for incoming connections - reception of data on a subscription
+void callback(char * topic, byte * payload, unsigned int length) {
+  Serial.println("");
+  Serial.println("-------");
+  Serial.println("New callback of MQTT-broker");
+  // let's transform a subject (topic) and value (payload) to a line
+  payload[length] = '\0';
+  String strTopic = String(topic);
+  String strPayload = String((char * ) payload);
+  // research that "arrived" from the server on a subscription::
+  // Change of an interval of inquiry
+  if (strTopic == "example1/send_interval") {
+    int tmp = strPayload.toInt();
+    if (tmp == 0) {
+      send_interval = 10;
+    } else {
+      send_interval = strPayload.toInt();
+    }
+  }
+  // Control of a LED
+  if (strTopic == "example1/led") {
+    if (strPayload == "off" || strPayload == "0" || strPayload == "false") digitalWrite(LED_pin, LOW);
+    if (strPayload == "on" || strPayload == "1" || strPayload == "true") digitalWrite(LED_pin, HIGH);
+  }
+  Serial.print(strTopic);
+  Serial.print(" ");
+  Serial.println(strPayload);
+  Serial.println("-------");
+  Serial.println("");
+}
+void setup() {
+  Serial.begin(9600);
+  Serial.println("Start...");
+  // start network connection
+  Ethernet.begin(mac, ip);
+  Serial.print("IP: ");
+  Serial.println(Ethernet.localIP());
+  // initialize input/output ports, register starting values
+  pinMode(LED_pin, OUTPUT);
+  digitalWrite(LED_pin, LOW); // when the LED is off
+}
+void loop() {
+  // If the MQTT connection inactively, then we try to set it and to publish/subscribe
+  if (!client.connected()) {
+    Serial.print("Connect to MQTT-boker... ");
+    // Connect and publish / subscribe
+    if (client.connect("example1")) {
+      Serial.println("success");
+      // Value from sensors
+      if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example1/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example1/temp", buff);
+      }
+      // subscribe for an inquiry interval
+      client.subscribe("example1/send_interval");
+      // subscribe to the LED control variable
+      client.subscribe("example1/led");
+    } else {
+      // If weren't connected, we wait for 10 seconds and try again
+      Serial.print("Failed, rc=");
+      Serial.print(client.state());
+      Serial.println(" try again in 10 seconds");
+      delay(10000);
+    }
+    // If connection is active, then sends the data to the server with the specified time interval
+  } else {
+    if (millis() & gt;
+      (last_time + send_interval * 1000)) {
+      last_time = millis();
+      if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example1/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example1/temp", buff);
+      }
+    }
+  }
+  // Check of incoming connections on a subscription
+  client.loop();
+}
+
+ +The result of the part of the broker (temperature and humidity data is updated with the preset time period): [![](http://www.yunkong2.net/wp-content/uploads//6-1024x201.png)](http://www.yunkong2.net/wp-content/uploads//6.png) The result of the client-side (incoming data subscription output to the console for debugging): [![](http://www.yunkong2.net/wp-content/uploads//MQTT-server4.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-server4.jpg)   + +## yunkong2 working as MQTT-client + +For an instance MQTT driver earned as a client / subscriber - you need to choose the appropriate type of configuration. In this set of options will change slightly: [![](http://www.yunkong2.net/wp-content/uploads//yunkong2_Adapter_MQTT_Konfig_Client.jpg)](http://www.yunkong2.net/wp-content/uploads//yunkong2_Adapter_MQTT_Konfig_Client.jpg)   + +### Allgemeine Einstellungen + +* Typ + +### Verbindungseinstellungen + +- specifies the URL and port of the broker (if you want to encrypt traffic, indicated SSL) - settings to connect to the broker,   + +### **Authentication settings** + +* - user name and password, if the broker requires authentication (it is appropriate to use SSL to avoid transmitting the password in clear text), + +### MQTT Einstellungen + +* **Patterns** - a mask for variables for which the customer subscribes (variables broker), the values are listed separated by commas, the # (pound) is used to indicate the set, +* **Mask private values** - filter variables that should be published (client variables) whose values are listed separated by commas, for indicating a set use the symbol * (asterisk), +* **To send only changes** - the client will publish only the variables that changed value (according to mask), +* **To give private values at startup** - if this option is checked, the will be published all the States (according to mask) every time a connection is established, to declare the available variables and their values, +* **The prefix for all values** - if the specified value, it will be added as a prefix to each published topic, for example, if you specify client1 /, then all topics will be published the following lines: `client1/javascript/0/cubietruck`, +* **Output log for each change** - in the log file will display debugging information for each change, +* **To send not only the team, but also the state (ack = true)** - if this option is not checked, the broker only sent variables / commands with ack = false, if the option to note that will be sent to all data, regardless of ack = true or ack = false, +* **The maximum length of a topic** - the maximum number of characters for the description of the topic, including service. + +Examples for setting the subscription mask variables (patterns). Consider topics: + +* "Sport" +* "Sport/Tennis" +* "Sport/Basketball" +* "Sport/Swimming" +* "Sport/Tennis/Finals" +* "Sport/Basketball/Finals" +* "Sport/Swimming/Finals" + +If you want to subscribe to a certain set of topics, you can use the characters # (pound sign) or + (plus sign). + +* "Sport/Tennis/#" (subscription only "Sport/Tennis" and "Sport/Tennis/Finals") +* "Sport/Tennis/+" (subscription only "Sport/Tennis/Finals", but not "Sport/Tennis") + +For JMS topics, if you want to subscribe to all topics "Finals", you can use the characters # (pound sign) or + (plus sign) + +* "Sport/#/Finals" +* "Sport/+/Finals" + +For MQTT topics if you want to subscribe to all topics "Finals", you can use the + (plus sign) + +* "Sport/+/Finals" + +As an example, consider the exchange of data between the two systems yunkong2. There is a working system yunkong2 for BananaPi-Board (IP address 192.168.69.51), it launched MQTT- driver in the server/broker mode from the example above. To the server connects a client that publishes data from the sensor DHT22 – temperature and humidity, as well as signed variables of interval measurement transmission and the status led (enable/disable) – in the example above. The second operating system yunkong2 on the Board Cubietruck, it will run the MQTT driver in a client/subscriber mode. He signs up for the variables temperature and humidity of the broker (which, in turn, receives from another client) and will publish all the script variables - [the state of the battery](http://www.yunkong2.net/?page_id=4268&lang=ru#_Li-polLi-ion) board (only the changes). Client configuration will be similar to the following: [![](http://www.yunkong2.net/wp-content/uploads//7-284x300.png)](http://www.yunkong2.net/wp-content/uploads//7.png) Connection type – the customer/subscriber indicates the IP address of the broker and the port (default 1883). Traffic encryption and authentication is not needed. Mask for the subscriptions (Patterns) - `mqtt/0/example1/hum,mqtt/0/example1/temp` - client is subscribed only on temperature and humidity (values separated by comma without spaces). Mask the data for publication - `javascript.0.cubietruck.battery.*` - publish all the script variables `cubietruck` in the group `battery` driver `javascript.0`. To send only the changes - send state variables batteries (makes no sense to send if the value has not changed). To give private values at startup – when starting the driver, the client immediately will release all variables according to the mask – even if they are null or empty to create variables in the broker. To send data with ack=false, variables work battery updated driver javascript, so they are always ack=false. The result of the work on the client side (temperature and humidity data of another customer - see the example above): [![](http://www.yunkong2.net/wp-content/uploads//9-1024x267.png)](http://www.yunkong2.net/wp-content/uploads//9.png) The result of the broker (status data of the battery client): [![](http://www.yunkong2.net/wp-content/uploads//11-1024x297.png)](http://www.yunkong2.net/wp-content/uploads//11.png) + +## Application - MQTT gateway protocols - ModBus RTU + +Driver MQTT can be used as a gateway for various protocols to connect new devices to the system yunkong2 or any other. A universal basis for the development of such solutions are arduino boards. In a network many examples, libraries and best practices. A huge community is working with these controllers, and the system integrated a variety of devices/equipment/devices. For example, consider the common industrial protocol ModBus. In yunkong2 system has a driver to work with it - version ModBus TCP (over ethernet). A set of sensors, controllers and actuators work physically on the RS-485 Network / 232 and ModBus RTU protocol. In order to integrate them can be applied MQTT Gateway - ModBus RTU based on arduino platform. Consider an example. **There is a temperature and humidity sensor** (for the test on the basis of arduino pro mini board DHT22 Sensor), that outputs data via ModBUS RTU: + +* Port UART (you can use MAX485 chip to convert RS-485 interface) running at 9600 with options 8E1 (1 start bit, 8 data bits, 1 Even parity bit, 1 stop bit), +* the address of the ModBus – 10, +* temperature address 0 the value multiplied by 10 (the reader function 3), +* humidity – address 1 value multiplied by 10 (read function 3), +* PWM LED address 2 value 0...1023 to check the recording function (write function 6). + +Connection scheme: + +![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus1.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus1.jpg) +by Fritzing + +Code for arduino pro mini controller produces the following: + +
#include  //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+#include  //https://code.google.com/archive/p/simple-modbus/
+#include  //https://github.com/PaulStoffregen/MsTimer2
+// modbus registers
+enum {
+    TEMP,
+    HUM,
+    PWM,
+    TEST,
+    HOLDING_REGS_SIZE
+};
+#define ID_MODBUS 10 // modbus address of the slave device
+unsigned int holdingRegs[HOLDING_REGS_SIZE]; // modbus register array
+// temperature and humidity sensor DHT22
+dht DHT;
+#define DHT22_PIN 2
+#define LED 9 // LED is connected to the PWM pin-9
+void setup()
+{
+    // configure the modbus
+    modbus_configure(& Serial, 9600, SERIAL_8E1, ID_MODBUS, 0, HOLDING_REGS_SIZE, holdingRegs);
+    holdingRegs[TEST] = -157; // for the test of the negative values
+    // initialize a timer for 2 seconds update data in temperature and humidity registers
+    MsTimer2::set(2000, read_sensors);
+    MsTimer2::start(); // run timer
+    pinMode(LED, OUTPUT); // LED port initialization
+}
+// the function launched by timer each 2 seconds
+void read_sensors()
+{
+    if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+        if
+            data from the sensor DHT22 managed to be read
+                // we write integer value in the register of humidity
+                holdingRegs[HUM]
+                = 10 * DHT.humidity;
+        // we write integer value in the register of temperature
+        holdingRegs[TEMP] = 10 * DHT.temperature;
+    }
+    else {
+        // if it wasn't succeeded to read data from the sensor DHT22, we write zero in registers
+        holdingRegs[HUM] = 0;
+        holdingRegs[TEMP] = 0;
+    }
+}
+void loop()
+{
+    modbus_update(); // modbus data update
+    // data from the LED control register transmit to the PWM (bit shift by 2 bits)
+    analogWrite(LED, holdingRegs[PWM] & gt; > 2);
+}
+ +To test the operation code and schema, you can connect to port serial board (for example, using a USB-UART Converter) and a special program to interview just made the temperature sensor and humidity with ModBus RTU interface. For the survey can be used, for example, [qmodbus](http://qmodbus.sourceforge.net/) or any other program. Settings: + +* port (choose from the list which port is connected to the serial Arduino boards); +* speed and other parameters – 9600 8E1; +* slave id: 10, read: function No. 3 read holding registers, starting address: 0, number of registers: 3, +* slave id: 10, record: function No. 6 write single register start address: 2, + +The answer in the program when reading should be approximately the following: + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus2.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus2.jpg) + +The answer in the program when recording: + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus3.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus3.jpg) + +**Now configure the gateway itself and connect it to the yunkong2** The gateway will be based on the platform arduino MEGA 2560 with ethernet shield - client MQTT, broker - an instance mqtt.0 yunkong2 system driver. Choosing the MEGA 2560 due to the fact that on this Board more than one UART port, respectively, is zero Serial0 (pin_0 (RX) and pin_1 (TX)) or simply Serial – use to output debug messages, and Serial1 (pin_19 (RX) and pin_18 (TX)) – for slave via ModBus. + +* the client – the fee for developing arduino MEGA 2560 + ethernet shield based on W5100 chip; +* to work with the ethernet board uses the [standard library](https://www.arduino.cc/en/Reference/Ethernet) for working with MQTT library [Pubsubclient](https://github.com/knolleary/pubsubclient); +* for the survey on the modbus use library [SimpleModbus](https://code.google.com/archive/p/simple-modbus/) version master; +* survey on UART port (just connect the RX port master, TX port slave and respectively TX port master, RX port slave), transmission control port is not used (it is for RS-485); +* port settings: speed 9600, 8Е1; +* - the address of the slave device 10, a function of reading number 3 (read holding registers), recording function no. 6 (write single register); +* - broker – yunkong2 system driver mqtt. + +Format topics of data exchange: + +* `modbusgateway/send_interval` - client signed to change the transmission interval of the temperature readings and humidity (int value in seconds), +* `modbusgateway/temp` - client publishes with a a given interval the value of the temperature sensor DHT22 (type float), +* `modbusgateway/hum` - the client publishes with a given interval the value of the humidity sensor DHT22 (type float), +* `modbusgateway/led` - the client is subscribed to the state change of the led (PWM control value 0...1024). + +СThe connection diagram will look something like this: + +[caption id="" align="alignnone" width="699"][![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus6.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus6.jpg) +By Fritzing + +For the test slave device energized from the master device. The Master in turn will work from the USB port, which is being debug (Serial0). Driver settings will be as follows: + +[![](http://www.yunkong2.net/wp-content/uploads//14-283x300.png)](http://www.yunkong2.net/wp-content/uploads//14.png) + +Connecting via TCP (WebSocket is not necessary), default port 1883\. The client within the local network, so to encrypt traffic and authenticate the user is not necessary. We will send only the changes since the client signed on the send interval indications and led state to obtain information about the update (without changing the value) to a variable makes no sense. To publish the subscription - note this option, as when you first connect (or connected after disconnection) of the client, he must know the state of the variables on which it is signed (a current interval of sending and whether the LED is to be turned on). Setting to send variables ack = true or false is also worth noting, as a variable (which signed the client) can change any driver / script / VIS and any changes should be sent to the client. The complete code for the arduino board will look like this: + +
// Connecting libraries
+#include 
+#include 
+#include  //https://github.com/knolleary/pubsubclient
+#include  //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+// Settings of a network
+byte mac[] = { 0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x31 };
+byte ip[] = { 192, 168, 69, 31 }; // arduino board IP address
+byte mqttserver[] = { 192, 168, 69, 51 }; // yunkong2 server IP address
+EthernetClient ethClient;
+void callback(char* topic, byte* payload, unsigned int length);
+PubSubClient client(mqttserver, 1884, callback, ethClient);
+// Global variables
+unsigned int send_interval = 10; // the sending interval of indications to the server, by default 10 seconds
+unsigned long last_time = 0; // the current time for the timer
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+//The processing function for incoming connections - reception of data on a subscription
+void callback(char* topic, byte* payload, unsigned int length)
+{
+    Serial.println("");
+    Serial.println("-------");
+    Serial.println("New callback of MQTT-broker");
+    // let's transform a subject (topic) and value (payload) to a line
+    payload[length] = '\0';
+    String strTopic = String(topic);
+    String strPayload = String((char*)payload);
+    // Research that "arrived" from the server on a subscription:
+    // Change of an interval of inquiry
+    if (strTopic == "example2/send_interval") {
+        int tmp = strPayload.toInt();
+        if (tmp == 0) {
+            send_interval = 10;
+        }
+        else {
+            send_interval = strPayload.toInt();
+        }
+    }
+    Serial.print(strTopic);
+    Serial.print(" ");
+    Serial.println(strPayload);
+    Serial.println("-------");
+    Serial.println("");
+}
+void setup()
+{
+    Serial.begin(9600);
+    Serial.println("Start...");
+    // start network connection
+    Ethernet.begin(mac, ip);
+    Serial.print("IP: ");
+    Serial.println(Ethernet.localIP());
+    // initialize input/output ports, register starting values
+}
+void loop()
+{
+    // If the MQTT connection inactively, then we try to set it and to publish/subscribe
+    if (!client.connected()) {
+        Serial.print("Connect to MQTT-boker... ");
+        // Connect and publish / subscribe
+        if (client.connect("example2")) {
+            Serial.println("success");
+            // Value from sensors
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+            // Subscribe for an inquiry interval
+            client.subscribe("example2/send_interval");
+        }
+        else {
+            // If weren't connected, we wait for 10 seconds and try again
+            Serial.print("Failed, rc=");
+            Serial.print(client.state());
+            Serial.println(" try again in 10 seconds");
+            delay(10000);
+        }
+        // If connection is active, then sends the data to the server with the specified time interval
+    }
+    else {
+        if (millis() & gt; (last_time + send_interval * 1000)) {
+            last_time = millis();
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+        }
+    }
+    // Check of incoming connections on a subscription
+    client.loop();
+}
+
+ +This solution can be used as a prototype (example) ModBus network in your automation system. The data from the slave is transmitted with the desired spacing in the yunkong2. + +[![](http://www.yunkong2.net/wp-content/uploads//10-1024x202.png)](http://www.yunkong2.net/wp-content/uploads//10.png) + +MQTT client signed variables and redirects needed in slave-device on the ModBus network. + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus5.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-modbus5.jpg) + +## Application - connecting mobile clients + +Recently MQTT protocol became very common due to the simplicity, economy of the traffic and the elaboration of good libraries for different platforms. There are many programs to work with MQTT on mobile devices, for example [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=en). With this program you can connect to the MQTT broker in a local network or the Internet. Consider an example, in the role of the broker will be the yunkong2 system, to which using MQTT to connect the client – application IoT MQTT Dashboard. In this example, we control the light controller [MegaD-328](http://www.ab-log.ru/smart-house/ethernet/megad-328), which is connected to the yunkong2 with the driver [MegaD](http://www.yunkong2.net/?page_id=4052&lang=en). Controls relay (MegaD port **P7**) light in the lobby, a special script, which is signed by the state of the port - button **P0** and MQTT-variable state **mqtt.0.remotectrl.light.hall**, which will publish the mobile client. This script toggles the state of the port that is bound to the switch (port P7), ie inverts it. It turns out that each time you press the button, electrically connected to port **P0** (caught the **true** state) and every time you publish variable **mqtt.0.remotectrl.light.hall** value as **true**, the port **P7** to turn on or off the light. The text of the script will be like this: + +
// Control of lighting in the hall by means of the button p0 port of the MegaD controller the driver instance megad.0
+on({ id : 'megad.0.p0_P0', change : 'any' }, function(obj) {
+    if (obj.newState.val != = '' || typeof obj.newState.val != = "undefined") {
+        if (obj.newState.val == = true) {
+            if (getState('megad.0.p7_P7').val == = true) {
+                setState('megad.0.p7_P7', false);
+            }
+            else {
+                setState('megad.0.p7_P7', true);
+            }
+        }
+    }
+});
+// Control of lighting in the hall is remote on MQTT a topic "mqtt.0.remotectrl.light.hall"
+on({ id : 'mqtt.0.remotectrl.light.hall', change : 'any' }, function(obj) {
+    if (obj.newState.val != = '' || typeof obj.newState.val != = "undefined") {
+        if (obj.newState.val == = true) {
+            if (getState('megad.0.p7_P7').val == = true) {
+                setState('megad.0.p7_P7', false);
+            }
+            else {
+                setState('megad.0.p7_P7', true);
+            }
+        }
+    }
+});
+
+ +Connect button and light bulbs to MegaD controller: + +[![](http://www.yunkong2.net/wp-content/uploads//mqtt-mobile1.jpg)](http://www.yunkong2.net/wp-content/uploads//mqtt-mobile1.jpg) + +MQTT driver settings: [![](http://www.yunkong2.net/wp-content/uploads//14-283x300.png)](http://www.yunkong2.net/wp-content/uploads//14.png) + +The mobile client can publish data to variable mqtt.0.remotectrl.light.hall and signs up for a real port status MegaD – megad.0.p7_P7\. The configure publishing and subscriptions: + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-mobile3.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-mobile3.png) + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-mobile4.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-mobile4.png) + +In total for one channel light control turn the control window (publish) and subscription window is a real condition light relay (for feedback): + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-mobile5.png) ![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-mobile6.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-mobile6.png) + +## Application - working with cloud servers + +The example described above has several disadvantages. First, it is not always the mobile client may be on the same local network as the server yunkong2, and secondly, even if you implement port forwarding in the Internet and to protect the connection, not always the server itself yunkong2 can accept incoming connection (located behind a NAT which has no access to settings). In the global network many different services that support MQTT - paid and free, for example sending weather data, geolocation, etc. Some services may act as MQTT protocol broker and can be used as a gateway (bridge) to output data from yunkong2 the global network, or to obtain data in yunkong2. As an example, consider the work of the bundles: + +* server / broker - service [cloudmqtt.com](https://www.cloudmqtt.com/) (there is a free tariff), +* customer/subscriber – the yunkong2 system with access to the Internet, publishes data of temperature and humidity (see [example above](http://www.yunkong2.net/?page_id=6435&lang=en#yunkong2_working_as_MQTT-broker)), publishes the real status of ports **P7-P13** (relay driver MegaD **megad.0** – light control), subscribing to properties of the remote light control (an instance of the driver mqtt **mqtt.0**), +* килент/подписчик - приложение [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=en) для удаленной работы - подписка на данные сенсора температуры и влажности, подписка на реальное состояние портов **P7-P13** (реле MegaD драйвера **megad.0**), публикация переменных удаленного управления светом (экземпляр драйвера **mqtt.0**). - customer/subscriber – the application of [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=en) to work remotely – subscribe to sensor data of temperature and humidity, subscription to the real status of ports **P7-P13** (relay driver MegaD **megad.0**), publication of variables of a remote control light (driver instance **mqtt.0**). + +he result is the following structure: + +[![](http://www.yunkong2.net/wp-content/uploads//mqtt-cloud1-1024x511.jpg)](http://www.yunkong2.net/wp-content/uploads//mqtt-cloud1.jpg) + +Bundle driver **mqtt.1** (broker) – Arduino UNO + Ethernet + DHT22 (client) as in [the example above](http://www.yunkong2.net/?page_id=6435&lang=en#yunkong2_working_as_MQTT-broker) with a few modifications. Configuring an instance of the mqtt **driver.1**: + +[![](http://www.yunkong2.net/wp-content/uploads//14-283x300.png)](http://www.yunkong2.net/wp-content/uploads//14.png) + +Code for the arduino platform: + +
// Connecting libraries
+#include 
+#include 
+#include  //https://github.com/knolleary/pubsubclient
+#include  //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+// Settings of a network
+byte mac[] = { 0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x31 };
+byte ip[] = { 192, 168, 69, 31 }; // arduino board IP address
+byte mqttserver[] = { 192, 168, 69, 51 }; // yunkong2 server IP address
+EthernetClient ethClient;
+void callback(char* topic, byte* payload, unsigned int length);
+PubSubClient client(mqttserver, 1884, callback, ethClient);
+// Global variables
+unsigned int send_interval = 10; // the sending interval of indications to the server, by default 10 seconds
+unsigned long last_time = 0; // the current time for the timer
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+//The processing function for incoming connections - reception of data on a subscription
+void callback(char* topic, byte* payload, unsigned int length)
+{
+    Serial.println("");
+    Serial.println("-------");
+    Serial.println("New callback of MQTT-broker");
+    // let's transform a subject (topic) and value (payload) to a line
+    payload[length] = '\0';
+    String strTopic = String(topic);
+    String strPayload = String((char*)payload);
+    // Research that "arrived" from the server on a subscription:
+    // Change of an interval of inquiry
+    if (strTopic == "example2/send_interval") {
+        int tmp = strPayload.toInt();
+        if (tmp == 0) {
+            send_interval = 10;
+        }
+        else {
+            send_interval = strPayload.toInt();
+        }
+    }
+    Serial.print(strTopic);
+    Serial.print(" ");
+    Serial.println(strPayload);
+    Serial.println("-------");
+    Serial.println("");
+}
+void setup()
+{
+    Serial.begin(9600);
+    Serial.println("Start...");
+    // start network connection
+    Ethernet.begin(mac, ip);
+    Serial.print("IP: ");
+    Serial.println(Ethernet.localIP());
+    // initialize input/output ports, register starting values
+}
+void loop()
+{
+    // If the MQTT connection inactively, then we try to set it and to publish/subscribe
+    if (!client.connected()) {
+        Serial.print("Connect to MQTT-boker... ");
+        // Connect and publish / subscribe
+        if (client.connect("example2")) {
+            Serial.println("success");
+            // Value from sensors
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+            // Subscribe for an inquiry interval
+            client.subscribe("example2/send_interval");
+        }
+        else {
+            // If weren't connected, we wait for 10 seconds and try again
+            Serial.print("Failed, rc=");
+            Serial.print(client.state());
+            Serial.println(" try again in 10 seconds");
+            delay(10000);
+        }
+        // If connection is active, then sends the data to the server with the specified time interval
+    }
+    else {
+        if (millis() & gt; (last_time + send_interval * 1000)) {
+            last_time = millis();
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+        }
+    }
+    // Check of incoming connections on a subscription
+    client.loop();
+}
+ +The result of the work - **mqtt.1** driver objects: + +[![](http://www.yunkong2.net/wp-content/uploads//12-1024x187.png)](http://www.yunkong2.net/wp-content/uploads//12.png) + +Now let's set up publish/subscribe data to the cloud. For a start, register on the site [cloudmqtt.com](https://www.cloudmqtt.com/), select the desired rate, create instance, get settings: + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud4.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud4.jpg) + +For greater security it is better to create a separate user, assume that it will be user **yunkong2**with the password **1234**. Give user permission to read and write in any topic: + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud5.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud5.jpg) + +Next set the instance of the mqtt **driver.0** to connect as a client/subscriber cloud broker and a list of publications/subscriptions: + +[![](http://www.yunkong2.net/wp-content/uploads//8-283x300.png)](http://www.yunkong2.net/wp-content/uploads//8.png) + +* connection type – the customer/subscriber, +* connection settings – specify the URL issued in the control panel [cloudmqtt.com](https://www.cloudmqtt.com/) the port will use **22809**that works with **SSL**, +* in the authentication options specify the user name and password, +* patterns – our client yunkong2 will be signed on all the topics that are in the cloud, so you specify here the number sign (**#**), you can use a mask to selectively subscribe, +* mask of the eigenvalues client will publish to the server **temperature/humidity** and the status of all ports megaD (ports with relay P7-P13),this field separated by a comma specify the required variables: **mqtt.1.example2.hum,mqtt.1.example2.temp,megad.0.p7_P7,megad.0.p8_P8,megad.0.p9_P9,megad.0.p10_P10,megad.0.p11_P11,megad.0.p12_P12,megad.0.p13_P13**, +* to send only changes – put a tick, will publish only the changes, +* to give your own values at the start – just specify to create topics, +* to send not only commands, but also the state (ack=true) – it should be noted that setting both the temperature/humidity updated driver mqtt (ack=true). + +Settings saved, make sure that the connection is established (on the control panel [cloudmqtt.com](https://www.cloudmqtt.com/) watch the log server). After some time, data will appear (in the panel link **WebsocketUI**): + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud7.jpg)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud7.jpg) + +In the end, it remains only to configure a mobile client, for example [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=en). Create a new connection: + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud8.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud8.png) + +Create topics for publication (for example, lighting of the hall - port **P7** MegaD): + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud9.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud9.png) + +Создаем топики подписок (температура, влажность, освещение зала порт **P7** MegaD): + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud10.png) ](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud10.png)[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud11.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud11.png) + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud12.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud12.png) + +In the end, your dashboard might look something like this: + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud13.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud13.png) + +[![](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud14.png)](http://www.yunkong2.net/wp-content/uploads//MQTT-example-cloud14.png) + +After the creation of the publications on a mobile device, in the driver instance **mqtt.0** system yunkong2 should appear variable light control that should be used in the script for lighting control (see [example above](http://www.yunkong2.net/?page_id=6435&lang=en#Application_8211_connecting_mobile_clients)): + +[![](http://www.yunkong2.net/wp-content/uploads//mqtt_13-1024x230.png)](http://www.yunkong2.net/wp-content/uploads//mqtt_13.png) + +Congratulations! Now you can control the system yunkong2 and receive data via a cloud service! diff --git a/docs/en/img/mqtt_1.png b/docs/en/img/mqtt_1.png new file mode 100644 index 0000000..8e39981 Binary files /dev/null and b/docs/en/img/mqtt_1.png differ diff --git a/docs/en/img/mqtt_10.png b/docs/en/img/mqtt_10.png new file mode 100644 index 0000000..c1fb16f Binary files /dev/null and b/docs/en/img/mqtt_10.png differ diff --git a/docs/en/img/mqtt_11.png b/docs/en/img/mqtt_11.png new file mode 100644 index 0000000..87c823d Binary files /dev/null and b/docs/en/img/mqtt_11.png differ diff --git a/docs/en/img/mqtt_12.png b/docs/en/img/mqtt_12.png new file mode 100644 index 0000000..3238f16 Binary files /dev/null and b/docs/en/img/mqtt_12.png differ diff --git a/docs/en/img/mqtt_13.png b/docs/en/img/mqtt_13.png new file mode 100644 index 0000000..63a877b Binary files /dev/null and b/docs/en/img/mqtt_13.png differ diff --git a/docs/en/img/mqtt_14.png b/docs/en/img/mqtt_14.png new file mode 100644 index 0000000..2410439 Binary files /dev/null and b/docs/en/img/mqtt_14.png differ diff --git a/docs/en/img/mqtt_2.png b/docs/en/img/mqtt_2.png new file mode 100644 index 0000000..40db590 Binary files /dev/null and b/docs/en/img/mqtt_2.png differ diff --git a/docs/en/img/mqtt_3.png b/docs/en/img/mqtt_3.png new file mode 100644 index 0000000..4473540 Binary files /dev/null and b/docs/en/img/mqtt_3.png differ diff --git a/docs/en/img/mqtt_4.png b/docs/en/img/mqtt_4.png new file mode 100644 index 0000000..f99f2fc Binary files /dev/null and b/docs/en/img/mqtt_4.png differ diff --git a/docs/en/img/mqtt_5.png b/docs/en/img/mqtt_5.png new file mode 100644 index 0000000..9e3eed3 Binary files /dev/null and b/docs/en/img/mqtt_5.png differ diff --git a/docs/en/img/mqtt_6.png b/docs/en/img/mqtt_6.png new file mode 100644 index 0000000..b926308 Binary files /dev/null and b/docs/en/img/mqtt_6.png differ diff --git a/docs/en/img/mqtt_7.png b/docs/en/img/mqtt_7.png new file mode 100644 index 0000000..45a6a55 Binary files /dev/null and b/docs/en/img/mqtt_7.png differ diff --git a/docs/en/img/mqtt_8.png b/docs/en/img/mqtt_8.png new file mode 100644 index 0000000..c5c2e17 Binary files /dev/null and b/docs/en/img/mqtt_8.png differ diff --git a/docs/en/img/mqtt_9.png b/docs/en/img/mqtt_9.png new file mode 100644 index 0000000..10717d5 Binary files /dev/null and b/docs/en/img/mqtt_9.png differ diff --git a/docs/en/img/mqtt_cloud1.jpg b/docs/en/img/mqtt_cloud1.jpg new file mode 100644 index 0000000..c65bf58 Binary files /dev/null and b/docs/en/img/mqtt_cloud1.jpg differ diff --git a/docs/en/img/mqtt_example-cloud10.png b/docs/en/img/mqtt_example-cloud10.png new file mode 100644 index 0000000..e7a9b1d Binary files /dev/null and b/docs/en/img/mqtt_example-cloud10.png differ diff --git a/docs/en/img/mqtt_example-cloud11.png b/docs/en/img/mqtt_example-cloud11.png new file mode 100644 index 0000000..866e817 Binary files /dev/null and b/docs/en/img/mqtt_example-cloud11.png differ diff --git a/docs/en/img/mqtt_example-cloud12.png b/docs/en/img/mqtt_example-cloud12.png new file mode 100644 index 0000000..5849999 Binary files /dev/null and b/docs/en/img/mqtt_example-cloud12.png differ diff --git a/docs/en/img/mqtt_example-cloud13.png b/docs/en/img/mqtt_example-cloud13.png new file mode 100644 index 0000000..c8d2aed Binary files /dev/null and b/docs/en/img/mqtt_example-cloud13.png differ diff --git a/docs/en/img/mqtt_example-cloud14.png b/docs/en/img/mqtt_example-cloud14.png new file mode 100644 index 0000000..b2610b1 Binary files /dev/null and b/docs/en/img/mqtt_example-cloud14.png differ diff --git a/docs/en/img/mqtt_example-cloud4.jpg b/docs/en/img/mqtt_example-cloud4.jpg new file mode 100644 index 0000000..8f87e1f Binary files /dev/null and b/docs/en/img/mqtt_example-cloud4.jpg differ diff --git a/docs/en/img/mqtt_example-cloud5.jpg b/docs/en/img/mqtt_example-cloud5.jpg new file mode 100644 index 0000000..e368d59 Binary files /dev/null and b/docs/en/img/mqtt_example-cloud5.jpg differ diff --git a/docs/en/img/mqtt_example-cloud7.jpg b/docs/en/img/mqtt_example-cloud7.jpg new file mode 100644 index 0000000..f612082 Binary files /dev/null and b/docs/en/img/mqtt_example-cloud7.jpg differ diff --git a/docs/en/img/mqtt_example-cloud8.png b/docs/en/img/mqtt_example-cloud8.png new file mode 100644 index 0000000..2cd9cf0 Binary files /dev/null and b/docs/en/img/mqtt_example-cloud8.png differ diff --git a/docs/en/img/mqtt_example-cloud9.png b/docs/en/img/mqtt_example-cloud9.png new file mode 100644 index 0000000..888fec0 Binary files /dev/null and b/docs/en/img/mqtt_example-cloud9.png differ diff --git a/docs/en/img/mqtt_example-mobile3.png b/docs/en/img/mqtt_example-mobile3.png new file mode 100644 index 0000000..24cabbe Binary files /dev/null and b/docs/en/img/mqtt_example-mobile3.png differ diff --git a/docs/en/img/mqtt_example-mobile4.png b/docs/en/img/mqtt_example-mobile4.png new file mode 100644 index 0000000..2d39c13 Binary files /dev/null and b/docs/en/img/mqtt_example-mobile4.png differ diff --git a/docs/en/img/mqtt_example-mobile5.png b/docs/en/img/mqtt_example-mobile5.png new file mode 100644 index 0000000..60fbf19 Binary files /dev/null and b/docs/en/img/mqtt_example-mobile5.png differ diff --git a/docs/en/img/mqtt_example-mobile6.png b/docs/en/img/mqtt_example-mobile6.png new file mode 100644 index 0000000..72fa127 Binary files /dev/null and b/docs/en/img/mqtt_example-mobile6.png differ diff --git a/docs/en/img/mqtt_example-modbus1.jpg b/docs/en/img/mqtt_example-modbus1.jpg new file mode 100644 index 0000000..6221949 Binary files /dev/null and b/docs/en/img/mqtt_example-modbus1.jpg differ diff --git a/docs/en/img/mqtt_example-modbus2.jpg b/docs/en/img/mqtt_example-modbus2.jpg new file mode 100644 index 0000000..b810f1d Binary files /dev/null and b/docs/en/img/mqtt_example-modbus2.jpg differ diff --git a/docs/en/img/mqtt_example-modbus3.jpg b/docs/en/img/mqtt_example-modbus3.jpg new file mode 100644 index 0000000..4474e99 Binary files /dev/null and b/docs/en/img/mqtt_example-modbus3.jpg differ diff --git a/docs/en/img/mqtt_example-modbus5.jpg b/docs/en/img/mqtt_example-modbus5.jpg new file mode 100644 index 0000000..a5a5225 Binary files /dev/null and b/docs/en/img/mqtt_example-modbus5.jpg differ diff --git a/docs/en/img/mqtt_example-modbus6.jpg b/docs/en/img/mqtt_example-modbus6.jpg new file mode 100644 index 0000000..47816ae Binary files /dev/null and b/docs/en/img/mqtt_example-modbus6.jpg differ diff --git a/docs/en/img/mqtt_mobile1.jpg b/docs/en/img/mqtt_mobile1.jpg new file mode 100644 index 0000000..3f339ee Binary files /dev/null and b/docs/en/img/mqtt_mobile1.jpg differ diff --git a/docs/en/img/mqtt_mqtt_13.png b/docs/en/img/mqtt_mqtt_13.png new file mode 100644 index 0000000..63a877b Binary files /dev/null and b/docs/en/img/mqtt_mqtt_13.png differ diff --git a/docs/en/img/mqtt_server4.jpg b/docs/en/img/mqtt_server4.jpg new file mode 100644 index 0000000..8588efa Binary files /dev/null and b/docs/en/img/mqtt_server4.jpg differ diff --git a/docs/en/mqtt.md b/docs/en/mqtt.md new file mode 100644 index 0000000..8846c21 --- /dev/null +++ b/docs/en/mqtt.md @@ -0,0 +1,799 @@ +![](MQTT) +# MQTT Server and Client + +## Description + +[MQTT](http://mqtt.org/) (Message Queue Telemetry Transport) is a lightweight protocol +used for communication between devices (M2M - machine-to-machine). +It uses a model publisher-subscriber to send messages over TCP / IP protocol. +The central part of the protocol is MQTT-server or broker who has access to the +publisher and the subscriber. This protocol is very primitive: a short title, without +integrity (that is why the transmission is implemented on top of TCP), does not impose any restrictions +on the structure, coding or database schema. The only requirement for the data in each packet - they +must be accompanied by the identifier information channel. This identifier specification called Topic Name. + +The MQTT Protocol requires a data broker. This is the Central idea of the technology. All devices send +data only to the broker and receive data also from him only. After receiving the packet, the broker +sends it to all devices on the network according to their subscription . For the device to get +something from the broker it must subscribe to a topic. Topics arise dynamically upon subscription +or upon arrival of the package with this topic. By subscribing to a topic, you can give up. Thus +topics are a convenient mechanism for organizing different kinds of relationships: one-to-many, +many-to-one and many-to-many. + +**Important points:** + +* the devices themselves establish communication with the broker, they may is behind a NAT and does not have static IP addresses, +* you can use SSL to encrypt the traffic, +* MQTT brokers allow you to connect to them through the WebSocket protocol on port 80, +* different brokers may be interconnected by subscribing to messages from each other. + +## Installation + +Installation is on the **Driver** tab page of the [administration system](http://www.yunkong2.net/?page_id=4179&lang=en). +In the driver group **Network** find a line called **MQTT Adapter**, and press the +button with the plus icon in the right side of the line.   + +![](img/mqtt_1.png) + +You will see a pop-up window driver installation, after installation, it will automatically close. + +![](img/mqtt_2.png) + +If all goes well, on the **Settings driver** tab appears **mqtt.0** installed instance of the driver. + +![](img/mqtt_3.png) + +## Setting + +As stated above, MQTT protocol implies a broker and clients. yunkong2 server can act as a broker and a client. +Setting to specify the operating mode - the type (server / broker or the customer / subscriber) +Consider every option. + +### yunkong2 working as MQTT-broker + +Basic settings if you intend to use the server/broker is shown in the picture: + +![](img/mqtt_4.png) + +* **Use WebSockets** - if there is a need to use WEB sockets for connection, you must install this option, with TCP-server will run in parallel with the WebSocket server, +* **Port** - the port to connect on TCP (default is 1883), a WebSocket server (see option above) runs on port +1 (default: 1884), +* **SSL** - this option is used if you want to encrypt all traffic (TCP or the WebSocket), thus it is necessary to specify certificates - simply select from a list of pre-set (specified in the system settings, see the [systems management driver description](http://www.yunkong2.net/?page_id=4179&lang=en)), +* **authentication settings** (username and password) - indicate, if necessary a specific user authentication, this setting is always used in conjunction with SSL-encryption option (not to transmit passwords in clear text over an insecure connection), +* **Mask private values** - the template (or several comma-separated) to filter variables yunkong2, which will be sent to the client, you can use special characters to specify a group of messages (for example, `memRSS, mqtt.0` - can be transmitted all the variables memory status of all drivers and all **mqtt.0 driver** instance variables), +* **To send only changes** - sending data to the client will be made only in case of change of a variable (if the state simply update - the value is not changed, the customer message will not be sent) from the client will be accepted any message, even if the value has not changed, +* **To give private values at startup** - for each successful client connection will be transferred to all known states (defined by the mask state) – in order to tell the client about the current state of the yunkong2, +* **Post status subscribes** - immediately after the subscription will be sent to the customer value of the variable on which it is signed (at the first start or restart the client will receive the values of variables on which it is signed, can be used to initialize variables), +* **The prefix for all values** - if the specified value, it will be added as a prefix to every sent topic, for example, if you specify yunkong2/, then all topics sent along the following lines: `yunkong2/mqtt/0/connected`, +* **Output log for each change** - in the log file will display debugging information for each change, +* **To send not only commands, but also the state (ack=true)** - if this option is not active, the client will only send variables/commands with ack=false, if the flag is set, then variables will be transferred regardless of the state of ack (false / true), +* **The maximum length of the name of a topic** - the maximum number of characters for the description of the topic, including service. + +As an example, consider the exchange of data between the client based on the [arduino board](https://www.arduino.cc/) and the broker is an instance of mqtt.0 driver system yunkong2. + +* - the client – the fee for developing [arduino UNO](https://www.arduino.cc/en/Main/ArduinoBoardUno) + [ethernet shield](https://store.arduino.cc/product/A000072) based on W5100 chip, +* - to work with the ethernet board uses the standard [library](https://www.arduino.cc/en/Reference/Ethernet) for working with MQTT library [Pubsubclient](https://github.com/knolleary/pubsubclient), +* - the AM2302 sensor (temperature and humidity) connected to pin_8 for the survey used library with DHTlib with [DHTlib](https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib) resource github.com, +* - led **led_green** is connected to pin_9, control in discrete mode on/off +* - broker – yunkong2 system driver mqtt. + +Format topics of data exchange: + +* `example1/send_interval` - client signed to change the transmission interval of the temperature readings and humidity (int value in seconds), +* `example1/temp` - client publishes a specified temperature interval with DHT22 sensor (float type), +* `example1/hum` - client publishes a specified humidity value intervals with DHT22 sensor (float type), +* `example1/led` - the client is subscribed to the state change of the led (the text on/off or 0/1 or true/false). + +Driver settings will be as follows: + +![](img/mqtt_5.png) + +Connecting via TCP (WebSocket is not necessary), default port 1883\. The client within the local network, so to encrypt traffic and authenticate the user is not necessary. We will send only the changes since the client signed on the send interval indications and led state to obtain information about the update (without changing the value) to a variable makes no sense. To publish the subscription - note this option, as when you first connect (or connected after disconnection) of the client, he must know the state of the variables on which it is signed (a current interval of sending and whether the LED is to be turned on). Setting to send variables ack = true or false is also worth noting, as a variable (which signed the client) can change any driver / script / VIS and any changes should be sent to the client. The complete code for the arduino board will look like this: + +
// Connecting libraries
+#include
+#include
+#include //https://github.com/knolleary/pubsubclient
+#include //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+//Settings of a network
+byte mac[] = {
+  0xAB,
+  0xBC,
+  0xCD,
+  0xDE,
+  0xEF,
+  0x31
+};
+byte ip[] = {
+  192,
+  168,
+  69,
+  31
+}; //arduino board IP address
+byte mqttserver[] = {
+  192,
+  168,
+  69,
+  51
+}; // yunkong2 server IP address
+
+EthernetClient ethClient;
+void callback(char * topic, byte * payload, unsigned int length);
+PubSubClient client(mqttserver, 1883, callback, ethClient);
+
+//Global variables
+#define LED_pin 9
+unsigned int send_interval = 10; // the sending interval of indications to the server, by default 10 seconds
+unsigned long last_time = 0; // the current time for the timer
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+
+// The processing function for incoming connections - reception of data on a subscription
+void callback(char * topic, byte * payload, unsigned int length) {
+  Serial.println("");
+  Serial.println("-------");
+  Serial.println("New callback of MQTT-broker");
+  // let's transform a subject (topic) and value (payload) to a line
+  payload[length] = '\0';
+  String strTopic = String(topic);
+  String strPayload = String((char * ) payload);
+  // research that "arrived" from the server on a subscription::
+  // Change of an interval of inquiry
+  if (strTopic == "example1/send_interval") {
+    int tmp = strPayload.toInt();
+    if (tmp == 0) {
+      send_interval = 10;
+    } else {
+      send_interval = strPayload.toInt();
+    }
+  }
+  // Control of a LED
+  if (strTopic == "example1/led") {
+    if (strPayload == "off" || strPayload == "0" || strPayload == "false") digitalWrite(LED_pin, LOW);
+    if (strPayload == "on" || strPayload == "1" || strPayload == "true") digitalWrite(LED_pin, HIGH);
+  }
+  Serial.print(strTopic);
+  Serial.print(" ");
+  Serial.println(strPayload);
+  Serial.println("-------");
+  Serial.println("");
+}
+
+void setup() {
+  Serial.begin(9600);
+  Serial.println("Start...");
+  // start network connection
+  Ethernet.begin(mac, ip);
+  Serial.print("IP: ");
+  Serial.println(Ethernet.localIP());
+  // initialize input/output ports, register starting values
+  pinMode(LED_pin, OUTPUT);
+  digitalWrite(LED_pin, LOW); // when the LED is off
+}
+
+void loop() {
+  // If the MQTT connection inactively, then we try to set it and to publish/subscribe
+  if (!client.connected()) {
+    Serial.print("Connect to MQTT-boker... ");
+    // Connect and publish / subscribe
+    if (client.connect("example1")) {
+      Serial.println("success");
+      // Value from sensors
+      if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example1/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example1/temp", buff);
+      }
+      // subscribe for an inquiry interval
+      client.subscribe("example1/send_interval");
+      // subscribe to the LED control variable
+      client.subscribe("example1/led");
+    } else {
+      // If weren't connected, we wait for 10 seconds and try again
+      Serial.print("Failed, rc=");
+      Serial.print(client.state());
+      Serial.println(" try again in 10 seconds");
+      delay(10000);
+    }
+    // If connection is active, then sends the data to the server with the specified time interval
+  } else {
+    if (millis() & gt;
+      (last_time + send_interval * 1000)) {
+      last_time = millis();
+      if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example1/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example1/temp", buff);
+      }
+    }
+  }
+  // Check of incoming connections on a subscription
+  client.loop();
+}
+
+ +The result of the part of the broker (temperature and humidity data is updated with the preset time period): + +![](img/mqtt_6.png) + +The result of the client-side (incoming data subscription output to the console for debugging): + +![](img/mqtt_server4.jpg) + +### yunkong2 working as MQTT-client + +For an instance MQTT driver earned as a client / subscriber - you need to choose the appropriate type of configuration. +In this set of options will change slightly: + +![](img/mqtt_4.png) + +* **Connection settings** - specifies the URL and port of the broker (if you want to encrypt traffic, indicated SSL) - settings to connect to the broker, +* **Authentication settings** - user name and password, if the broker requires authentication (it is appropriate to use SSL to avoid transmitting the password in clear text), +* **Patterns** - a mask for variables for which the customer subscribes (variables broker), the values are listed separated by commas, the # (pound) is used to indicate the set, +* **Mask private values** - filter variables that should be published (client variables) whose values are listed separated by commas, for indicating a set use the symbol * (asterisk), +* **To send only changes** - the client will publish only the variables that changed value (according to mask), +* **To give private values at startup** - if this option is checked, the will be published all the States (according to mask) every time a connection is established, to declare the available variables and their values, +* **The prefix for all values** - if the specified value, it will be added as a prefix to each published topic, for example, if you specify client1 /, then all topics will be published the following lines: `client1/javascript/0/cubietruck`, +* **Output log for each change** - in the log file will display debugging information for each change, +* **To send not only the team, but also the state (ack = true)** - if this option is not checked, the broker only sent variables / commands with ack = false, if the option to note that will be sent to all data, regardless of ack = true or ack = false, +* **The maximum length of a topic** - the maximum number of characters for the description of the topic, including service. + +Examples for setting the subscription mask variables (patterns). Consider topics: + +* "Sport" +* "Sport/Tennis" +* "Sport/Basketball" +* "Sport/Swimming" +* "Sport/Tennis/Finals" +* "Sport/Basketball/Finals" +* "Sport/Swimming/Finals" + +If you want to subscribe to a certain set of topics, you can use the characters # (pound sign) or + (plus sign). + +* "Sport/Tennis/#" (subscription only "Sport/Tennis" and "Sport/Tennis/Finals") +* "Sport/Tennis/+" (subscription only "Sport/Tennis/Finals", but not "Sport/Tennis") + +For JMS topics, if you want to subscribe to all topics "Finals", you can use the characters # (pound sign) or + (plus sign) + +* "Sport/#/Finals" +* "Sport/+/Finals" + +For MQTT topics if you want to subscribe to all topics "Finals", you can use the + (plus sign) + +* "Sport/+/Finals" + +As an example, consider the exchange of data between the two systems yunkong2. There is a working system yunkong2 for +BananaPi-Board (IP address 192.168.69.51), it launched MQTT- driver in the server/broker mode from the example above. +To the server connects a client that publishes data from the sensor DHT22 – temperature and humidity, +as well as signed variables of interval measurement transmission and the status led (enable/disable) – in the example above. +The second operating system yunkong2 on the Board Cubietruck, it will run the MQTT driver in a client/subscriber mode. +He signs up for the variables temperature and humidity of the broker (which, in turn, receives from another client) +and will publish all the script variables - [the state of the battery](http://www.yunkong2.net/?page_id=4268&lang=ru#_Li-polLi-ion) +board (only the changes). Client configuration will be similar to the following: + +![](img/mqtt_7.png) + +Connection type – the customer/subscriber indicates the IP address of the broker and the port (default 1883). +Traffic encryption and authentication is not needed. + +Mask for the subscriptions (Patterns) - `mqtt/0/example1/hum,mqtt/0/example1/temp` - client is subscribed only +on temperature and humidity (values separated by comma without spaces). + +Mask the data for publication - `javascript.0.cubietruck.battery.*` - publish all the script +variables `cubietruck` in the group `battery` driver `javascript.0`. + +To send only the changes - send +state variables batteries (makes no sense to send if the value has not changed). To give private values +at startup – when starting the driver, the client immediately will release all variables according to the +mask – even if they are null or empty to create variables in the broker. + +To send data with ack=false, +variables work battery updated driver javascript, so they are always ack=false. The result of the work +on the client side (temperature and humidity data of another customer - see the example above): + +![](img/mqtt_9.png) + +The result of the broker (status data of the battery client): + +![](img/mqtt_11.png) + +## Application - MQTT gateway protocols - ModBus RTU + +Driver MQTT can be used as a gateway for various protocols to connect new devices to the +system yunkong2 or any other. A universal basis for the development of such solutions are +arduino boards. In a network many examples, libraries and best practices. A huge community +is working with these controllers, and the system integrated a variety of devices/equipment/devices. + +For example, consider the common industrial protocol ModBus. In yunkong2 system has a driver to work with it - version +ModBus TCP (over ethernet). A set of sensors, controllers and actuators work physically on the RS-485 Network / 232 and ModBus RTU protocol. +In order to integrate them can be applied MQTT Gateway - ModBus RTU based on arduino platform. Consider an example. + +**There is a temperature and humidity sensor** (for the test on the basis of arduino pro mini board DHT22 Sensor), +that outputs data via ModBUS RTU: + +* Port UART (you can use MAX485 chip to convert RS-485 interface) running at 9600 with options 8E1 (1 start bit, 8 data bits, 1 Even parity bit, 1 stop bit), +* the address of the ModBus – 10, +* temperature address 0 the value multiplied by 10 (the reader function 3), +* humidity – address 1 value multiplied by 10 (read function 3), +* PWM LED address 2 value 0...1023 to check the recording function (write function 6). + +Connection scheme: + +![](img/mqtt_example-modbus1.jpg) +by Fritzing + +Code for arduino pro mini controller produces the following: + +
+#include  //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+#include  //https://code.google.com/archive/p/simple-modbus/
+#include  //https://github.com/PaulStoffregen/MsTimer2
+// modbus registers
+enum {
+    TEMP,
+    HUM,
+    PWM,
+    TEST,
+    HOLDING_REGS_SIZE
+};
+#define ID_MODBUS 10 // modbus address of the slave device
+unsigned int holdingRegs[HOLDING_REGS_SIZE]; // modbus register array
+// temperature and humidity sensor DHT22
+dht DHT;
+#define DHT22_PIN 2
+#define LED 9 // LED is connected to the PWM pin-9
+void setup()
+{
+    // configure the modbus
+    modbus_configure(& Serial, 9600, SERIAL_8E1, ID_MODBUS, 0, HOLDING_REGS_SIZE, holdingRegs);
+    holdingRegs[TEST] = -157; // for the test of the negative values
+    // initialize a timer for 2 seconds update data in temperature and humidity registers
+    MsTimer2::set(2000, read_sensors);
+    MsTimer2::start(); // run timer
+    pinMode(LED, OUTPUT); // LED port initialization
+}
+// the function launched by timer each 2 seconds
+void read_sensors()
+{
+    if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+        if
+            data from the sensor DHT22 managed to be read
+                // we write integer value in the register of humidity
+                holdingRegs[HUM]
+                = 10 * DHT.humidity;
+        // we write integer value in the register of temperature
+        holdingRegs[TEMP] = 10 * DHT.temperature;
+    }
+    else {
+        // if it wasn't succeeded to read data from the sensor DHT22, we write zero in registers
+        holdingRegs[HUM] = 0;
+        holdingRegs[TEMP] = 0;
+    }
+}
+void loop()
+{
+    modbus_update(); // modbus data update
+    // data from the LED control register transmit to the PWM (bit shift by 2 bits)
+    analogWrite(LED, holdingRegs[PWM] >> 2);
+}
+
+ +To test the operation code and schema, you can connect to port serial board (for example, using a USB-UART Converter) +and a special program to interview just made the temperature sensor and humidity with ModBus RTU interface. +For the survey can be used, for example, [qmodbus](http://qmodbus.sourceforge.net/) or any other program. + +Settings: + +- port (choose from the list which port is connected to the serial Arduino boards); +- speed and other parameters – 9600 8E1; +- slave id: 10, read: function No. 3 read holding registers, starting address: 0, number of registers: 3, +- slave id: 10, record: function No. 6 write single register start address: 2, + +The answer in the program when reading should be approximately the following: + +![](img/mqtt_example-modbus2.jpg) + +The answer in the program when recording: + +![](img/mqtt_example-modbus3.jpg) + +**Now configure the gateway itself and connect it to the yunkong2** + +The gateway will be based on the +platform arduino MEGA 2560 with ethernet shield - client MQTT, broker - an instance mqtt.0 yunkong2 system driver. +Choosing the MEGA 2560 due to the fact that on this Board more than one UART port, respectively, is +zero Serial0 (pin_0 (RX) and pin_1 (TX)) or simply Serial – use to output debug messages, and Serial1 (pin_19 (RX) and pin_18 (TX)) – for slave via ModBus. + +* the client – the fee for developing arduino MEGA 2560 + ethernet shield based on W5100 chip; +* to work with the ethernet board uses the [standard library](https://www.arduino.cc/en/Reference/Ethernet) + for working with MQTT library [Pubsubclient](https://github.com/knolleary/pubsubclient); +* for the survey on the modbus use library [SimpleModbus](https://code.google.com/archive/p/simple-modbus/) version master; +* survey on UART port (just connect the RX port master, TX port slave and respectively TX port master, RX port slave), transmission control port is not used (it is for RS-485); +* port settings: speed 9600, 8Е1; +* the address of the slave device 10, a function of reading number 3 (read holding registers), recording function no. 6 (write single register); +* broker – yunkong2 system driver mqtt. + +Format topics of data exchange: + +* `modbusgateway/send_interval` - client signed to change the transmission interval of the temperature readings and humidity (int value in seconds), +* `modbusgateway/temp` - client publishes with a a given interval the value of the temperature sensor DHT22 (type float), +* `modbusgateway/hum` - the client publishes with a given interval the value of the humidity sensor DHT22 (type float), +* `modbusgateway/led` - the client is subscribed to the state change of the led (PWM control value 0...1024). + +СThe connection diagram will look something like this: + +![](img/mqtt_example-modbus6.jpg) + +For the test slave device energized from the master device. The Master in turn will work from the USB port, which is being debug (Serial0). +Driver settings will be as follows: + +![](img/mqtt_14.png) + +Connecting via TCP (WebSocket is not necessary), default port 1883\. The client within the local network, so to encrypt traffic and authenticate the user is not necessary. We will send only the changes since the client signed on the send interval indications and led state to obtain information about the update (without changing the value) to a variable makes no sense. To publish the subscription - note this option, as when you first connect (or connected after disconnection) of the client, he must know the state of the variables on which it is signed (a current interval of sending and whether the LED is to be turned on). Setting to send variables ack = true or false is also worth noting, as a variable (which signed the client) can change any driver / script / VIS and any changes should be sent to the client. The complete code for the arduino board will look like this: + +
// Connecting libraries
+#include 
+#include 
+#include  //https://github.com/knolleary/pubsubclient
+#include  //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+
+// Settings of a network
+byte mac[] = { 0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x31 };
+byte ip[] = { 192, 168, 69, 31 }; // arduino board IP address
+byte mqttserver[] = { 192, 168, 69, 51 }; // yunkong2 server IP address
+EthernetClient ethClient;
+void callback(char* topic, byte* payload, unsigned int length);
+PubSubClient client(mqttserver, 1884, callback, ethClient);
+// Global variables
+unsigned int send_interval = 10; // the sending interval of indications to the server, by default 10 seconds
+unsigned long last_time = 0; // the current time for the timer
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+
+//The processing function for incoming connections - reception of data on a subscription
+void callback(char* topic, byte* payload, unsigned int length)
+{
+    Serial.println("");
+    Serial.println("-------");
+    Serial.println("New callback of MQTT-broker");
+    // let's transform a subject (topic) and value (payload) to a line
+    payload[length] = '\0';
+    String strTopic = String(topic);
+    String strPayload = String((char*)payload);
+    // Research that "arrived" from the server on a subscription:
+    // Change of an interval of inquiry
+    if (strTopic == "example2/send_interval") {
+        int tmp = strPayload.toInt();
+        if (tmp == 0) {
+            send_interval = 10;
+        }
+        else {
+            send_interval = strPayload.toInt();
+        }
+    }
+    Serial.print(strTopic);
+    Serial.print(" ");
+    Serial.println(strPayload);
+    Serial.println("-------");
+    Serial.println("");
+}
+
+void setup()
+{
+    Serial.begin(9600);
+    Serial.println("Start...");
+    // start network connection
+    Ethernet.begin(mac, ip);
+    Serial.print("IP: ");
+    Serial.println(Ethernet.localIP());
+    // initialize input/output ports, register starting values
+}
+
+void loop()
+{
+    // If the MQTT connection inactively, then we try to set it and to publish/subscribe
+    if (!client.connected()) {
+        Serial.print("Connect to MQTT-boker... ");
+        // Connect and publish / subscribe
+        if (client.connect("example2")) {
+            Serial.println("success");
+            // Value from sensors
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+            // Subscribe for an inquiry interval
+            client.subscribe("example2/send_interval");
+        }
+        else {
+            // If weren't connected, we wait for 10 seconds and try again
+            Serial.print("Failed, rc=");
+            Serial.print(client.state());
+            Serial.println(" try again in 10 seconds");
+            delay(10000);
+        }
+        // If connection is active, then sends the data to the server with the specified time interval
+    }
+    else {
+        if (millis() & gt; (last_time + send_interval * 1000)) {
+            last_time = millis();
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+        }
+    }
+    // Check of incoming connections on a subscription
+    client.loop();
+}
+
+ +This solution can be used as a prototype (example) ModBus network in your automation system. The data from the slave is +transmitted with the desired spacing in the yunkong2. + +![](img/mqtt_10.png) + +MQTT client signed variables and redirects needed in slave-device on the ModBus network. + +![](img/mqtt_example-modbus5.jpg) + +## Application - connecting mobile clients + +Recently MQTT protocol became very common due to the simplicity, economy of the traffic and the elaboration of good libraries for different platforms. +There are many programs to work with MQTT on mobile devices, for example [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=en). +With this program you can connect to the MQTT broker in a local network or the Internet. + +Consider an example, in the role of the broker will be the yunkong2 system, to which using MQTT to connect the client – application +IoT MQTT Dashboard. + +In this example, we control the light controller [MegaD-328](http://www.ab-log.ru/smart-house/ethernet/megad-328), +which is connected to the yunkong2 with the driver [MegaD](http://www.yunkong2.net/?page_id=4052&lang=en). +Controls relay (MegaD port **P7**) light in the lobby, a special script, which is signed by the state +of the port - button **P0** and MQTT-variable state **mqtt.0.remotectrl.light.hall**, which will publish the mobile client. +This script toggles the state of the port that is bound to the switch (port P7), ie inverts it. + +It turns out that each time you press the button, electrically connected to port **P0** (caught the **true** state) and every +time you publish variable **mqtt.0.remotectrl.light.hall** value as **true**, the port **P7** to turn on or off the light. +The text of the script will be like this: + +
+// Control of lighting in the hall by means of the button p0 port of the MegaD controller the driver instance megad.0
+on({ id : 'megad.0.p0_P0', change : 'any' }, function(obj) {
+    if (obj.newState.val != = '' || typeof obj.newState.val != = "undefined") {
+        if (obj.newState.val == = true) {
+            if (getState('megad.0.p7_P7').val == = true) {
+                setState('megad.0.p7_P7', false);
+            }
+            else {
+                setState('megad.0.p7_P7', true);
+            }
+        }
+    }
+});
+// Control of lighting in the hall is remote on MQTT a topic "mqtt.0.remotectrl.light.hall"
+on({ id : 'mqtt.0.remotectrl.light.hall', change : 'any' }, function(obj) {
+    if (obj.newState.val != = '' || typeof obj.newState.val != = "undefined") {
+        if (obj.newState.val == = true) {
+            if (getState('megad.0.p7_P7').val == = true) {
+                setState('megad.0.p7_P7', false);
+            }
+            else {
+                setState('megad.0.p7_P7', true);
+            }
+        }
+    }
+});
+
+ +Connect button and light bulbs to MegaD controller: + +![](img/mqtt_mobile1.jpg) + +MQTT driver settings: + +![](img/mqtt_14.png) + +The mobile client can publish data to variable mqtt.0.remotectrl.light.hall and signs up for a real port status MegaD – megad.0.p7_P7. + +The configure publishing and subscriptions: + +![](img/mqtt_example-mobile3.png) + +![](img/mqtt_example-mobile4.png) + +In total for one channel light control turn the control window (publish) and subscription window is a real condition light +relay (for feedback): + +![](img/mqtt_example-mobile5.png) + +![](img/mqtt_example-mobile6.png) + +## Application - working with cloud servers + +The example described above has several disadvantages. First, it is not always the mobile client may be on the same local network as the server yunkong2, and secondly, even if you implement port forwarding in the Internet and to protect the connection, not always the server itself yunkong2 can accept incoming connection (located behind a NAT which has no access to settings). In the global network many different services that support MQTT - paid and free, for example sending weather data, geolocation, etc. Some services may act as MQTT protocol broker and can be used as a gateway (bridge) to output data from yunkong2 the global network, or to obtain data in yunkong2. As an example, consider the work of the bundles: + +* server / broker - service [cloudmqtt.com](https://www.cloudmqtt.com/) (there is a free tariff), +* customer/subscriber – the yunkong2 system with access to the Internet, publishes data of temperature and humidity (see [example above](http://www.yunkong2.net/?page_id=6435&lang=en#yunkong2_working_as_MQTT-broker)), publishes the real status of ports **P7-P13** (relay driver MegaD **megad.0** – light control), subscribing to properties of the remote light control (an instance of the driver mqtt **mqtt.0**), +* customer/subscriber – the application of [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=en) to work remotely – subscribe to sensor data of temperature and humidity, subscription to the real status of ports **P7-P13** (relay driver MegaD **megad.0**), publication of variables of a remote control light (driver instance **mqtt.0**). + +The result is the following structure: + +![](img/mqtt_cloud1.jpg) + +Bundle driver **mqtt.1** (broker) – Arduino UNO + Ethernet + DHT22 (client) +as in [the example above](http://www.yunkong2.net/?page_id=6435&lang=en#yunkong2_working_as_MQTT-broker) with a few modifications. +Configuring an instance of the mqtt **driver.1**: + +![](img/mqtt_14.png) + +Code for the arduino platform: + +
// Connecting libraries
+#include 
+#include 
+#include  //https://github.com/knolleary/pubsubclient
+#include  //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+
+// Settings of a network
+byte mac[] = { 0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x31 };
+byte ip[] = { 192, 168, 69, 31 }; // arduino board IP address
+byte mqttserver[] = { 192, 168, 69, 51 }; // yunkong2 server IP address
+EthernetClient ethClient;
+void callback(char* topic, byte* payload, unsigned int length);
+PubSubClient client(mqttserver, 1884, callback, ethClient);
+// Global variables
+unsigned int send_interval = 10; // the sending interval of indications to the server, by default 10 seconds
+unsigned long last_time = 0; // the current time for the timer
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+//The processing function for incoming connections - reception of data on a subscription
+void callback(char* topic, byte* payload, unsigned int length)
+{
+    Serial.println("");
+    Serial.println("-------");
+    Serial.println("New callback of MQTT-broker");
+    // let's transform a subject (topic) and value (payload) to a line
+    payload[length] = '\0';
+    String strTopic = String(topic);
+    String strPayload = String((char*)payload);
+    // Research that "arrived" from the server on a subscription:
+    // Change of an interval of inquiry
+    if (strTopic == "example2/send_interval") {
+        int tmp = strPayload.toInt();
+        if (tmp == 0) {
+            send_interval = 10;
+        }
+        else {
+            send_interval = strPayload.toInt();
+        }
+    }
+    Serial.print(strTopic);
+    Serial.print(" ");
+    Serial.println(strPayload);
+    Serial.println("-------");
+    Serial.println("");
+}
+
+void setup()
+{
+    Serial.begin(9600);
+    Serial.println("Start...");
+    // start network connection
+    Ethernet.begin(mac, ip);
+    Serial.print("IP: ");
+    Serial.println(Ethernet.localIP());
+    // initialize input/output ports, register starting values
+}
+
+void loop()
+{
+    // If the MQTT connection inactively, then we try to set it and to publish/subscribe
+    if (!client.connected()) {
+        Serial.print("Connect to MQTT-boker... ");
+        // Connect and publish / subscribe
+        if (client.connect("example2")) {
+            Serial.println("success");
+            // Value from sensors
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+            // Subscribe for an inquiry interval
+            client.subscribe("example2/send_interval");
+        }
+        else {
+            // If weren't connected, we wait for 10 seconds and try again
+            Serial.print("Failed, rc=");
+            Serial.print(client.state());
+            Serial.println(" try again in 10 seconds");
+            delay(10000);
+        }
+        // If connection is active, then sends the data to the server with the specified time interval
+    }
+    else {
+        if (millis() & gt; (last_time + send_interval * 1000)) {
+            last_time = millis();
+            if (DHT.read22(DHT22_PIN) == DHTLIB_OK) {
+                dtostrf(DHT.humidity, 5, 2, buff);
+                client.publish("example2/hum", buff);
+                dtostrf(DHT.temperature, 5, 2, buff);
+                client.publish("example2/temp", buff);
+            }
+        }
+    }
+    // Check of incoming connections on a subscription
+    client.loop();
+}
+ +The result of the work - **mqtt.1** driver objects: + +![](img/mqtt_12.png) + +Now let's set up publish/subscribe data to the cloud. For a start, register on the site [cloudmqtt.com](https://www.cloudmqtt.com/), select +the desired rate, create instance, get settings: + +![](img/mqtt_example-cloud4.jpg) + +For greater security it is better to create a separate user, assume that it will be user **yunkong2**with the password **1234**. +Give user permission to read and write in any topic: + +![](img/mqtt_example-cloud5.jpg) + +Next set the instance of the mqtt **driver.0** to connect as a client/subscriber cloud broker and a list of publications/subscriptions: + +![](img/mqtt_8.png) + +* connection type – the customer/subscriber, +* connection settings – specify the URL issued in the control panel [cloudmqtt.com](https://www.cloudmqtt.com/) the port will use **22809**that works with **SSL**, +* in the authentication options specify the user name and password, +* patterns – our client yunkong2 will be signed on all the topics that are in the cloud, so you specify here the number sign (**#**), you can use a mask to selectively subscribe, +* mask of the eigenvalues client will publish to the server **temperature/humidity** and the status of all ports megaD (ports with relay P7-P13),this field separated by a comma specify the required variables: **mqtt.1.example2.hum,mqtt.1.example2.temp,megad.0.p7_P7,megad.0.p8_P8,megad.0.p9_P9,megad.0.p10_P10,megad.0.p11_P11,megad.0.p12_P12,megad.0.p13_P13**, +* to send only changes – put a tick, will publish only the changes, +* to give your own values at the start – just specify to create topics, +* to send not only commands, but also the state (ack=true) – it should be noted that setting both the temperature/humidity updated driver mqtt (ack=true). + +Settings saved, make sure that the connection is established (on the control panel [cloudmqtt.com](https://www.cloudmqtt.com/) watch the log server). +After some time, data will appear (in the panel link **WebsocketUI**): + +![](img/mqtt_example-cloud7.jpg) + +In the end, it remains only to configure a mobile client, for example [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=en). +Create a new connection: + +![](img/mqtt_example-cloud8.png) + +Create topics for publication (for example, lighting of the hall - port **P7** MegaD): + +![](img/mqtt_example-cloud9.png) + +then create subscription for the topics (temperature, humidity, hall light on port **P7** MegaD): + +![](img/mqtt_example-cloud10.png) + +![](img/mqtt_example-cloud11.png) + +![](img/mqtt_example-cloud12.png) + +In the end, your dashboard might look something like this: + +![](img/mqtt_example-cloud13.png) + +![](img/mqtt_example-cloud14.png) + +After the creation of the publications on a mobile device, in the driver instance **mqtt.0** system yunkong2 should appear variable +light control that should be used in the script for lighting +control (see [example above](http://www.yunkong2.net/?page_id=6435&lang=en#Application_8211_connecting_mobile_clients)): + +![](img/mqtt_13.png) + +Congratulations! Now you can control the system yunkong2 and receive data via a cloud service! \ No newline at end of file diff --git a/docs/ru/img/driver-mqtt_MQTT-client1.jpg b/docs/ru/img/driver-mqtt_MQTT-client1.jpg new file mode 100644 index 0000000..65cb458 Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-client1.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-client4.jpg b/docs/ru/img/driver-mqtt_MQTT-client4.jpg new file mode 100644 index 0000000..4f1e7d9 Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-client4.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-example-cloud15.jpg b/docs/ru/img/driver-mqtt_MQTT-example-cloud15.jpg new file mode 100644 index 0000000..38fd512 Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-example-cloud15.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-example-cloud6.jpg b/docs/ru/img/driver-mqtt_MQTT-example-cloud6.jpg new file mode 100644 index 0000000..18ed069 Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-example-cloud6.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-example-mobile6.png b/docs/ru/img/driver-mqtt_MQTT-example-mobile6.png new file mode 100644 index 0000000..72fa127 Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-example-mobile6.png differ diff --git a/docs/ru/img/driver-mqtt_MQTT-example-modbus3.jpg b/docs/ru/img/driver-mqtt_MQTT-example-modbus3.jpg new file mode 100644 index 0000000..4474e99 Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-example-modbus3.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-example-modbus5.jpg b/docs/ru/img/driver-mqtt_MQTT-example-modbus5.jpg new file mode 100644 index 0000000..a5a5225 Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-example-modbus5.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-install3.jpg b/docs/ru/img/driver-mqtt_MQTT-install3.jpg new file mode 100644 index 0000000..ef8d80e Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-install3.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-server1.jpg b/docs/ru/img/driver-mqtt_MQTT-server1.jpg new file mode 100644 index 0000000..767e68a Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-server1.jpg differ diff --git a/docs/ru/img/driver-mqtt_MQTT-server4.jpg b/docs/ru/img/driver-mqtt_MQTT-server4.jpg new file mode 100644 index 0000000..8588efa Binary files /dev/null and b/docs/ru/img/driver-mqtt_MQTT-server4.jpg differ diff --git a/docs/ru/img/mqtt_client1.jpg b/docs/ru/img/mqtt_client1.jpg new file mode 100644 index 0000000..65cb458 Binary files /dev/null and b/docs/ru/img/mqtt_client1.jpg differ diff --git a/docs/ru/img/mqtt_client2.jpg b/docs/ru/img/mqtt_client2.jpg new file mode 100644 index 0000000..5da15ea Binary files /dev/null and b/docs/ru/img/mqtt_client2.jpg differ diff --git a/docs/ru/img/mqtt_client3.jpg b/docs/ru/img/mqtt_client3.jpg new file mode 100644 index 0000000..b0bb716 Binary files /dev/null and b/docs/ru/img/mqtt_client3.jpg differ diff --git a/docs/ru/img/mqtt_client4.jpg b/docs/ru/img/mqtt_client4.jpg new file mode 100644 index 0000000..4f1e7d9 Binary files /dev/null and b/docs/ru/img/mqtt_client4.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud1.jpg b/docs/ru/img/mqtt_example-cloud1.jpg new file mode 100644 index 0000000..c160999 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud1.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud10.png b/docs/ru/img/mqtt_example-cloud10.png new file mode 100644 index 0000000..e7a9b1d Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud10.png differ diff --git a/docs/ru/img/mqtt_example-cloud11.png b/docs/ru/img/mqtt_example-cloud11.png new file mode 100644 index 0000000..866e817 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud11.png differ diff --git a/docs/ru/img/mqtt_example-cloud12.png b/docs/ru/img/mqtt_example-cloud12.png new file mode 100644 index 0000000..5849999 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud12.png differ diff --git a/docs/ru/img/mqtt_example-cloud13.png b/docs/ru/img/mqtt_example-cloud13.png new file mode 100644 index 0000000..c8d2aed Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud13.png differ diff --git a/docs/ru/img/mqtt_example-cloud14.png b/docs/ru/img/mqtt_example-cloud14.png new file mode 100644 index 0000000..b2610b1 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud14.png differ diff --git a/docs/ru/img/mqtt_example-cloud15.jpg b/docs/ru/img/mqtt_example-cloud15.jpg new file mode 100644 index 0000000..38fd512 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud15.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud2.jpg b/docs/ru/img/mqtt_example-cloud2.jpg new file mode 100644 index 0000000..ae85299 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud2.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud3.jpg b/docs/ru/img/mqtt_example-cloud3.jpg new file mode 100644 index 0000000..79201d6 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud3.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud4.jpg b/docs/ru/img/mqtt_example-cloud4.jpg new file mode 100644 index 0000000..8f87e1f Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud4.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud5.jpg b/docs/ru/img/mqtt_example-cloud5.jpg new file mode 100644 index 0000000..e368d59 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud5.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud6.jpg b/docs/ru/img/mqtt_example-cloud6.jpg new file mode 100644 index 0000000..18ed069 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud6.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud7.jpg b/docs/ru/img/mqtt_example-cloud7.jpg new file mode 100644 index 0000000..f612082 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud7.jpg differ diff --git a/docs/ru/img/mqtt_example-cloud8.png b/docs/ru/img/mqtt_example-cloud8.png new file mode 100644 index 0000000..2cd9cf0 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud8.png differ diff --git a/docs/ru/img/mqtt_example-cloud9.png b/docs/ru/img/mqtt_example-cloud9.png new file mode 100644 index 0000000..888fec0 Binary files /dev/null and b/docs/ru/img/mqtt_example-cloud9.png differ diff --git a/docs/ru/img/mqtt_example-mobile1.jpg b/docs/ru/img/mqtt_example-mobile1.jpg new file mode 100644 index 0000000..3d5de4b Binary files /dev/null and b/docs/ru/img/mqtt_example-mobile1.jpg differ diff --git a/docs/ru/img/mqtt_example-mobile2.jpg b/docs/ru/img/mqtt_example-mobile2.jpg new file mode 100644 index 0000000..ebaaef1 Binary files /dev/null and b/docs/ru/img/mqtt_example-mobile2.jpg differ diff --git a/docs/ru/img/mqtt_example-mobile3.png b/docs/ru/img/mqtt_example-mobile3.png new file mode 100644 index 0000000..24cabbe Binary files /dev/null and b/docs/ru/img/mqtt_example-mobile3.png differ diff --git a/docs/ru/img/mqtt_example-mobile4.png b/docs/ru/img/mqtt_example-mobile4.png new file mode 100644 index 0000000..2d39c13 Binary files /dev/null and b/docs/ru/img/mqtt_example-mobile4.png differ diff --git a/docs/ru/img/mqtt_example-mobile6.png b/docs/ru/img/mqtt_example-mobile6.png new file mode 100644 index 0000000..72fa127 Binary files /dev/null and b/docs/ru/img/mqtt_example-mobile6.png differ diff --git a/docs/ru/img/mqtt_example-modbus1.jpg b/docs/ru/img/mqtt_example-modbus1.jpg new file mode 100644 index 0000000..6221949 Binary files /dev/null and b/docs/ru/img/mqtt_example-modbus1.jpg differ diff --git a/docs/ru/img/mqtt_example-modbus2.jpg b/docs/ru/img/mqtt_example-modbus2.jpg new file mode 100644 index 0000000..b810f1d Binary files /dev/null and b/docs/ru/img/mqtt_example-modbus2.jpg differ diff --git a/docs/ru/img/mqtt_example-modbus3.jpg b/docs/ru/img/mqtt_example-modbus3.jpg new file mode 100644 index 0000000..4474e99 Binary files /dev/null and b/docs/ru/img/mqtt_example-modbus3.jpg differ diff --git a/docs/ru/img/mqtt_example-modbus4.jpg b/docs/ru/img/mqtt_example-modbus4.jpg new file mode 100644 index 0000000..b165e2a Binary files /dev/null and b/docs/ru/img/mqtt_example-modbus4.jpg differ diff --git a/docs/ru/img/mqtt_example-modbus5.jpg b/docs/ru/img/mqtt_example-modbus5.jpg new file mode 100644 index 0000000..a5a5225 Binary files /dev/null and b/docs/ru/img/mqtt_example-modbus5.jpg differ diff --git a/docs/ru/img/mqtt_example-modbus6.jpg b/docs/ru/img/mqtt_example-modbus6.jpg new file mode 100644 index 0000000..47816ae Binary files /dev/null and b/docs/ru/img/mqtt_example-modbus6.jpg differ diff --git a/docs/ru/img/mqtt_install1.jpg b/docs/ru/img/mqtt_install1.jpg new file mode 100644 index 0000000..179079d Binary files /dev/null and b/docs/ru/img/mqtt_install1.jpg differ diff --git a/docs/ru/img/mqtt_install2.jpg b/docs/ru/img/mqtt_install2.jpg new file mode 100644 index 0000000..25b0022 Binary files /dev/null and b/docs/ru/img/mqtt_install2.jpg differ diff --git a/docs/ru/img/mqtt_install3.jpg b/docs/ru/img/mqtt_install3.jpg new file mode 100644 index 0000000..ef8d80e Binary files /dev/null and b/docs/ru/img/mqtt_install3.jpg differ diff --git a/docs/ru/img/mqtt_server1.jpg b/docs/ru/img/mqtt_server1.jpg new file mode 100644 index 0000000..767e68a Binary files /dev/null and b/docs/ru/img/mqtt_server1.jpg differ diff --git a/docs/ru/img/mqtt_server2.jpg b/docs/ru/img/mqtt_server2.jpg new file mode 100644 index 0000000..b9826f5 Binary files /dev/null and b/docs/ru/img/mqtt_server2.jpg differ diff --git a/docs/ru/img/mqtt_server3.jpg b/docs/ru/img/mqtt_server3.jpg new file mode 100644 index 0000000..2502d48 Binary files /dev/null and b/docs/ru/img/mqtt_server3.jpg differ diff --git a/docs/ru/img/mqtt_server4.jpg b/docs/ru/img/mqtt_server4.jpg new file mode 100644 index 0000000..8588efa Binary files /dev/null and b/docs/ru/img/mqtt_server4.jpg differ diff --git a/docs/ru/mqtt.md b/docs/ru/mqtt.md new file mode 100644 index 0000000..3fa9784 --- /dev/null +++ b/docs/ru/mqtt.md @@ -0,0 +1,741 @@ +![](MQTT) +## MQTT Broker и клиент + +[MQTT](http://mqtt.org/) (Message Queue Telemetry Transport) это легковесный протокол, +применяемый для общения между устройствами (M2M — machine-to-machine). +Он использует модель издатель-подписчик (publish/subscribe) для передачи сообщений поверх +протокола TCP/IP. Центральная часть протокола это MQTT-сервер или брокер, который имеет +доступ к издателю и подписчику. Этот протокол предельно примитивен: с коротким заголовком, +без контроля целостности (поэтому передача реализована поверх TCP), не накладывает никаких +ограничения на структуру, кодирование или схему данных. Единственное требование к данным в +каждом пакете – они должны сопровождаться идентификатором информационного канала. +Этот идентификатор в спецификации называется Topic Name. + +Протокол MQTT требует обязательного наличия брокера данных. Это центральная идея технологии. +Все устройства посылают данные только брокеру и принимают данные тоже только от него. +Получив пакет, брокер рассылает его всем устройствам в сети согласно их подписке. Чтобы +устройство что-то получило от брокера оно должно подписаться на топик. Топики возникают +динамически по факту подписки или по факту прихода пакета с данным топиком. От подписки +на топик можно отказаться. Таким образом топики представляют собой удобный механизм +организации связей разных видов: один ко многим, многие к одному и многие ко многим. + +**Важные моменты:** + +* устройства сами устанавливают связь с брокером, они могут находится за NAT и не иметь статических IP-адресов, +* можно применить протокол SSL для шифрования трафика, +* MQTT брокеры позволяют подключаться к ним через протокол WebSocket на 80-й порт, +* разные брокеры могут соединяться между собой подписываясь на сообщения друг у друга. + +## Установка + +Установка осуществляется на вкладке **Драйвера** странички [администрирования](http://www.yunkong2.net/?page_id=3800&lang=ru) системы. +В группе драйверов **Сетевые** находим строчку с названием **MQTT Adapter** и нажимаем кнопку со +значком плюса в этой строке справа. + +![](img/mqtt_install1.jpg) + +На экране появится всплывающее окно установки драйвера, в конце установки оно +автоматически закроется. + +![](img/mqtt_install2.jpg) + +Если все прошло удачно, на вкладке **Настройка драйверов** появится строка **mqtt.0** с установленным экземпляром драйвера. + +![](img/mqtt_install3.jpg) + +## Настройка + +Как писалось выше, протокол MQTT подразумевает наличие брокера и клиентов. +Сервер yunkong2 может выступать как в роли брокера, так и в роли клиента. +Настройка для указания режима работы - тип (сервер/брокер или клиент/подписчик) Рассмотрим каждый вариант. + +### Работа yunkong2 в качестве MQTT-брокера + +Основные настройки, если предполагается использование режима сервер/брокер, приведены на картинке: + +![](img/mqtt_server1.jpg) + +* **Use WebSockets** - если есть надобность использовать WEB-сокеты для соединения, необходимо установить эту опцию, при этом TCP-server будет работать параллельно с WebSocket-сервером, +* **Порт** -порт для установки соединения по TCP (значение по-умолчанию 1883), сервер WebSocket (см. опция выше) запускается по порту +1 (по-умолчанию 1884), +* **SSL** - данная опция используется, если необходимо шифровать трафик (TCP или WebSocket), при этом необходимо указать сертификаты - просто выбрать из списка заранее установленные (указываются в системных настройках, см. описание драйвера [администрирования](http://www.yunkong2.net/?page_id=3800&lang=ru) системы), +* **настройки аутентификации** (имя пользователя и пароль) - указываются, если необходима аутентификация конкретного пользователя, данная настройка всегда используется вместе с опцией SSL-шифрования (чтобы не передавать пароли в открытом виде через незащищенное соединение). +* **Маска для собственных значений** - шаблон (или несколько через запятую) для фильтрации переменных yunkong2, которые будут отправляться клиентам, можно использовать специальные символы чтобы указать группу сообщения (к примеру, `memRSS, mqtt.0` - могут передаваться все переменные состояния памяти всех драйверов и все переменные экземпляра драйвера **mqtt.0**) +* **Отсылать только изменения** - отправка данных клиенту будет произведена только в случае изменения переменной (если состояние просто обновилось - значение не поменялось, клиенту сообщение не будет отправлено), от клиента будет принято любое сообщение, даже если значение не изменилось, +* **Выдавать собственные значения при старте** - для каждого успешного соединения с клиентом ему будут переданы все известные состояния (определяются маской состояний) - для того, чтобы сообщить клиенту текущее состояние yunkong2, +* **Публиковать состояния при подписке** - сразу после подписки клиенту будет отправлено значение переменной, на которую он подписан (при первом старте или рестарте клиент получит значения переменных, на которые он подписан, можно использовать для инициализации переменных), +* **Префикс для всех значений** - если указано значение, то оно будет добавляться как префикс к каждому отправленному топику, к примеру, если указать `yunkong2/`, то все отправленные топики примерно следующего содержания: `yunkong2/mqtt/0/connected`, +* **Вывод лога для каждого изменения** - в лог-файле будет отображаться отладочная информация по каждому изменению, +* **Посылать не только команды, но и состояния (ack=true)** - если опция не активна, то клиенту будут отправляться только переменные/команды с ack=false, если флаг установлен, то будут переданы переменные не зависимо от состояния ack (false или true), +* **Максимальная длина имени топика** - максимальное кол-во символов для описания топика, включая служебные. + +В качестве примера рассмотрим обмен данными между клиентом на базе платы [arduino](https://www.arduino.cc/) и брокером - экземпляр драйвера mqtt.0 системы yunkong2. + +* клиент - плата для разработки [arduino UNO](https://www.arduino.cc/en/Main/ArduinoBoardUno) + [ethernet shield](https://store.arduino.cc/product/A000072) на базе чипа W5100, +* для работы с платой ethernet используется стандартная [библиотека](https://www.arduino.cc/en/Reference/Ethernet), для работы с MQTT библиотека [Pubsubclient](https://github.com/knolleary/pubsubclient), +* датчик AM2302 (температура и влажность) подключен на pin_8 для опроса используется библиотека с [DHTlib](https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib) с ресурса github.com, +* светодиод led_green подключен на pin_9, управление в дискретном режиме вкл/откл, +* брокер - система yunkong2 драйвер mqtt. + +Формат топиков обмена данными: + +* `example1/send_interval` - клиент подписан на изменение интервала отправки показаний температуры и влажности (значение int в секундах) +* `example1/temp` - клиент публикует с заданным интервалом значение температуры с датчика DHT22 (тип float), +* `example1/hum` -клиент публикует с заданным интервалом значение влажности с датчика DHT22 (тип float), +* `example1/led` -клиент подписан на изменение состояния светодиода (тип text on/off или 0/1 или true/false). + +Настройки драйвера будут следующие: + +![](img/mqtt_server2.jpg) + +Подключение по TCP (WebSocket не нужен), порт по-умолчанию 1883\. Клиент внутри локальной сети, поэтому шифровать трафик и проводить +аутентификацию пользователя нет необходимости. Отсылать будем только изменения, так как клиент подписан на интервал +отправки показаний и состояние светодиода, получать информацию только об обновлении (без изменения значения) +переменной нет смысла. Публиковать состояния при подписке - отметим эту опцию, так как при первом подключении +(или подключении после обрыва соединения) клиента он должен знать состояния переменных, на которые он +подписан (какой текущий интервал отправки и должен ли быть включен светодиод). Настройку отсылать +переменные с ack=true или false тоже стоит отметить, так как переменную (на которую подписан клиент) +может изменить любой драйвер/скрипт/VIS и все изменения надо отправлять клиенту. Полный код для платы +arduino будет выглядеть так: + +
+//Подключаем библиотеки
+#include 
+#include 
+#include  //https://github.com/knolleary/pubsubclient
+#include           //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+//Настройки сети
+byte mac[] = {  0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x31 };
+byte ip[] = { 192, 168, 69, 31 }; //IP-адрес платы arduino
+byte mqttserver[] = { 192, 168, 69, 51 }; //IP-адрес сервера yunkong2
+EthernetClient ethClient;
+void callback(char* topic, byte* payload, unsigned int length);
+PubSubClient client(mqttserver, 1883, callback, ethClient);
+//Глобальные переменные
+#define LED_pin 9
+unsigned int send_interval = 10; //интервал отправки показаний на сервер по-умолчанию 10 секунд
+unsigned long last_time = 0; //текущее время для таймера
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+//Функция обработки входящих соединений - прием данных по подписке
+void callback(char* topic, byte* payload, unsigned int length) {
+  Serial.println ("");
+  Serial.println ("-------");
+  Serial.println ("New callback of MQTT-broker");
+  //преобразуем тему(topic) и значение (payload) в строку
+  payload[length] = '\0';
+  String strTopic = String(topic);
+  String strPayload = String((char*)payload);
+  //Исследуем что "прилетело" от сервера по подписке:
+  //Изменение интервала опроса
+  if (strTopic == "example1/send_interval") {
+    int tmp = strPayload.toInt();
+    if (tmp == 0) {
+      send_interval = 10;
+    } else {
+      send_interval = strPayload.toInt();
+    }
+  }
+  //Управление светодиодом
+  if (strTopic == "example1/led") {
+    if (strPayload == "off" || strPayload == "0" || strPayload == "false") digitalWrite(LED_pin, LOW);
+    if (strPayload == "on" || strPayload == "1" || strPayload == "true") digitalWrite(LED_pin, HIGH);
+  }  
+  Serial.print (strTopic);
+  Serial.print (" ");
+  Serial.println (strPayload);
+  Serial.println ("-------");
+  Serial.println ("");  
+}
+void setup() {
+  Serial.begin(9600);
+  Serial.println("Start...");
+  //стартуем сетевое подключение
+  Ethernet.begin(mac, ip);
+  Serial.print("IP: ");
+  Serial.println(Ethernet.localIP());
+  //Инициализируем порты ввода-вывода, прописываем начальные значения
+  pinMode(LED_pin, OUTPUT);
+  digitalWrite(LED_pin, LOW); //при светодиод отключен
+}
+void loop() {
+  //Если соединение MQTT неактивно, то пытаемся установить его и опубликовать/подписаться
+  if (!client.connected()) {
+    Serial.print("Connect to MQTT-boker...  ");
+    //Подключаемся и публикуемся/подписываемся
+    if (client.connect("example1")) {
+      Serial.println("success");
+      //Значение с датчиков
+      if (DHT.read22(DHT22_PIN)==DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example1/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example1/temp", buff);
+      }
+      //Подписываемся на интервал опроса
+      client.subscribe("example1/send_interval");
+      //Подписываемся на переменную управления светодиодом
+      client.subscribe("example1/led");
+    } else {
+      //Если не подключились, ждем 10 секунд и пытаемся снова
+      Serial.print("Failed, rc=");
+      Serial.print(client.state());
+      Serial.println(" try again in 10 seconds");
+      delay (10000);
+    }
+  //Если соединение активно, то отправляем данные на сервер с заданным интервалом времени
+  } else {
+    if(millis() > (last_time + send_interval*1000)) {
+      last_time = millis();
+      if (DHT.read22(DHT22_PIN)==DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example1/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example1/temp", buff);
+      }
+    }
+  }
+  //Проверка входящих соединений по подписке
+  client.loop();
+}
+
+ +### Работа yunkong2 в качестве MQTT-клиента + +Чтобы экземпляр драйвера MQTT заработал как клиент/подписчик - нужно в настройках выбрать соответствующий тип. +При этом набор настроек немного поменяется: + +![](img/mqtt_client1.jpg) + +* **Настройки соединения** - указывается URL и порт брокера (если необходимо шифровать трафик, то указывается SSL) - настройки для подключения к брокеру, +* **Настройки аутентификации** - имя пользователя и пароль, если брокер требует аутентификацию (уместно использовать SSL-шифрование, чтобы не передавать пароль в открытом виде), +* **Patterns** - маска для переменных, на которые клиент подписывается (переменные брокера), значения перечисляются через запятую, для указания множества используется символ # (решетка), +* **Маска для собственных значений** - фильтр переменных, которые необходимо публиковать (переменные клиента), значения перечисляются через запятую, для указания множества используется символ * (звездочка), +* **Отсылать только изменения** - клиент будет публиковать только переменные, которые изменили значение (согласно маски), +* **Выдавать собственные значения при старте** - если эту опцию отметить, то будут публиковаться все состояния (согласно маски) каждый раз, когда устанавливается соединение, чтобы объявить доступные собственные переменные и их значения, +* **Префикс для всех значений** - если указано значение, то оно будет добавляться как префикс к каждому публикуемому топику, к примеру, если указать `client1/`, то все публикуемые топики будут примерно следующего содержания: `client1/javascript/0/cubietruck`, +* **Вывод лога для каждого изменения** - в лог-файле будет отображаться отладочная информация по каждому изменению, +* **Посылать не только команды, но и состояния (ack=true)** - если данная опция не отмечена, то брокеру отправляются только переменные/команды с ack=false, если опцию отметить, то будут отправляться все данные, независимо от ack=true или ack=false, +* **Максимальная длина топика** - максимальное кол-во символов для описания топика, включая служебные. + +Примеры для задания маски подписки на переменные (patterns). Рассмотрим топики: + +* "Sport" +* "Sport/Tennis" +* "Sport/Basketball" +* "Sport/Swimming" +* "Sport/Tennis/Finals" +* "Sport/Basketball/Finals" +* "Sport/Swimming/Finals" + +Если необходимо подписаться на определенное множество топиков, можно использовать символы # (решетка) или + (знак плюс). + +* "Sport/Tennis/#" (подписка только на "Sport/Tennis" и "Sport/Tennis/Finals") +* "Sport/Tennis/+" (подписка только на  "Sport/Tennis/Finals", но не "Sport/Tennis") + +Для JMS топиков, если нужно подписаться на все топики "Finals", можно использовать символы # (решетка) или + (знак плюс) + +* "Sport/#/Finals" +* "Sport/+/Finals" + +Для MQTT топиков, если нужно подписаться на все топики "Finals", можно использовать символ + (знак плюс) + +* "Sport/+/Finals" + +В качестве примера рассмотрим обмен данными между двумя системами yunkong2. Есть +работающая система yunkong2 на плате BananaPi (IP-адрес 192.168.69.51), на ней +запущен MQTT-драйвер в режиме сервер/брокер из примера выше. К серверу +подключается клиент, который публикует данные с датчика DHT22 - температуру и +влажность, а так же подписывается на переменные интервал передачи показаний и +состояние светодиода (включить/отключить) - так же из примера выше. Вторая работающая +система yunkong2 на плате Cubietruck, на ней запустим MQTT-драйвер в режиме клиент/подписчик. +Он подпишется на переменные температура и влажность брокера (который в свою очередь получает от +другого клиента) и будет публиковать все переменные скрипта - [состояние АКБ](http://www.yunkong2.net/?page_id=4268&lang=ru#_Li-polLi-ion) платы +(только изменения). Настройки клиента будут примерно следующие: + +![](img/mqtt_client2.jpg) + +Тип соединения - клиент/подписчик, указывается IP-адрес брокера и порт (по-умолчанию 1883). +Шифрование трафика и аутентификация не нужны. + +Маска для подписки (Patterns) - `mqtt/0/example1/hum,mqtt/0/example1/temp` - клиент подписывается только на температуру и +влажность (значения через запятую без пробелов). + +Маска данных для публикации - `javascript.0.cubietruck.battery.*` - публикуются все переменные +скрипта `cubietruck` в группе `battery` драйвера `javascript.0`. + +Отсылать только изменения - отправляем переменные состояния АКБ (нет смысла отправлять, если значение не изменилось). +Выдавать собственные значения при старте - при старте драйвера, клиент сразу опубликует все переменные +согласно маске - даже если они нулевые или пустые, чтобы создать переменные в брокере. Посылать данные +с ack=false - переменные работы АКБ обновляются драйвером javascript, поэтому они всегда ack=false. + +Результат работы на стороне клиента (данные температуры и влажности другого клиента - см. пример выше): + +![](img/mqtt_client3.jpg) + +Результат работы со стороны брокера (данные состояния АКБ клиента): + +![](img/mqtt_client4.jpg) + +## Применение - шлюз протоколов MQTT - ModBus RTU + +Драйвер MQTT можно использовать как шлюз различных протоколов, чтобы подключить новые +устройства в систему yunkong2 или любую другую. Универсальной базой для разработки подобных +решений являются платы arduino. В сети много примеров, библиотек и наработок. Огромное сообщество +работает с этими контроллерами и в систему интегрированы множество устройств и оборудования. Для примера, +рассмотрим распространенный промышленный протокол ModBus. В системе yunkong2 имеется драйвер для работы с ним - версии +ModBus TCP (по сети ethernet). Множество датчиков, контроллеров и исполнительных устройств работают физически по +сети RS-485/232 и протоколу ModBus RTU. Чтобы интегрировать их можно применить шлюз MQTT - ModBus RTU на базе платформы +arduino. + +Рассмотрим пример. + +**Имеется датчик температуры и влажности** (для теста на базе платы arduino pro mini и сенсора DHT22),  +который выдает данные по ModBUS RTU: + +* Порт UART (можно с помощью микросхемы MAX485 преобразовать в интерфейс RS-485) работает на скорости 9600 с параметрами 8E1 (1 start bit, 8 data bits, 1 Even parity bit, 1 stop bit) +* Адрес ModBus - 10 +* температура - адрес 0 значение, умноженное на 10 (read function 3) +* влажность - адрес 1 значение, умноженное на 10 (read function 3) +* PWM LED - адрес 2 значение 0...1023 для проверки функции записи (write function 6) + +Схема соединения: + +![](img/mqtt_example-modbus1.jpg) +by Fritzing + +Код для контроллера arduino pro mini получается следующий: + +
+#include  //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+#include  //https://code.google.com/archive/p/simple-modbus/
+#include  //https://github.com/PaulStoffregen/MsTimer2
+//регистры modbus
+enum {
+  TEMP,
+  HUM,
+  PWM,
+  TEST,
+  HOLDING_REGS_SIZE
+};
+#define ID_MODBUS 10 //адрес modbus slave устройства
+unsigned int holdingRegs[HOLDING_REGS_SIZE]; //массив регистров modbus
+//датчик температуры и влажности DHT22
+dht DHT;
+#define DHT22_PIN 2
+#define LED 9 //светодиод подключен на PWM-пин 9
+void setup() {
+  //конфигурируем modbus
+  modbus_configure(&Serial, 9600, SERIAL_8E1, ID_MODBUS, 0, HOLDING_REGS_SIZE, holdingRegs);
+  holdingRegs[TEST] = -157; //для теста отрицательных значений
+  //инициализируем таймер 2 секунды обновления данных в регистрах температуры и влажности
+  MsTimer2::set(2000, read_sensors);
+  MsTimer2::start(); //запускаем таймер
+  pinMode(LED, OUTPUT); //инициализация порта светодиода
+}
+//функция, запускаемая по таймеру каждые 2 секунды
+void read_sensors() {
+  if (DHT.read22(DHT22_PIN)==DHTLIB_OK) { //если данные с датчика DHT22 удалось прочитать
+    //записываем в регистр влажности целочисленное значение
+    holdingRegs[HUM] = 10 * DHT.humidity;
+    //записываем в регистр температуры целочисленное значение
+    holdingRegs[TEMP]= 10 * DHT.temperature;
+  } else {
+    //если не удалось прочитать данные с датчика DHT22, записываем в регистры нули
+    holdingRegs[HUM] = 0;
+    holdingRegs[TEMP] = 0;
+  }
+}
+void loop() {
+  modbus_update(); //обновляем данные modbus
+  //данные из регистра управления светодиодом передаем в ШИМ (битовый сдвиг на 2 разряда)
+  analogWrite(LED, holdingRegs[PWM]>>2);
+}
+
+ +* порт (выбрать из списка к какому порту подключен serial платы ардуино); +* скорость и прочие параметры - 9600 8E1; +* slave id: 10, чтение: функция №3 read holding registers, начальный адрес: 0, число регистров: 3, +* slave id: 10, запись: функция №6 write single register, начальный адрес: 2, + +Ответ в программе при чтении должен быть примерно следующий: + +![](img/mqtt_example-modbus2.jpg) + +Ответ в программе при записи: + +![](img/mqtt_example-modbus3.jpg) + +**Теперь настроем сам шлюз и подключим его в yunkong2** Шлюз будет на базе платформы +arduino MEGA 2560 с ethernet shield - клиент MQTT, брокер - экземпляр драйвера mqtt.0 системы yunkong2. Выбор именно MEGA 2560 +обусловлен тем, что на этой плате более одного UART-порта, соответственно нулевой Serial0 (pin_0 (RX) и зшт_1 (TX)) или +просто Serial - используем для вывода отладочных сообщений, а Serial1 (pin_19 (RX) и pin_18 (TX)) - для +работы с slave-устройством по ModBus. + +* клиент - плата для разработки [arduino MEGA 2560](https://www.arduino.cc/en/Main/ArduinoBoardMega2560) + [ethernet shield](https://store.arduino.cc/product/A000072) на базе чипа W5100; +* для работы с платой ethernet используется стандартная [библиотека](https://www.arduino.cc/en/Reference/Ethernet), для работы с MQTT библиотека [Pubsubclient](https://github.com/knolleary/pubsubclient); +* для опроса по modbus используется библиотека [SimpleModbus](https://code.google.com/archive/p/simple-modbus/) версии master; +* опрос по порту UART (просто соединить RX порт master, TX порт slave и соответственно TX порт master, RX порт slave), порт управления передачей не используется (он для RS-485); +* параметры порта: скорость 9600, 8Е1; +* адрес slave-устройства 10, функция чтения №3 (read holding registers), функция записи №6 (write single register); +* брокер - система yunkong2 драйвер mqtt. + +Формат топиков обмена данными: + +* `modbusgateway/send_interval` - клиент подписан на изменение интервала отправки показаний температуры и влажности (значение int в секундах) +* `modbusgateway/temp` - клиент публикует с заданным интервалом значение температуры с датчика DHT22 (тип float), +* `modbusgateway/hum` -клиент публикует с заданным интервалом значение влажности с датчика DHT22 (тип float), +* `modbusgateway/led` -клиент подписан на изменение состояния светодиода (ШИМ управление, значения 0...1024). + +Схема соединений получится примерно такая: + +![](img/mqtt_example-modbus6.jpg) +By Fritzing[/caption] +Для теста slave-устройство запитаем от master-устройства. Master в свою очередь, будет работать от USB-порта, +по которому ведется отладка (Serial0). Настройки драйвера будут следующие: + +![](img/mqtt_server2.jpg) + +Подключение по TCP (WebSocket не нужен), порт по-умолчанию 1883\. Клиент внутри локальной сети, поэтому шифровать трафик и +проводить аутентификацию пользователя нет необходимости. Отсылать будем только изменения, так как клиент подписан на интервал +отправки показаний и состояние светодиода, получать информацию только об обновлении (без изменения значения) переменной нет смысла. +Публиковать состояния при подписке - отметим эту опцию, так как при первом подключении (или подключении после обрыва соединения) +клиента он должен знать состояния переменных, на которые он подписан (какой текущий интервал отправки и должен ли быть включен светодиод). +Настройку отсылать переменные с ack=true или false тоже стоит отметить, так как переменную (на которую подписан клиент) может изменить +любой драйвер/скрипт/VIS и все изменения надо отправлять клиенту. + +Полный код для платы arduino MEGA 2560 будет выглядеть так: + +
+//Подключаем библиотеки
+#include 
+#include 
+#include        //https://github.com/knolleary/pubsubclient
+#include  //https://code.google.com/archive/p/simple-modbus/
+//Настройки сети
+byte mac[] = {  0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x31 };
+byte ip[] = { 192, 168, 69, 31 }; //IP-адрес платы arduino
+byte mqttserver[] = { 192, 168, 69, 51 }; //IP-адрес сервера yunkong2
+//Объекты/переменные/функции ethernet и MQTT
+EthernetClient ethClient;
+void callback(char* topic, byte* payload, unsigned int length);
+PubSubClient client(mqttserver, 1883, callback, ethClient);
+//Переменные временного интервала и буфер для отправки данных по MQTT
+unsigned int send_interval = 10; //интервал отправки показаний на сервер по-умолчанию 10 секунд
+unsigned long last_time = 0; //текущее время для таймера
+char buff[20];
+//Параметры порта Serial1 (19 (RX) and 18 (TX))
+#define baud 9600       //скорость порта
+#define timeout 1000    //интервал времени ожидания ответа /(мс)
+#define polling 200     //интервал опроса (мс)
+#define retry_count 10  //кол-во повторов при неудачном опросе
+#define TxEnablePin 0   //пин управления передачей для RS485 (в UART не используется = 0)
+// Общая сумма доступной памяти на master устройстве для хранения данных
+// Из слейва запрашиваем 4 регистра, в массиве regs должно быть не меньше 4х ячеек
+#define TOTAL_NO_OF_REGISTERS 4
+//Массив регистров для работы с modbus (хранение, чтение, запись)
+unsigned int regs[TOTAL_NO_OF_REGISTERS];
+//Определение пакетов mmodbus
+enum {
+  TEMP,
+  HUM,
+  PWM,
+  TEST,
+  TOTAL_NO_OF_PACKETS //всегда последней записью
+};
+//Создание массива пакетов modbus
+Packet packets[TOTAL_NO_OF_PACKETS];
+
+//Функция обработки входящих соединений - прием данных по подписке
+void callback(char* topic, byte* payload, unsigned int length) {
+  Serial.println ("");
+  Serial.println ("-------");
+  Serial.println ("New callback of MQTT-broker");
+  //преобразуем тему(topic) и значение (payload) в строку
+  payload[length] = '\0';
+  String strTopic = String(topic);
+  String strPayload = String((char*)payload);
+  //Исследуем что "прилетело" от сервера по подписке:
+  //Изменение интервала опроса
+  if (strTopic == "modbusgateway/send_interval") {
+    int tmp = strPayload.toInt();
+    if (tmp == 0) {
+      send_interval = 10;
+    } else {
+      send_interval = strPayload.toInt();
+    }
+  }
+  //Управление светодиодом значение int от 0 до 1023
+  if (strTopic == "modbusgateway/led") {
+    int tmp = strPayload.toInt();
+    if (tmp >= 0 && tmp <=1023) {
+      regs[2] = tmp;
+    }
+  }  
+  Serial.print (strTopic);
+  Serial.print (" ");
+  Serial.println (strPayload);
+  Serial.println ("-------");
+  Serial.println ("");  
+}
+
+void setup() {
+  Serial.begin(9600);
+  Serial.println("Start...");
+  //стартуем сетевое подключение
+  Ethernet.begin(mac, ip);
+  Serial.print("IP: ");
+  Serial.println(Ethernet.localIP());
+  //Инициализируем все пакеты modbus
+  modbus_construct(&packets[TEMP], 10, READ_HOLDING_REGISTERS, 0, 1, 0); //температура
+  modbus_construct(&packets[HUM], 10, READ_HOLDING_REGISTERS, 1, 1, 1); //влажность
+  modbus_construct(&packets[PWM], 10, PRESET_SINGLE_REGISTER, 2, 1, 2); //данные ШИМ для светодиода
+  modbus_construct(&packets[TEST], 10, READ_HOLDING_REGISTERS, 3, 1, 3); //тест
+  //Конфигурируем соединение modbus (порт serial1, скорость и пр.)
+  modbus_configure(&Serial1, baud, SERIAL_8E1, timeout, polling, retry_count, TxEnablePin, packets, TOTAL_NO_OF_PACKETS, regs);
+}
+
+void loop() {
+  //Обоновляем данные в регистрах modbus
+  modbus_update();
+  //Если соединение MQTT неактивно, то пытаемся установить его и опубликовать/подписаться
+  if (!client.connected()) {
+    Serial.print("Connect to MQTT-boker...  ");
+    //Подключаемся и публикуемся/подписываемся
+    if (client.connect("modbusgateway")) {
+      Serial.println("success");
+      //Значение с датчиков температуры и влажности
+      dtostrf((float)regs[0]/10, 5, 1, buff);
+      client.publish("modbusgateway/temp", buff);
+      dtostrf((float)regs[1]/10, 5, 1, buff);
+      client.publish("modbusgateway/hum", buff);
+      //Подписываемся на интервал опроса
+      client.subscribe("modbusgateway/send_interval");
+      //Подписываемся на переменную управления светодиодом
+      client.subscribe("modbusgateway/led");
+    } else {
+      //Если не подключились, пытаемся снова
+      Serial.print("Failed, rc=");
+      Serial.print(client.state());
+      Serial.println(" try again");
+      delay(10000);
+    }
+  //Если соединение активно, то отправляем данные на сервер с заданным интервалом времени
+  } else {
+    if(millis() > (last_time + send_interval*1000)) {
+      last_time = millis();
+      //Значение с датчиков температуры и влажности
+      dtostrf((float)regs[0]/10, 5, 1, buff);
+      client.publish("modbusgateway/temp", buff);
+      dtostrf((float)regs[1]/10, 5, 1, buff);
+      client.publish("modbusgateway/hum", buff);
+    }
+  }
+  //Проверка входящих соединений по подписке
+  client.loop();
+}
+
+ +## Применение - подключение мобильных клиентов + +В последнее время протокол MQTT получил большое распространение ввиду простоты, экономии трафика и хорошей +проработке библиотек под разные платформы. Существует множество программ для работы с MQTT на мобильных +устройствах, к примеру [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=ru). +С помощью этой программы можно подключиться к MQTT-брокеру в локальной сети или в сети интернет. Рассмотрим пример - +в роли брокера будет выступать система yunkong2, к которой по MQTT будет подключаться клиент - приложение IoT MQTT Dashboard. +В данном примере будем управлять светом с помощью контроллера [MegaD-328](http://www.ab-log.ru/smart-house/ethernet/megad-328), +который подключен к yunkong2 с помощью драйвера [MegaD](http://www.yunkong2.net/?page_id=4052&lang=ru). +Управляет непосредственно реле (MegaD порт **P7**) света в холле специальный скрипт, который подписывается на состояние +порта-кнопки **P0** и состояние MQTT-переменной **mqtt.0.remotectrl.light.hall**, которую будет публиковать мобильный клиент. +При этом скрипт переключает состояние порта, привязанного к реле (порт P7), т.е. инвертирует его. + +Получается, что при каждом +нажатии на кнопку, электрически подключенную к порту **P0** (вылавливается состояние **true**) и при каждой публикации +переменной **mqtt.0.remotectrl.light.hall** значением так же **true**, порт **P7** будет включать или выключать свет. +Текст скрипта будет примерно такой: + +
+//Управление освещением в зале с помощью кнопки порт p0 контроллера MegaD драйвер экземпляр megad.0
+on({id: 'megad.0.p0_P0', change: 'any'}, function (obj) {
+   if (obj.newState.val !== '' || typeof obj.newState.val !== "undefined"){
+     if (obj.newState.val === true) {
+       if (getState('megad.0.p7_P7').val === true) {
+         setState('megad.0.p7_P7', false);
+       } else {
+         setState('megad.0.p7_P7', true);
+       }
+     }
+   }
+});
+//Управление освещением в зале удаленно по MQTT топик "mqtt.0.remotectrl.light.hall"
+on({id: 'mqtt.0.remotectrl.light.hall', change: 'any'}, function (obj) {
+  if (obj.newState.val !== '' || typeof obj.newState.val !== "undefined"){
+    if (obj.newState.val === true) {
+      if (getState('megad.0.p7_P7').val === true) {
+        setState('megad.0.p7_P7', false);
+      } else {
+        setState('megad.0.p7_P7', true);
+      }
+    }
+  }
+});
+
+ +## Применение - работа с облачными серверами + +Описанный выше пример имеет ряд недостатков. Во-первых, не всегда мобильный клиент может находиться в одной локальной сети с сервером yunkong2, во-вторых, даже если осуществить проброс портов в интернет и защитить соединение, не всегда сам сервер yunkong2 может принять входящие подключения (находится за NAT, к которому нет доступа для настройки). В глобальной сети много различных сервисов, которые поддерживают MQTT - платных и бесплатных, к примеру отправка погодных данных, геолокации и пр. Некоторые сервисы могут выступать в качестве брокера протокола MQTT и их можно использовать как шлюз (мост) для вывода данных из yunkong2 в глобальную сеть или для получения данных в yunkong2. В качестве примера рассмотрим работу связки: + +* сервер/брокер - сервис [cloudmqtt.com](https://www.cloudmqtt.com/) (есть бесплатный тариф), +* клиент/подписчик - система yunkong2 с выходом в сеть интернет, публикует данные температуры и влажности (см. [пример выше](http://www.yunkong2.net/?page_id=4643&lang=ru#_yunkong2__MQTT)), публикует реальное состояние портов **P7-P13** (реле MegaD драйвера **megad.0** - управление освещением), подписка на переменные удаленного управления светом (экземпляр драйвера **mqtt.0**), +* клиент/подписчик - приложение [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=ru) для удаленной работы - подписка на данные сенсора температуры и влажности, подписка на реальное состояние портов **P7-P13** (реле MegaD драйвера **megad.0**), публикация переменных удаленного управления светом (экземпляр драйвера **mqtt.0**). + +В итоге получается следующая структура: + +![](img/mqtt_example-cloud1.jpg) + +Связка драйвер **mqtt.1** (брокер) - Arduino UNO + Ethernet + DHT22 (клиент) как в [примере выше](http://www.yunkong2.net/?page_id=4643&lang=ru#_yunkong2__MQTT) с +несколькими изменениями. Настройки экземпляра драйвера **mqtt.1**: + +[![](img/mqtt_example-cloud2.jpg)](img/mqtt_example-cloud2.jpg) + +Код для платформы arduino: + +
+//Подключаем библиотеки
+#include 
+#include 
+#include  //https://github.com/knolleary/pubsubclient
+#include           //https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
+//Настройки сети
+byte mac[] = {  0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x31 };
+byte ip[] = { 192, 168, 69, 31 }; //IP-адрес платы arduino
+byte mqttserver[] = { 192, 168, 69, 51 }; //IP-адрес сервера yunkong2
+EthernetClient ethClient;
+void callback(char* topic, byte* payload, unsigned int length);
+PubSubClient client(mqttserver, 1884, callback, ethClient);
+//Глобальные переменные
+unsigned int send_interval = 10; //интервал отправки показаний на сервер по-умолчанию 10 секунд
+unsigned long last_time = 0; //текущее время для таймера
+dht DHT;
+#define DHT22_PIN 8
+char buff[20];
+
+//Функция обработки входящих соединений - прием данных по подписке
+void callback(char* topic, byte* payload, unsigned int length) {
+  Serial.println ("");
+  Serial.println ("-------");
+  Serial.println ("New callback of MQTT-broker");
+  //преобразуем тему(topic) и значение (payload) в строку
+  payload[length] = '\0';
+  String strTopic = String(topic);
+  String strPayload = String((char*)payload);
+  //Исследуем что "прилетело" от сервера по подписке:
+  //Изменение интервала опроса
+  if (strTopic == "example2/send_interval") {
+    int tmp = strPayload.toInt();
+    if (tmp == 0) {
+      send_interval = 10;
+    } else {
+      send_interval = strPayload.toInt();
+    }
+  }
+  Serial.print (strTopic);
+  Serial.print (" ");
+  Serial.println (strPayload);
+  Serial.println ("-------");
+  Serial.println ("");  
+}
+void setup() {
+  Serial.begin(9600);
+  Serial.println("Start...");
+  //стартуем сетевое подключение
+  Ethernet.begin(mac, ip);
+  Serial.print("IP: ");
+  Serial.println(Ethernet.localIP());
+  //Инициализируем порты ввода-вывода, прописываем начальные значения
+}
+void loop() {
+  //Если соединение MQTT неактивно, то пытаемся установить его и опубликовать/подписаться
+  if (!client.connected()) {
+    Serial.print("Connect to MQTT-boker...  ");
+    //Подключаемся и публикуемся/подписываемся
+    if (client.connect("example2")) {
+      Serial.println("success");
+      //Значение с датчиков
+      if (DHT.read22(DHT22_PIN)==DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example2/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example2/temp", buff);
+      }
+      //Подписываемся на интервал опроса
+      client.subscribe("example2/send_interval");
+    } else {
+      //Если не подключились, ждем 10 секунд и пытаемся снова
+      Serial.print("Failed, rc=");
+      Serial.print(client.state());
+      Serial.println(" try again in 10 seconds");
+      delay (10000);
+    }
+  //Если соединение активно, то отправляем данные на сервер с заданным интервалом времени
+  } else {
+    if(millis() > (last_time + send_interval*1000)) {
+      last_time = millis();
+      if (DHT.read22(DHT22_PIN)==DHTLIB_OK) {
+        dtostrf(DHT.humidity, 5, 2, buff);
+        client.publish("example2/hum", buff);
+        dtostrf(DHT.temperature, 5, 2, buff);
+        client.publish("example2/temp", buff);
+      }
+    }
+  }
+  //Проверка входящих соединений по подписке
+  client.loop();
+}
+
+ +* тип соединения - клиент/подписчик, +* настройки соединения - указываем URL выданный в панели управления [cloudmqtt.com](https://www.cloudmqtt.com/) порт будем использовать **22809**, который работает с **SSL**, +* в настройках аутентификации указываем имя пользователя и пароль, +* patterns - наш клиент yunkong2 будет подписан на все топики, что есть в облаке, поэтому указываем здесь символ решетка (**#**), можно использовать маску и подписаться выборочно, +* маска для собственных значений - клиент будет публиковать на сервер значения температуры/влажности и состояния всех портов megaD (порты с реле **P7-P13**),в этом поле через запятую указываем необходимые переменные: **mqtt.1.example2.hum,mqtt.1.example2.temp,megad.0.p7_P7,megad.0.p8_P8,megad.0.p9_P9,megad.0.p10_P10,megad.0.p11_P11,megad.0.p12_P12,megad.0.p13_P13**, +* отсылать только изменения - ставим галочку, публиковать будем только изменения, +* выдавать собственные значения при старте - так же указываем для создания топиков, +* посылать не только команды, но и состояния (ack=true) - необходимо отметить эту настройку, так как данные температуры/влажности обновляются драйвером mqtt (ack=true). + +Настройки сохраняем, убеждаемся, что соединение установилось +(можно на панели управления [cloudmqtt.com](https://www.cloudmqtt.com/) посмотреть лог сервера). Через +некоторое время появятся данные (в панели ссылка **WebsocketUI**): + +![](img/mqtt_example-cloud7.jpg) + +В итоге остается только настроить мобильный клиент, к примеру [IoT MQTT Dashboard](https://play.google.com/store/apps/details?id=com.thn.iotmqttdashboard&hl=ru). +Создаем новое подключение: + +![](img/mqtt_example-cloud8.png) + +Создаем топики для публикации (на примере освещения зала - порт **P7** MegaD): + +![](img/mqtt_example-cloud9.png) + +Создаем топики подписок (температура, влажность, освещение зала порт **P7** MegaD): + +![](img/mqtt_example-cloud10.png) + +![](img/mqtt_example-cloud11.png) + +![](img/mqtt_example-cloud12.png) + +В итоге dashboard может выглядеть примерно так: + +![](img/mqtt_example-cloud13.png) + +![](img/mqtt_example-cloud14.png) + +После создания публикаций на мобильном устройстве, в экземпляре драйвера **mqtt.0** системы yunkong2 должны появиться переменные управления светом, +которые следует использовать в скрипте управления освещением (см. [пример выше](http://www.yunkong2.net/?page_id=4643&lang=ru#_8211)): + +![](img/mqtt_example-cloud15.jpg) + +Поздравляем! Теперь вы сможете управления системой yunkong2 и +получать от нее данные через облачный сервис! \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..06db6b4 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,400 @@ +'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 += '};'; + + 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, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); + }); + + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} +function languages2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} + +gulp.task('adminWords2languages', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('adminWords2languagesFlat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('adminLanguagesFlat2words', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('adminLanguages2words', function (done) { + languages2words('./admin/'); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + var news = iopackage.common.news; + var newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('updateReadme', function (done) { + var readme = fs.readFileSync('README.md').toString(); + var pos = readme.indexOf('## Changelog\n'); + if (pos !== -1) { + var readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + var readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) === -1) { + var timestamp = new Date(); + var date = timestamp.getFullYear() + '-' + + ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ('0' + (timestamp.getDate()).toString(10)).slice(-2); + + var news = ''; + if (iopackage.common.news && iopackage.common.news[pkg.version]) { + news += '* ' + iopackage.common.news[pkg.version].en; + } + + fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + done(); +}); + +gulp.task('default', ['updatePackages', 'updateReadme']); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..94e1074 --- /dev/null +++ b/io-package.json @@ -0,0 +1,89 @@ +{ + "common": { + "name": "mqtt", + "version": "2.0.4", + "title": "MQTT Broker/Client", + "titleLang": { + "en": "MQTT Broker/Client" + }, + "desc": { + "en": "This adapter allows to send and receive MQTT messages from yunkong2 and to be a broker" + }, + "docs": { + "en": "docs/en/mqtt.md" + }, + "license": "MIT", + "platform": "Javascript/Node.js", + "mode": "daemon", + "messagebox": true, + "readme": "https://git.spacen.net/yunkong2/yunkong2.mqtt/blob/master/README.md", + "loglevel": "info", + "icon": "mqtt.png", + "materialize": true, + "keywords": [ + "notification", + "MQTT", + "message" + ], + "extIcon": "https://git.spacen.net/raw/yunkong2/yunkong2.mqtt/master/admin/mqtt.png", + "type": "protocols", + "config": { + "width": 800, + "height": 850, + "minWidth": 400, + "minHeight": 400 + }, + "news": { + "2.0.4": { + "en": "news", + "de": "neues", + "ru": "новое" + } + } + }, + "native": { + "type": "client", + "clientId": "", + "port": 1883, + "ssl": false, + "user": "", + "pass": "", + "url": "localhost", + "patterns": "#", + "onchange": true, + "publishAllOnStart": true, + "debug": false, + "publish": "*", + "certPublic": "", + "certPrivate": "", + "certChained": "", + "prefix": "", + "sendAckToo": false, + "webSocket": false, + "maxTopicLength": 100, + "publishOnSubscribe": true, + "extraSet": false, + "sendOnStartInterval": 2000, + "sendInterval": 10, + "keepalive": 1000, + "reconnectPeriod": 10, + "connectTimeout": 30, + "clean": false, + "defaultQoS": 0, + "retain": false, + "retransmitInterval": 2000, + "retransmitCount": 10, + "storeClientsTime": 1440 + }, + "objects": [], + "instanceObjects": [ + { + "_id": "info", + "type": "channel", + "common": { + "name": "Information" + }, + "native": {} + } + ] +} \ No newline at end of file diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 0000000..d51c874 --- /dev/null +++ b/lib/client.js @@ -0,0 +1,375 @@ +'use strict'; + +const mqtt = require('mqtt'); +const utils = require(__dirname + '/utils'); +const tools = require(require(__dirname + '/utils').controllerDir + '/lib/tools'); +const state2string = require(__dirname + '/common').state2string; +const convertTopic2id = require(__dirname + '/common').convertTopic2id; +const convertID2topic = require(__dirname + '/common').convertID2topic; + +const messageboxRegex = new RegExp('\\.messagebox$'); + +function MQTTClient(adapter, states) { + if (!(this instanceof MQTTClient)) return new MQTTClient(adapter, states); + + let client = null; + let topic2id = {}; + let id2topic = {}; + const namespaceRegEx = new RegExp('^' + adapter.namespace.replace('.', '\\.') + '\\.'); + let connected = false; + + this.destroy = () => { + if (client) { + client.end(); + client = null; + } + }; + + this.onStateChange = (id, state) => send2Server(id, state); + + function send2Server(id, state, cb) { + if (!client) return; + const topic = id2topic[id]; + adapter.log.info('send2Server ' + id + '[' + topic + ']'); + if (!topic) { + adapter.getForeignObject(id, (err, obj) => { + if (!client) return; + if (!obj) { + adapter.log.warn('Cannot resolve topic name for ID: ' + id + ' (object not found)'); + if (cb) cb(id); + return; + } else if (!obj.native || !obj.native.topic) { + id2topic[obj._id] = convertID2topic(obj._id, null, adapter.config.prefix, adapter.namespace); + } else { + id2topic[obj._id] = obj.native.topic; + } + send2Server(obj._id, state, cb); + }); + return; + } + if (!state) { + if (adapter.config.debug) adapter.log.debug('Send to server "' + topic + '": deleted'); + client.publish(topic, null, {qos: adapter.config.defaultQoS, retain: adapter.config.retain}); + } else { + const s = state2string(state.val); + if (adapter.config.debug) adapter.log.debug('Send to server "' + adapter.config.prefix + topic + '": ' + s); + + if (adapter.config.extraSet && state && !state.ack) { + client.publish(topic + '/set', s, {qos: adapter.config.defaultQoS, retain: adapter.config.retain}); + } else { + //if (s > 0) client.publish(topic, s, {qos: adapter.config.defaultQoS, retain: adapter.config.retain}); + client.publish(topic, s, {qos: adapter.config.defaultQoS, retain: adapter.config.retain}); + } + } + if (cb) cb(id); + } + + function publishAllStates(config, toPublish) { + if (!toPublish || !toPublish.length) { + adapter.log.info('All states published'); + return; + } + + if (!client) return; + + const id = toPublish[0]; + if (!id2topic[id]) { + adapter.getForeignObject(id, (err, obj) => { + if (!client) return; + if (!obj) { + adapter.log.warn('Cannot resolve topic name for ID: "' + id + '" (object not found)'); + return; + } else if (!obj.native || !obj.native.topic) { + id2topic[obj._id] = convertID2topic(obj._id, null, config.prefix, adapter.namespace); + } else { + id2topic[obj._id] = obj.native.topic; + } + setImmediate(() => publishAllStates(config, toPublish)); + }); + return; + } + toPublish.shift(); + + if (adapter.config.extraSet && states[id] && !states[id].ack) { + client.publish(id2topic[id] + '/set', state2string(states[id].val), {qos: adapter.config.defaultQoS, retain: adapter.config.retain}, err => { + if (err) adapter.log.error('client.publish2: ' + err); + setImmediate(() => publishAllStates(config, toPublish)); + }); + } else { + if (states[id]) { + client.publish(id2topic[id], state2string(states[id].val), {qos: adapter.config.defaultQoS, retain: adapter.config.retain}, err => { + if (err) adapter.log.error('client.publish: ' + err); + setImmediate(() => publishAllStates(config, toPublish)); + }); + } else { + setImmediate(() => publishAllStates(config, toPublish)); + } + } + } + + (function _constructor(config) { + const clientId = config.clientId || ((tools.getHostname ? tools.getHostname() : utils.appName) + '.' + adapter.namespace); + const _url = ((!config.ssl) ? 'mqtt' : 'mqtts') + '://' + (config.user ? (config.user + ':' + config.pass + '@') : '') + config.url + (config.port ? (':' + config.port) : '') + '?clientId=' + clientId; + const __url = ((!config.ssl) ? 'mqtt' : 'mqtts') + '://' + (config.user ? (config.user + ':*******************@') : '') + config.url + (config.port ? (':' + config.port) : '') + '?clientId=' + clientId; + adapter.log.info('Try to connect to ' + __url); + client = mqtt.connect(_url, { + keepalive: config.keepalive || 10, /* in seconds */ + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: config.reconnectPeriod || 1000, /* in milliseconds */ + connectTimeout: (config.connectTimeout || 30) * 1000, /* in milliseconds */ + clean: config.clean === undefined ? true : config.clean + }); + + // By default subscribe on all topics + if (!config.patterns) config.patterns = '#'; + + if (typeof config.patterns === 'string') { + config.patterns = config.patterns.split(','); + } + + // create connected object and state + adapter.getObject('info.connection', (err, obj) => { + if (!obj || !obj.common || obj.common.type !== 'boolean') { + obj = { + _id: 'info.connection', + type: 'state', + common: { + role: 'indicator.connected', + name: 'If connected to MQTT broker', + type: 'boolean', + read: true, + write: false, + def: false + }, + native: {} + }; + + adapter.setObject('info.connection', obj, () => adapter.setState('info.connection', connected, true)); + } + }); + + // topic from MQTT broker received + client.on('message', (topic, message) => { + if (!topic) return; + + let isAck = true; + + if (adapter.config.extraSet) { + if (topic.match(/\/set$/)) { + isAck = false; + topic = topic.substring(0, topic.length - 4); + } + } + + // try to convert topic to ID + let id = (topic2id[topic] && topic2id[topic].id) || convertTopic2id(topic, false, config.prefix, adapter.namespace); + + if (id.length > config.maxTopicLength) { + adapter.log.warn('[' + client.id + '] Topic name is too long: ' + id.substring(0, 100) + '...'); + return; + } + + if (typeof message === 'object') { + message = message.toString(); + } + + if (typeof message === 'string') { + // Try to convert value + let _val = message.replace(',', '.').replace(/^\+/, ''); + + // +23.560 => 23.56, -23.000 => -23 + if (_val.indexOf('.') !== -1) { + let i = _val.length - 1; + while (_val[i] === '0' || _val[i] === '.') { + i--; + if (_val[i + 1] === '.') break; + } + if (_val[i + 1] === '0' || _val[i + 1] === '.') { + _val = _val.substring(0, i + 1); + } + } + const f = parseFloat(_val); + + if (f.toString() === _val) message = f; + if (message === 'true') message = true; + if (message === 'false') message = false; + } + + if (config.debug) { + adapter.log.debug('Server publishes "' + topic + '": ' + message); + } + + if (typeof message === 'string' && message[0] === '{') { + try { + const _message = JSON.parse(message); + if (_message.val !== undefined && _message.ack !== undefined) { + message = _message; + } + } catch (e) { + adapter.log.warn('Cannot parse "' + topic + '": ' + message); + } + } + + // if no cache for this topic found + if (!topic2id[topic]) { + topic2id[topic] = {id: null, isAck: isAck, message: message}; + + // Create object if not exists + adapter.getObject(id, (err, obj) => { + if (!obj) { + adapter.getForeignObject(id, (err, obj) => { + if (!obj) { + // create state + obj = { + common: { + name: topic, + write: true, + read: true, + role: 'variable', + desc: 'mqtt client variable', + type: typeof topic2id[topic].message + }, + native: { + topic: topic + }, + type: 'state' + }; + + if (obj.common.type === 'object' && topic2id[topic].message.val !== undefined) { + obj.common.type = typeof topic2id[topic].message.val; + } + id = adapter.namespace + '.' + id; + topic2id[topic].id = id; + id2topic[id] = topic; + + adapter.log.debug('Create object for topic: ' + topic + '[ID: ' + topic2id[topic].id + ']'); + + adapter.setForeignObject(topic2id[topic].id, obj, err => err && adapter.log.error('setForeignObject: ' + err)); + + if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message); + + // write + if (typeof topic2id[topic].message === 'object') { + adapter.setForeignState(topic2id[topic].id, topic2id[topic].message); + } else { + adapter.setForeignState(topic2id[topic].id, {val: topic2id[topic].message, ack: topic2id[topic].isAck}); + } + } else { + // expand old version of objects + if (namespaceRegEx.test(obj._id) && (!obj.native || !obj.native.topic)) { + obj.native = obj.native || {}; + obj.native.topic = topic; + adapter.setForeignObject(obj._id, obj); + } + // this is topic from other adapter + topic2id[topic].id = id; + id2topic[topic2id[topic].id] = topic; + + if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message); + + if (typeof topic2id[topic].message === 'object') { + adapter.setForeignState(topic2id[topic].id, topic2id[topic].message); + } else { + adapter.setForeignState(topic2id[topic].id, {val: topic2id[topic].message, ack: topic2id[topic].isAck}); + } + } + }); + } else { + // expand old version of objects + if (namespaceRegEx.test(obj._id) && (!obj.native || !obj.native.topic)) { + obj.native = obj.native || {}; + obj.native.topic = topic; + adapter.setForeignObject(obj._id, obj, err => err && adapter.log.error('setForeignObject2: ' + err)); + } + + // this is topic from this adapter + topic2id[topic].id = obj._id; + id2topic[obj._id] = topic; + + if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message); + + if (typeof topic2id[topic].message === 'object') { + adapter.setForeignState(topic2id[topic].id, topic2id[topic].message); + } else { + adapter.setForeignState(topic2id[topic].id, {val: topic2id[topic].message, ack: topic2id[topic].isAck}); + } + } + }); + } else if (topic2id[topic].id === null) { + if (config.debug) adapter.log.debug('Client received (but in process) "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message); + topic2id[topic].message = message; + } else { + if (!config.onchange) { + if (topic2id[topic].message !== undefined) delete topic2id[topic].message; + if (topic2id[topic].isAck !== undefined) delete topic2id[topic].isAck; + } + if (typeof message === 'object') { + if (!config.onchange || JSON.stringify(topic2id[topic].message) !== JSON.stringify(message)) { + if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof message + '): ' + message); + adapter.setForeignState(topic2id[topic].id, message); + } else { + if (config.debug) adapter.log.debug('Client received (but ignored) "' + topic + '" (' + typeof message + '): ' + message); + } + } else { + if (!config.onchange || topic2id[topic].message !== message || topic2id[topic].isAck !== isAck) { + if (config.onchange) { + topic2id[topic].message = message; + topic2id[topic].isAck = isAck; + } + if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof message + '): ' + message); + adapter.setForeignState(topic2id[topic].id, {val: message, ack: isAck}); + } else { + if (config.debug) adapter.log.debug('Client received (but ignored) "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message); + } + } + } + }); + + client.on('connect', () => { + adapter.log.info('Connected to ' + config.url); + connected = true; + adapter.setState('info.connection', connected, true); + + for (let i = 0; i < config.patterns.length; i++) { + config.patterns[i] = config.patterns[i].trim(); + adapter.log.info('Subscribe on: "' + config.patterns[i] + '"'); + client.subscribe(config.patterns[i]); + } + + if (config.publishAllOnStart) { + const toPublish = []; + for (const id in states) { + if (states.hasOwnProperty(id) && !messageboxRegex.test(id)) { + toPublish.push(id); + } + } + publishAllStates(config, toPublish); + } + }); + + client.on('error', err => { + adapter.log.error('Client error:' + err); + + if (connected) { + adapter.log.info('Disconnected from ' + config.url); + connected = false; + adapter.setState('info.connection', connected, true); + } + }); + + client.on('close', err => { + if (connected) { + adapter.log.info('Disconnected from ' + config.url + ': ' + err); + connected = false; + adapter.setState('info.connection', connected, true); + } + }); + })(adapter.config); + + process.on('uncaughtException', err => adapter.log.error('uncaughtException: ' + err)); + + return this; +} + +module.exports = MQTTClient; diff --git a/lib/common.js b/lib/common.js new file mode 100644 index 0000000..44ee76b --- /dev/null +++ b/lib/common.js @@ -0,0 +1,57 @@ +'use strict'; + +function convertID2topic(id, pattern, prefix, namespace) { + let topic; + id = (id || '').toString(); + if (pattern && pattern.substring(0, (prefix + namespace).length) === (prefix + namespace)) { + topic = prefix + id; + } else if (pattern && pattern.substring(0, namespace.length) === namespace) { + topic = id; + } else if (prefix && pattern && pattern.substring(0, prefix.length) === prefix) { + topic = prefix + id;//.substring(namespace.length + 1); + } else if (id.substring(0, namespace.length) === namespace) { + topic = (prefix || '') + id.substring(namespace.length + 1); + } else { + topic = (prefix || '') + id; + } + topic = topic.replace(/\./g, '/'); + return topic; +} + +function state2string(val) { + if (val && typeof val === 'object') { + if (val.ack === undefined && val.val !== undefined) { + if (val.val === null) return 'null'; + return val.val.toString(); + } else { + return JSON.stringify(val); + } + } else { + return (val === null) ? 'null' : (val === undefined ? 'undefined' : val.toString()); + } +} + +function convertTopic2id(topic, dontCutNamespace, prefix, namespace) { + if (!topic) return topic; + + // Remove own prefix if + if (prefix && topic.substring(0, prefix.length) === prefix) { + topic = topic.substring(prefix.length); + } + + topic = topic.replace(/\//g, '.').replace(/\s/g, '_'); + if (topic[0] === '.') topic = topic.substring(1); + if (topic[topic.length - 1] === '.') topic = topic.substring(0, topic.length - 1); + + + + if (!dontCutNamespace && topic.substring(0, namespace.length) === namespace) { + topic = topic.substring(namespace.length + 1); + } + + return topic; +} + +exports.convertTopic2id = convertTopic2id; +exports.convertID2topic = convertID2topic; +exports.state2string = state2string; diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..04e098c --- /dev/null +++ b/lib/server.js @@ -0,0 +1,1143 @@ +'use strict'; + +const mqtt = require('mqtt-connection'); +const state2string = require(__dirname + '/common').state2string; +const convertTopic2id = require(__dirname + '/common').convertTopic2id; +const convertID2topic = require(__dirname + '/common').convertID2topic; +const messageboxRegex = new RegExp('\\.messagebox$'); + +// todo delete from persistentSessions the sessions and messages after some time + +function MQTTServer(adapter, states) { + if (!(this instanceof MQTTServer)) return new MQTTServer(adapter, states); + + const namespaceRegEx = new RegExp('^' + adapter.namespace.replace('.', '\\.') + '\\.'); + + let net; + let http; + let ws; + let wsStream; + let server = null; + let serverWs = null; + let serverForWs = null; + let clients = {}; + let topic2id = {}; + let id2topic = {}; + let messageId = 1; + let persistentSessions = {}; + let resending = false; + let resendTimer = null; + + adapter.config.sendOnStartInterval = parseInt(adapter.config.sendOnStartInterval, 10) || 2000; + adapter.config.sendInterval = parseInt(adapter.config.sendInterval, 10) || 0; + + this.destroy = (cb) => { + if (resendTimer) { + clearInterval(resendTimer); + resendTimer = null; + } + persistentSessions = {}; + let tasks = 0; + let timeout; + if (cb) { + timeout = setTimeout(() => { + timeout = null; + if (cb) { + cb(); + cb = null; + } + }, 2000); + } + if (server) { + tasks++; + // to release all resources + server.close(() => { + console.log('all gone!'); + if (!--tasks && cb) { + clearTimeout(timeout); + cb(); + cb = null; + } + }); + server = null; + } + + if (serverForWs) { + tasks++; + // to release all resources + serverForWs.close(() => { + console.log('all ws gone!'); + if (!--tasks && cb) { + clearTimeout(timeout); + cb(); + cb = null; + } + }); + serverForWs = null; + } + if (!tasks && cb) { + clearTimeout(timeout); + cb(); + cb = null; + } + }; + + this.onStateChange = (id, state) => { + adapter.log.debug('onStateChange ' + id + ': ' + JSON.stringify(state)); + if (server) { + setImmediate(() => { + for (let k in clients) { + if (clients.hasOwnProperty(k)) { + sendState2Client(clients[k], id, state, adapter.config.defaultQoS, true); + } + } + for (let clientId in persistentSessions) { + if (persistentSessions.hasOwnProperty(clientId) && !clients[clientId]) { + (function (_clientId) { + getMqttMessage(persistentSessions[_clientId], id, state, adapter.config.defaultQoS, true, (err, message) => { + message && persistentSessions[_clientId].messages.push(message); + }); + })(clientId); + } + } + }); + } + }; + + function updateClients() { + let text = ''; + if (clients) { + for (let id in clients) { + if (clients.hasOwnProperty(id)) { + text += (text ? ',' : '') + id; + } + } + } + + adapter.setState('info.connection', {val: text, ack: true}); + } + + function getMqttMessage(client, id, state, qos, retain, cb) { + if (typeof qos === 'function') { + cb = qos; + qos = undefined; + } + if (typeof retain === 'function') { + cb = retain; + retain = undefined; + } + + if (!id2topic[id]) { + return adapter.getForeignObject(id, (err, obj) => { + if (err) { + return cb(`Client [${client.id}] Cannot resolve topic name for ID: ${id} (err: ${err})`); + } + if (!obj) { + return cb(`Client [${client.id}] Cannot resolve topic name for ID: ${id} (object not found)`); + } else if (!obj._id) { + return cb(`Client [${client.id}] Cannot resolve topic name for ID: ${id} (object has no id): ${JSON.stringify(obj)}`); + } else if (!obj.native || !obj.native.topic) { + id2topic[obj._id] = convertID2topic(obj._id, null, adapter.config.prefix, adapter.namespace); + } else { + id2topic[obj._id] = obj.native.topic; + } + getMqttMessage(client, obj._id, state, qos, retain, cb); + }); + } + + // client has subscription for this ID + let message; + let topic; + let pattern; + if (client._subsID && client._subsID[id]) { + topic = id2topic[id]; + if (adapter.config.extraSet && state && !state.ack) { + message = { + topic: topic + '/set', + payload: (state ? state2string(state.val) : null), + qos: client._subsID[id].qos + }; + } else { + message = { + topic: topic, + payload: (state ? state2string(state.val) : null), + qos: client._subsID[id].qos + }; + } + } else + // Check patterns + if (client._subs && (pattern = checkPattern(client._subs, id)) !== null) { + topic = id2topic[id]; + // Cache the value + client._subsID[id] = pattern; + + if (adapter.config.extraSet && state && !state.ack) { + message = { + topic: topic + '/set', + payload: (state ? state2string(state.val) : null), + qos: pattern.qos + }; + } else { + message = { + topic: topic, + payload: (state ? state2string(state.val) : null), + qos: pattern.qos + }; + } + } + if (message) { + message.qos = message.qos === undefined ? qos : message.qos; + message.retain = retain; + message.messageId = messageId; + message.ts = Date.now(); + message.count = 0; + message.cmd = 'publish'; + + messageId++; + messageId &= 0xFFFFFFFF; + } + cb(null, message, client); + } + + function sendState2Client(client, id, state, qos, retain, cb) { + if (messageboxRegex.test(id)) return; + getMqttMessage(client, id, state, qos, retain, (err, message, client) => { + if (message) { + if (adapter.config.debug) { + adapter.log.debug(`Client [${client.id}] send to this client "${message.topic}": ${(message.payload !== null ? message.payload : 'deleted')}`); + } + client.publish(message); + if (message.qos > 0) { + client._messages.push(message); + } + } + cb && cb(id); + }); + } + + function sendStates2Client(client, list) { + if (list && list.length) { + const id = list.shift(); + sendState2Client(client, id, states[id], 0, true, () => { + setTimeout(() => sendStates2Client(client, list), adapter.config.sendInterval); + }); + } else { + //return; + } + } + + function resendMessages2Client(client, messages, i) { + i = i || 0; + if (messages && i < messages.length) { + try { + messages[i].ts = Date.now(); + messages[i].count++; + adapter.log.debug(`Client [${client.id}] Resend messages on connect: ${messages[i].topic} = ${messages[i].payload}`); + if (messages[i].cmd === 'publish') { + client.publish(messages[i]); + } + } catch (e) { + adapter.log.warn(`Client [${client.id}] Cannot resend message: ${e}`); + } + + if (adapter.config.sendInterval) { + setTimeout(() => resendMessages2Client(client, messages, i + 1), adapter.config.sendInterval); + } else { + setImmediate(() => resendMessages2Client(client, messages, i + 1)); + } + } else { + //return; + } + } + + /* + 4.7.1.2 Multi-level wildcard + + The number sign (‘#’ U+0023) is a wildcard character that matches any number of levels within a topic. The multi-level wildcard represents the parent and any number of child levels. The multi-level wildcard character MUST be specified either on its own or following a topic level separator. In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. + + Non normative comment + For example, if a Client subscribes to “sport/tennis/player1/#”, it would receive messages published using these topic names: + · “sport/tennis/player1” + · “sport/tennis/player1/ranking” + · “sport/tennis/player1/score/wimbledon” + + Non normative comment + · “sport/#” also matches the singular “sport”, since # includes the parent level. + · “#” is valid and will receive every Application Message + · “sport/tennis/#” is valid + · “sport/tennis#” is not valid + · “sport/tennis/#/ranking” is not valid + + */ + function checkPattern(patterns, id) { + for (let pattern in patterns) { + if (patterns.hasOwnProperty(pattern) && patterns[pattern].regex.test(id)) { + return patterns[pattern]; + } + } + + return null; + } + + function processTopic(id, topic, message, qos, retain, isAck, obj, ignoreClient, cb) { + // expand old version of objects + if (obj && namespaceRegEx.test(id) && (!obj.native || !obj.native.topic)) { + obj.native = obj.native || {}; + obj.native.topic = topic; + adapter.setForeignObject(id, obj); + } + // this is topic from other adapter + topic2id[topic].id = id; + id2topic[id] = topic; + + if (adapter.config.debug) adapter.log.debug('Server received "' + topic + '" (' + typeof message + '): ' + message); + + if (message !== undefined) { + if (typeof message === 'object') { + adapter.setForeignState(id, message, (err, id) => states[id] = message); + } else { + adapter.setForeignState(id, {val: message, ack: isAck}, (err, id) => states[id] = {val: message, ack: isAck}); + } + } else { + states[id] = {val: null, ack: isAck}; + } + + // send message to all other clients + if (adapter.config.onchange && server && message !== undefined) { + setImmediate(() => { + if (typeof message !== 'object') { + message = {val: message}; + } + for (let k in clients) { + // if get and set have different topic names, send state to issuing client too. + if (!clients.hasOwnProperty(k) || (clients[k] === ignoreClient && !adapter.config.extraSet)) continue; + sendState2Client(clients[k], id, message, qos, retain, cb); + } + }); + } + // ELSE + // this will be done indirect. Message will be sent to js-controller and if adapter is subscribed, it gets this message over stateChange + + if (cb) cb(); + } + + function checkObject(id, topic, callback) { + topic2id[topic] = topic2id[topic] || {id: null}; + + adapter.getObject(id, (err, obj) => { + if (!obj) { + adapter.getForeignObject(id, (err, obj) => { + if (!obj) { + id = adapter.namespace + '.' + id; + // create state + obj = { + common: { + name: topic, + write: true, + read: true, + role: 'variable', + desc: 'mqtt server variable', + type: topic2id[topic].message !== undefined ? typeof topic2id[topic].message : 'string' + }, + native: { + topic: topic + }, + type: 'state' + }; + if (obj.common.type === 'object' && topic2id[topic].message !== undefined && topic2id[topic].message.val !== undefined) { + obj.common.type = typeof topic2id[topic].message.val; + } + + adapter.log.debug('Create object for topic: ' + topic + '[ID: ' + id + ']'); + adapter.setForeignObject(id, obj, err => { + topic2id[topic].id = id; + err && adapter.log.error('setForeignObject: ' + err); + obj._id = id; + callback && callback(err, id, obj); + }); + } else { + topic2id[topic].id = obj._id; + callback && callback(null, obj._id, obj); + } + }); + } else { + topic2id[topic].id = obj._id; + callback && callback(null, obj._id, obj); + } + }); + } + + /*4.7.1.3 Single level wildcard + + The plus sign (‘+’ U+002B) is a wildcard character that matches only one topic level. + + The single-level wildcard can be used at any level in the Topic Filter, including first and last levels. Where it is used it MUST occupy an entire level of the filter [MQTT-4.7.1-3]. It can be used at more than one level in the Topic Filter and can be used in conjunction with the multilevel wildcard. + + Non normative comment + For example, “sport/tennis/+” matches “sport/tennis/player1” and “sport/tennis/player2”, but not “sport/tennis/player1/ranking”. Also, because the single-level wildcard matches only a single level, “sport/+” does not match “sport” but it does match “sport/”. + + Non normative comment + · “+” is valid + · “+/tennis/#” is valid + · “sport+” is not valid + · “sport/+/player1” is valid + · “/finance” matches “+/+” and “/+”, but not “+” + */ + function pattern2RegEx(pattern) { + pattern = convertTopic2id(pattern, true, adapter.config.prefix, adapter.namespace); + pattern = pattern.replace(/#/g, '*'); + pattern = pattern.replace(/\$/g, '\\$'); + pattern = pattern.replace(/\^/g, '\\^'); + + if (pattern !== '*') { + if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; + if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; + if (pattern[0] === '+') pattern = '^[^.]*' + pattern.substring(1); + if (pattern[pattern.length - 1] === '+') pattern = pattern.substring(0, pattern.length - 1) + '[^.]*$'; + } + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + pattern = pattern.replace(/\+/g, '[^.]*'); + return pattern; + } + + function receivedTopic(packet, client, cb) { + let isAck = true; + let topic = packet.topic; + let message = packet.payload; + const qos = packet.qos; + const retain = packet.retain; + const now = Date.now(); + let id; + + if (adapter.config.extraSet) { + if (packet.topic.match(/\/set$/)) { + isAck = false; + packet.topic = packet.topic.substring(0, packet.topic.length - 4); + topic = packet.topic; + } + } + + if (topic2id[topic]) { + id = topic2id[topic].id || convertTopic2id(topic, false, adapter.config.prefix, adapter.namespace); + } else { + id = convertTopic2id(topic, false, adapter.config.prefix, adapter.namespace); + } + + if (!id) { + adapter.log.error(`Client [${client.id}] Invalid topic name: ${JSON.stringify(topic)}`); + if (cb) { + cb(); + cb = null; + } + return; + } + + //adapter.log.info('Type: ' + typeof message); + let type = typeof message; + + if (type !== 'string' && type !== 'number' && type !== 'boolean') { + message = message ? message.toString('utf8') : 'null'; + type = 'string'; + } + + // try to convert 101,124,444,... To utf8 string + if (type === 'string' && message.match(/^(\d)+,\s?(\d)+,\s?(\d)+/)) { + //adapter.log.info('Try to convert ' + message); + + let parts = message.split(','); + try { + let str = ''; + for (let p = 0; p < parts.length; p++) { + str += String.fromCharCode(parseInt(parts[p].trim(), 10)); + } + message = str; + } catch (e) { + // cannot convert and ignore it + } + //adapter.log.info('Converted ' + message); + } + + // If state is unknown => create mqtt.X.topic + if ((adapter.namespace + '.' + id).length > adapter.config.maxTopicLength) { + adapter.log.warn(`Client [${client.id}] Topic name is too long: ${id.substring(0, 100)}...`); + if (cb) { + cb(); + cb = null; + } + return; + } + + if (type === 'string') { + // Try to convert value + let _val = message.replace(',', '.').replace(/^\+/, ''); + + // +23.560 => 23.56, -23.000 => -23 + if (_val.indexOf('.') !== -1) { + let i = _val.length - 1; + while (_val[i] === '0' || _val[i] === '.') { + i--; + if (_val[i + 1] === '.') break; + } + if (_val[i + 1] === '0' || _val[i + 1] === '.') { + _val = _val.substring(0, i + 1); + } + } + const f = parseFloat(_val); + + if (f.toString() === _val) message = f; + if (message === 'true') message = true; + if (message === 'false') message = false; + } + + if (type === 'string' && message[0] === '{') { + try { + const _message = JSON.parse(message); + // Fast solution + if (_message.val !== undefined) { + message = _message; + // Really right, but slow + //var valid = true; + //for (var attr in _message) { + // if (!_message.hasOwnProperty(attr)) continue; + // if (attr !== 'val' && attr !== 'ack' && attr !== 'ts' && attr !== 'q' && + // attr !== 'lc' && attr !== 'comm' && attr !== 'lc') { + // valid = false; + // break; + // } + //} + //if (valid) message = _message; + } + } catch (e) { + adapter.log.error(`Client [${client.id}] Cannot parse ${message}`); + } + } + + if (!topic2id[topic]) { + checkObject(id, topic, (err, id, obj) => { + processTopic(id, topic, message, qos, retain, isAck, obj, client); + if (cb) { + cb(); + cb = null; + } + }); + } else if (topic2id[topic].id === null) { + topic2id[topic].message = message; + // still looking for id + if (adapter.config.debug) { + adapter.log.debug(`Client [${client.id}] Server received (but in process) "${topic}" (${typeof message}): ${message}`); + } + if (cb) { + cb(); + cb = null; + } + } else { + if (topic2id[topic].message !== undefined) { + delete topic2id[topic].message; + } + + if (qos) { + for (const clientId in persistentSessions) { + if (persistentSessions.hasOwnProperty(clientId) && clientId !== client.id && !persistentSessions[clientId].connected) { + // try to collect this message if client subscribed + persistentSessions[clientId].messages.push({topic, qos, retain, messageId: packet.messageId, ts: now, payload: message, count: 0, cmd: 'publish'}); + } + } + } + + processTopic(topic2id[topic].id, topic, message, qos, retain, isAck, null, client, cb); + } + } + + function clientClose(client, reason) { + if (!client) return; + + if (persistentSessions[client.id]) { + persistentSessions[client.id].connected = false; + } + + if (client._sendOnStart) { + clearTimeout(client._sendOnStart); + client._sendOnStart = null; + } + if (client._resendonStart) { + clearTimeout(client._resendonStart); + client._resendonStart = null; + } + + try { + if (clients[client.id] && (client.__secret === clients[client.id].__secret)) { + adapter.log.info(`Client [${client.id}] connection closed: ${reason}`); + delete clients[client.id]; + updateClients(); + if (client._will) { + receivedTopic(client._will, client, () => client.destroy()); + } else { + client.destroy(); + } + } else { + client.destroy(); + } + } catch (e) { + adapter.log.warn(`Client [${client.id}] Cannot close client: ${e}`); + } + } + + function startServer(config, socket, server, port, bind, ssl, ws) { + socket.on('connection', stream => { + let client; + if (ws) { + client = mqtt(wsStream(stream)); + } else { + client = mqtt(stream); + } + + // Store unique connection identifier + client.__secret = Date.now() + '_' + Math.round(Math.random() * 10000); + + client.on('connect', options => { + // set client id + client.id = options.clientId; + client.cleanSession = options.cleanSession; + + // get possible old client + let oldClient = clients[client.id]; + + if (config.user) { + if (config.user !== options.username || + config.pass !== (options.password || '').toString()) { + adapter.log.warn(`Client [${client.id}] has invalid password(${options.password}) or username(${options.username})`); + client.connack({returnCode: 4}); + if (oldClient) { + // delete existing client + delete clients[client.id]; + updateClients(); + oldClient.destroy(); + } + client.destroy(); + return; + } + } + + if (oldClient) { + adapter.log.info(`Client [${client.id}] reconnected. Old secret ${clients[client.id].__secret}. New secret ${client.__secret}`); + // need to destroy the old client + + if (client.__secret !== clients[client.id].__secret) { + // it is another socket!! + + // It was following situation: + // - old connection was active + // - new connection is on the same TCP + // Just forget him + // oldClient.destroy(); + } + } else { + adapter.log.info(`Client [${client.id}] connected with secret ${client.__secret}`); + } + + let sessionPresent = false; + + if (!client.cleanSession && adapter.config.storeClientsTime !== 0) { + if (persistentSessions[client.id]) { + sessionPresent = true; + persistentSessions[client.id].lastSeen = Date.now(); + } else { + persistentSessions[client.id] = { + _subsID: {}, + _subs: {}, + messages: [], + lastSeen: Date.now() + }; + } + client._messages = persistentSessions[client.id].messages; + persistentSessions[client.id].connected = true; + } else if (client.cleanSession && persistentSessions[client.id]) { + delete persistentSessions[client.id]; + } + client._messages = client._messages || []; + + client.connack({returnCode: 0, sessionPresent}); + clients[client.id] = client; + updateClients(); + + if (options.will) { // the client's will message options. object that supports the following properties: + // topic: the will topic. string + // payload: the will payload. string + // qos: will qos level. number + // retain: will retain flag. boolean + client._will = JSON.parse(JSON.stringify(options.will)); + let id; + if (topic2id[client._will.topic]) { + id = topic2id[client._will.topic].id || convertTopic2id(client._will.topic, false, config.prefix, adapter.namespace); + } else { + id = convertTopic2id(client._will.topic, false, config.prefix, adapter.namespace); + } + checkObject(id, client._will.topic); + } + + // Send all subscribed variables to client + if (config.publishAllOnStart) { + // Give to client 2 seconds to send subscribe + client._sendOnStart = setTimeout(() => { + client._sendOnStart = null; + let list = []; + // If client still connected + for (let id in states) { + if (states.hasOwnProperty(id)) { + list.push(id); + } + } + sendStates2Client(client, list); + }, adapter.config.sendOnStartInterval); + } + + if (persistentSessions[client.id]) { + client._subsID = persistentSessions[client.id]._subsID; + client._subs = persistentSessions[client.id]._subs; + if (persistentSessions[client.id].messages.length) { + // give to the client a little bit time + client._resendonStart = setTimeout(clientId => { + client._resendonStart = null; + resendMessages2Client(client, persistentSessions[clientId].messages); + }, 100, client.id); + } + } + }); + + client.on('publish', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends publish. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + if (packet.qos === 1) { + // send PUBACK to client + client.puback({ + messageId: packet.messageId + }); + } else if (packet.qos === 2) { + const pack = client._messages.find(e => { + return e.messageId === packet.messageId; + }); + if (pack) { + // duplicate message => ignore + adapter.log.warn(`Client [${client.id}] Ignored duplicate message with ID: ${packet.messageId}`); + return; + } else { + packet.ts = Date.now(); + packet.cmd = 'pubrel'; + packet.count = 0; + client._messages.push(packet); + + client.pubrec({ + messageId: packet.messageId + }); + return; + } + } + + receivedTopic(packet, client); + }); + + // response for QoS2 + client.on('pubrec', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends pubrec. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== -1) { + client.pubrel({ + messageId: packet.messageId + }); + } else { + adapter.log.warn(`Client [${client.id}] Received pubrec on ${client.id} for unknown messageId ${packet.messageId}`); + } + }); + + // response for QoS2 + client.on('pubcomp', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends pubcomp. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== null) { + client._messages.splice(pos, 1); + } else { + adapter.log.warn(`Client [${client.id}] Received pubcomp for unknown message ID: ${packet.messageId}`); + } + }); + + // response for QoS2 + client.on('pubrel', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends pubrel. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== -1) { + client.pubcomp({ + messageId: packet.messageId + }); + receivedTopic(client._messages[pos], client); + } else { + adapter.log.warn(`Client [${client.id}] Received pubrel on ${client.id} for unknown messageId ${packet.messageId}`); + } + }); + + // response for QoS1 + client.on('puback', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends puback. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + // remove this message from queue + let pos = null; + // remove this message from queue + client._messages.forEach((e, i) => { + if (e.messageId === packet.messageId) { + pos = i; + return false; + } + }); + if (pos !== null) { + adapter.log.debug(`Client [${client.id}] Received puback for ${client.id} message ID: ${packet.messageId}`); + client._messages.splice(pos, 1); + } else { + adapter.log.warn(`Client [${client.id}] Received puback for unknown message ID: ${packet.messageId}`); + } + }); + + client.on('subscribe', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends subscribe. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + const granted = []; + if (!client._subsID) client._subsID = {}; + if (!client._subs) client._subs = {}; + + for (let i = 0; i < packet.subscriptions.length; i++) { + granted.push(packet.subscriptions[i].qos); + + const topic = packet.subscriptions[i].topic; + let id; + + if (topic2id[topic]) { + id = topic2id[topic].id || convertTopic2id(topic, false, config.prefix, adapter.namespace); + } else { + id = convertTopic2id(topic, false, config.prefix, adapter.namespace); + } + + if (!id) { + adapter.log.error(`Client [${client.id}] Invalid topic: ${topic}`); + continue; + } + + // if pattern without wildcards + if (id.indexOf('*') === -1 && id.indexOf('#') === -1 && id.indexOf('+') === -1) { + // If state is unknown => create mqtt.X.topic + if (!topic2id[topic]) { + checkObject(id, topic, (err, id) => { + adapter.log.info(`Client [${client.id}] subscribes on topic "${topic}"`); + client._subsID[id] = {id, qos: packet.subscriptions[i].qos}; + }); + } else { + client._subsID[topic2id[topic].id] = {id: topic2id[topic].id, qos: packet.subscriptions[i].qos}; + adapter.log.info(`Client [${client.id}] subscribes on "${topic2id[topic].id}"`); + if (adapter.config.publishOnSubscribe) { + adapter.log.info(`Client [${client.id}] publishOnSubscribe`); + sendState2Client(client, topic2id[topic].id, states[topic2id[topic].id]); + } + } + } else { + let pattern = topic; + // remove prefix + if (pattern.startsWith(adapter.config.prefix)) { + pattern = pattern.substring(adapter.config.prefix.length); + } + pattern = pattern.replace(/\//g, '.'); + if (pattern[0] === '.') pattern = pattern.substring(1); + + // add simple pattern + let regText = pattern2RegEx(pattern); + client._subs[topic] = { + regex: new RegExp(regText), + qos: packet.subscriptions[i].qos, + pattern: pattern + }; + adapter.log.info(`Client [${client.id}] subscribes on "${topic}" with regex /${regText}/`); + + // add simple mqtt.0.pattern + pattern = adapter.namespace + '/' + pattern; + regText = pattern2RegEx(pattern); + client._subs[adapter.namespace + '/' + topic] = { + regex: new RegExp(regText), + qos: packet.subscriptions[i].qos, + pattern: pattern + }; + adapter.log.info(`Client [${client.id}] subscribes on "${topic}" with regex /${regText}/`); + + if (adapter.config.publishOnSubscribe) { + adapter.log.info(`Client [${client.id}] publishOnSubscribe send all known states`); + for (const savedId in states) { + if (states.hasOwnProperty(savedId) && checkPattern(client._subs, savedId)) { + sendState2Client(client, savedId, states[savedId]); + } + } + } + } + } + + client.suback({granted: granted, messageId: packet.messageId}); + }); + + client.on('unsubscribe', packet => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends unsubscribe. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + for (let i = 0; i < packet.unsubscriptions.length; i++) { + const topic = packet.unsubscriptions[i]; + let id; + + if (topic2id[topic]) { + id = topic2id[topic].id || convertTopic2id(topic, false, config.prefix, adapter.namespace); + } else { + id = convertTopic2id(topic, false, config.prefix, adapter.namespace); + } + + if (!id) { + adapter.log.error(`Client [${client.id}] unsubscribes from invalid topic: ${topic}`); + continue; + } + + // if pattern without wildcards + if (id.indexOf('*') === -1 && id.indexOf('#') === -1 && id.indexOf('+') === -1) { + // If state is known + if (topic2id[topic]) { + let _id = topic2id[topic].id; + if (client._subsID[_id]) { + delete client._subsID[_id]; + adapter.log.info(`Client [${client.id}] unsubscribes on "${_id}"`); + } else { + adapter.log.info(`Client [${client.id}] unsubscribes on unknown "${_id}"`); + } + } else { + adapter.log.info(`Client [${client.id}] unsubscribes on unknown "${_id}"`); + } + } else { + let pattern = topic.replace(/\//g, '.'); + if (pattern[0] === '.') pattern = pattern.substring(1); + + // add simple pattern + if (client._subs[topic]) { + adapter.log.info(`Client [${client.id}] unsubscribes on "${topic}"`); + delete client._subs[topic]; + if (client._subs[adapter.namespace + '/' + topic]) {// add simple mqtt.0.pattern + delete client._subs[adapter.namespace + '/' + topic]; + adapter.log.info(`Client [${client.id}] unsubscribes on "${adapter.namespace}/${topic}"`); + } + } else { + adapter.log.warn(`Client [${client.id}] unsubscribes on unknwon "${topic}"`); + } + } + } + client.unsuback({messageId: packet.messageId}); + }); + + client.on('pingreq', (/*packet*/) => { + if (clients[client.id] && client.__secret !== clients[client.id].__secret) { + return adapter.log.debug(`Old client ${client.id} with secret ${client.__secret} sends pingreq. Ignore! Actual secret is ${clients[client.id].__secret}`); + } + + if (persistentSessions[client.id]) { + persistentSessions[client.id].lastSeen = Date.now(); + } + + adapter.log.debug(`Client [${client.id}] pingreq`); + client.pingresp(); + }); + + // connection error handling + client.on('close', had_error => clientClose(client, had_error ? 'closed because of error' : 'closed')); + client.on('error', e => clientClose(client, e)); + client.on('disconnect', () => clientClose(client, 'disconnected')); + + }); + (server || socket).listen(port, bind, () => { + adapter.log.info(`Starting MQTT ${ws ? '-WebSocket' : ''}${ssl ? ' (Secure)' : ''}' ${config.user ? 'authenticated ' : ''} server on port ${port}`); + }); + } + + function checkResends() { + const now = Date.now(); + resending = true; + for (const clientId in clients) { + if (clients.hasOwnProperty(clientId) && clients[clientId] && clients[clientId]._messages) { + for (let m = clients[clientId]._messages.length - 1; m >= 0; m--) { + const message = clients[clientId]._messages[m]; + if (now - message.ts >= adapter.config.retransmitInterval) { + if (message.count > adapter.config.retransmitCount) { + adapter.log.warn(`Client [${clientId}] Message ${message.messageId} deleted after ${message.count} retries`); + clients[clientId]._messages.splice(m, 1); + continue; + } + + // resend this message + message.count++; + message.ts = now; + try { + adapter.log.debug(`Client [${clientId}] Resend message topic: ${message.topic}, payload: ${message.payload}`); + if (message.cmd === 'publish') { + clients[clientId].publish(message); + } + } catch (e) { + adapter.log.warn(`Client [${clientId}] Cannot publish message: ${e}`); + } + + if (adapter.config.sendInterval) { + setTimeout(checkResends, adapter.config.sendInterval); + } else { + setImmediate(checkResends); + } + return; + } + } + } + } + + // delete old sessions + if (adapter.config.storeClientsTime !== -1) { + for (const id in persistentSessions) { + if (persistentSessions.hasOwnProperty(id)) { + if (now - persistentSessions[id].lastSeen > adapter.config.storeClientsTime * 60000) { + delete persistentSessions[id]; + } + } + } + } + + resending = false; + } + + (function _constructor(config) { + // create connected object and state + adapter.getObject('info.connection', (err, obj) => { + if (!obj || !obj.common || obj.common.type !== 'string') { + obj = { + _id: 'info.connection', + type: 'state', + common: { + role: 'info.clients', + name: 'List of connected clients', + type: 'string', + read: true, + write: false, + def: false + }, + native: {} + }; + + adapter.setObject('info.connection', obj, () => updateClients()); + } else { + updateClients(); + } + }); + + config.port = parseInt(config.port, 10) || 1883; + config.retransmitInterval = config.retransmitInterval || 2000; + config.retransmitCount = config.retransmitCount || 10; + if (config.storeClientsTime === undefined) { + config.storeClientsTime = 1440; + } else { + config.storeClientsTime = parseInt(config.storeClientsTime, 10) || 0; + } + + config.defaultQoS = parseInt(config.defaultQoS, 10) || 0; + + if (config.ssl) { + net = net || require('tls'); + if (config.webSocket) { + http = http || require('https'); + } + } else { + net = net || require('net'); + if (config.webSocket) { + http = http || require('http'); + } + } + server = new net.Server(config.certificates); + startServer(config, server, null, config.port, config.bind, config.ssl, false); + if (config.webSocket) { + http = http || require('https'); + ws = ws || require('ws'); + wsStream = wsStream || require('websocket-stream'); + serverForWs = http.createServer(config.certificates); + serverWs = new ws.Server({server: serverForWs}); + + startServer(config, serverWs, serverForWs, config.port + 1, config.bind, config.ssl, true); + } + + resendTimer = setInterval(() => { + if (!resending) { + checkResends(); + } + }, adapter.config.retransmitInterval || 2000); + + })(adapter.config); + + return this; +} + +module.exports = MQTTServer; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..a40fe76 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,83 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +let controllerDir; +let appName; + +/** + * returns application name + * + * The name of the application can be different and this function finds it out. + * + * @returns {string} + */ + function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +/** + * looks for js-controller home folder + * + * @param {boolean} isInstall + * @returns {string} + */ +function getControllerDir(isInstall) { + // Find the js-controller location + const possibilities = [ + 'yunkong2.js-controller', + 'yunkong2.js-controller', + ]; + /** @type {string} */ + let controllerPath; + for (const pkg of possibilities) { + try { + const possiblePath = require.resolve(pkg); + if (fs.existsSync(possiblePath)) { + controllerPath = possiblePath; + break; + } + } catch (e) { /* not found */ } + } + if (!controllerPath) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/main.js b/main.js new file mode 100644 index 0000000..eba1880 --- /dev/null +++ b/main.js @@ -0,0 +1,175 @@ +/** + * + * yunkong2 mqtt Adapter + * + * (c) 2014-2018 bluefox + * + * MIT License + * + */ +'use strict'; + +const utils = require(__dirname + '/lib/utils'); // Get common adapter utils +const adapter = new utils.Adapter('mqtt'); + +let server = null; +let client = null; +let states = {}; + +const messageboxRegex = new RegExp('\.messagebox$'); + +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; +} + +adapter.on('message', function (obj) { + if (obj) processMessage(obj); + processMessages(); +}); + +adapter.on('ready', function () { + adapter.config.pass = decrypt('Zgfr56gFe87jJOM', adapter.config.pass); + adapter.config.maxTopicLength = adapter.config.maxTopicLength || 100; + if (adapter.config.ssl && adapter.config.type === 'server') { + // Load certificates + adapter.getCertificates(function (err, certificates) { + adapter.config.certificates = certificates; + main(); + }); + } else { + // Start + main(); + } +}); + +adapter.on('unload', function () { + if (client) client.destroy(); + if (server) server.destroy(); +}); + +// is called if a subscribed state changes +adapter.on('stateChange', (id, state) => { + adapter.log.debug('stateChange ' + id + ': ' + JSON.stringify(state)); + // State deleted + if (!state) { + delete states[id]; + // If SERVER + if (server) server.onStateChange(id); + // if CLIENT + if (client) client.onStateChange(id); + } else + // you can use the ack flag to detect if state is desired or acknowledged + if ((adapter.config.sendAckToo || !state.ack) && !messageboxRegex.test(id)) { + const oldVal = states[id] ? states[id].val : null; + const oldAck = states[id] ? states[id].ack : null; + states[id] = state; + + // If value really changed + if (!adapter.config.onchange || oldVal !== state.val || oldAck !== state.ack) { + // If SERVER + if (server) server.onStateChange(id, state); + // if CLIENT + if (client) client.onStateChange(id, state); + } + } +}); + +function processMessage(obj) { + if (!obj || !obj.command) return; + switch (obj.command) { + case 'test': { + // Try to connect to mqtt broker + if (obj.callback && obj.message) { + const mqtt = require('mqtt'); + const _url = 'mqtt://' + (obj.message.user ? (obj.message.user + ':' + obj.message.pass + '@') : '') + obj.message.url + (obj.message.port ? (':' + obj.message.port) : '') + '?clientId=yunkong2.' + adapter.namespace; + const _client = mqtt.connect(_url); + // Set timeout for connection + const timeout = setTimeout(() => { + _client.end(); + adapter.sendTo(obj.from, obj.command, 'timeout', obj.callback); + }, 2000); + + // If connected, return success + _client.on('connect', () => { + _client.end(); + clearTimeout(timeout); + adapter.sendTo(obj.from, obj.command, 'connected', obj.callback); + }); + } + } + } +} + +function processMessages() { + adapter.getMessage((err, obj) => { + if (obj) { + processMessage(obj.command, obj.message); + processMessages(); + } + }); +} + +let cnt = 0; +function readStatesForPattern(pattern) { + adapter.getForeignStates(pattern, (err, res) => { + if (!err && res) { + if (!states) states = {}; + + for (const id in res) { + if (res.hasOwnProperty(id) && !messageboxRegex.test(id)) { + states[id] = res[id]; + } + } + } + // If all patters answered, start client or server + if (!--cnt) { + if (adapter.config.type === 'client') { + client = new require(__dirname + '/lib/client')(adapter, states); + } else { + server = new require(__dirname + '/lib/server')(adapter, states); + } + } + }); +} + +function main() { + // Subscribe on own variables to publish it + if (adapter.config.publish) { + const parts = adapter.config.publish.split(','); + for (let t = 0; t < parts.length; t++) { + if (parts[t].indexOf('#') !== -1) { + adapter.log.warn('Used MQTT notation for yunkong2 in pattern "' + parts[t] + '": use "' + parts[t].replace(/#/g, '*') + ' notation'); + parts[t] = parts[t].replace(/#/g, '*'); + } + adapter.subscribeForeignStates(parts[t].trim()); + cnt++; + readStatesForPattern(parts[t]); + } + } else { + // subscribe for all variables + adapter.subscribeForeignStates('*'); + readStatesForPattern('*'); + } + + adapter.config.defaultQoS = parseInt(adapter.config.defaultQoS, 10) || 0; + adapter.config.retain = adapter.config.retain === 'true' || adapter.config.retain === true; + adapter.config.retransmitInterval = parseInt(adapter.config.retransmitInterval, 10) || 2000; + adapter.config.retransmitCount = parseInt(adapter.config.retransmitCount, 10) || 10; + + if (adapter.config.retransmitInterval < adapter.config.sendInterval) { + adapter.config.retransmitInterval = adapter.config.sendInterval * 5; + } + + // If no subscription, start client or server + if (!cnt) { + if (adapter.config.type === 'client') { + client = new require(__dirname + '/lib/client')(adapter, states); + } else { + server = new require(__dirname + '/lib/server')(adapter, states); + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a06fae5 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "yunkong2.mqtt", + "description": "The adapter allows to send and receive MQTT messages from yunkong2 and to be a broker.", + "version": "2.0.4", + "homepage": "https://git.spacen.net/yunkong2/yunkong2.mqtt", + "repository": { + "type": "git", + "url": "https://git.spacen.net/yunkong2/yunkong2.mqtt" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://git.spacen.net/yunkong2/yunkong2.mqtt/blob/master/LICENSE" + } + ], + "keywords": [ + "yunkong2", + "notification", + "MQTT", + "message" + ], + "dependencies": { + "mqtt": "^2.18.3", + "mqtt-connection": "^4.0.0", + "websocket-stream": "^5.1.2", + "ws": "^5.2.0" + }, + "devDependencies": { + "chai": "^4.1.2", + "gulp": "^3.9.1", + "mocha": "^5.2.0" + }, + "bugs": { + "url": "https://git.spacen.net/yunkong2/yunkong2.mqtt/issues" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "engines": { + "node": ">=6.0.0" + }, + "license": "MIT" +} diff --git a/test/lib/adapterSim.js b/test/lib/adapterSim.js new file mode 100644 index 0000000..088d4bb --- /dev/null +++ b/test/lib/adapterSim.js @@ -0,0 +1,87 @@ +'use strict'; + +module.exports = function (config) { + this.namespace = 'mqtt.0'; + + this.config = { + prefix: '', + sendInterval: 100, + publishOnSubscribe: false, + sendOnStartInterval: false, + defaultQoS: 1, + extraSet: false, + debug: true, + onchange: false, + port: 1883, + ssl: false, + webSocket: false, + certificates: null, + }; + let objects = {}; + let states = {}; + + this.config = Object.assign(this.config, config || {}); + + this.log = { + silly: text => console.log(`[${new Date().toISOString()} ${text}`), + debug: text => console.log(`[${new Date().toISOString()} ${text}`), + info: text => console.log(`[${new Date().toISOString()} ${text}`), + warn: text => console.warn(`[${new Date().toISOString()} ${text}`), + error: text => console.error(`[${new Date().toISOString()} ${text}`) + }; + + this.setState = (id, state, ack, cb) => { + if (!id.startsWith(this.namespace + '.')) { + id = this.namespace + '.' + id; + } + return this.setForeignState(id, state, ack, cb); + }; + + this.setForeignState = (id, state, ack, cb) => { + if (typeof ack === 'function') { + cb = ack; + ack = undefined; + } + + if (typeof state !== 'object') { + state = { + val: state, + ack: ack || false + } + } + state.ts = Date.now(); + state.ack = !!state.ack; + + states[id] = state; + cb && cb(null); + }; + + this.getObject = (id, cb) => { + if (!id.startsWith(this.namespace + '.')) { + id = this.namespace + '.' + id; + } + return this.getForeignObject(id, cb); + }; + + this.setObject = (id, obj, cb) => { + if (!id.startsWith(this.namespace + '.')) { + id = this.namespace + '.' + id; + } + return this.setForeignObject(id, obj, cb); + }; + + this.getForeignObject = (id, cb) => { + cb(null, objects[id]); + }; + + this.setForeignObject = (id, obj, cb) => { + objects[id] = obj; + cb && cb(); + }; + + this.clearAll = () => { + objects = {}; + states = {}; + }; + return this; +}; \ No newline at end of file diff --git a/test/lib/mqttClient.js b/test/lib/mqttClient.js new file mode 100644 index 0000000..b0f502b --- /dev/null +++ b/test/lib/mqttClient.js @@ -0,0 +1,93 @@ +'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', () => { + // message is Buffer + if (cbConnected) { + cbConnected(false); + } else { + console.log('Test MQTT Client closed'); + } + }); + + this.client.on('error', error => { + console.error('Test MQTT Client error: ' + error); + }); + + this.publish = (topic, message, qos, retain, cb) => { + if (typeof qos === 'function') { + cb = qos; + qos = undefined; + } + if (typeof retain === 'function') { + cb = retain; + retain = undefined; + } + const opts = { + retain: retain || false, + qos: qos || 0 + }; + that.client.publish(topic, message, opts, cb); + }; + this.subscribe = (topic, opts, cb) => { + if (typeof opts === 'function') { + cb = opts; + opts = null; + } + that.client.subscribe(topic, opts, cb); + }; + this.unsubscribe = (topic, cb) => { + that.client.unsubscribe(topic, cb); + }; + this.destroy = () => { + if (that.client) { + that.client.end(); + that.client = null; + } + }; + + this.stop = this.destroy; + + return this; +} + +if (typeof module !== 'undefined' && module.parent) { + module.exports = Client; +} else { + new Client(); +} diff --git a/test/lib/mqttServer.js b/test/lib/mqttServer.js new file mode 100644 index 0000000..301f56f --- /dev/null +++ b/test/lib/mqttServer.js @@ -0,0 +1,149 @@ +'use strict'; +const mqtt = require('mqtt-connection'); + +function Server(config) { + let clients = {}; + let server; + let net; + let http; + let ws; + let wsStream; + let serverForWs; + let serverWs; + + config = config || {}; + + function startServer(socket, server, port, bind, ssl, ws) { + socket.on('connection', stream => { + let client; + if (ws) { + client = mqtt(wsStream(stream)); + } else { + client = mqtt(stream); + } + + client.on('connect', function (packet) { + client.id = packet.clientId; + clients[client.id] = client; + if (config.user) { + if (config.user !== packet.username || + config.pass !== packet.password.toString()) { + console.error('Client [' + packet.clientId + '] has invalid password(' + packet.password + ') or username(' + packet.username + ')'); + client.connack({returnCode: 4}); + if (clients[client.id]) delete clients[client.id]; + client.stream.end(); + return; + } + } + console.log('Client [' + packet.clientId + '] connected: user - ' + packet.username + ', pass - ' + packet.password); + client.connack({returnCode: 0}); + + client.publish({topic: 'testServer/connected', payload: 'true'}); + }); + + client.on('publish', function (packet) { + console.log('Client [' + client.id + '] publishes "' + packet.topic + '": ' + packet.payload); + for (let k in clients) { + clients[k].publish({topic: packet.topic, payload: packet.payload}); + } + }); + + client.on('subscribe', function (packet) { + let granted = []; + console.log('Client [' + client.id + '] subscribes on "' + JSON.stringify(packet.subscriptions) + '"'); + for (let i = 0; i < packet.subscriptions.length; i++) { + granted.push(packet.subscriptions[i].qos); + } + client.suback({granted: granted, messageId: packet.messageId}); + }); + + client.on('pingreq', function (packet) { + console.log('Client [' + client.id + '] pingreq'); + client.pingresp(); + }); + + client.on('disconnect', function (packet) { + if (clients[client.id]) delete clients[client.id]; + console.log('Client [' + client.id + '] disconnected'); + client.stream.end(); + }); + + client.on('close', function (err) { + if (clients[client.id]) delete clients[client.id]; + console.log('Client [' + client.id + '] closed'); + }); + + client.on('error', function (err) { + if (clients[client.id]) delete clients[client.id]; + console.log('[' + client.id + '] ' + err); + client.stream.end(); + }); + }); + (server || socket).listen(port, bind, () => { + console.log(`Starting MQTT ${ws ? '-WebSocket' : ''}${ssl ? ' (Secure)' : ''}' server on port ${port}`); + }); + } + + const port = 1883; + const sslOptions = { + key: "-----BEGIN RSA PRIVATE KEY-----\r\nMIICXQIBAAKBgQDQ6dVCuqpl0hdECy35tQP7n/FKAK6Yz8z04F3g8NtkLrJ3IR1+\r\nNo0ijLE2Ka5ONZV2WlRzybWomAvOGnfbSH7NG/wkQ9saBb15bAU03RLeyFmDc5Rz\r\newgjoQzJwXNWIIbqdiUWUqhy3IOzfoRrNprpDm5mv2pwEUxOuF8mB62vgQIDAQAB\r\nAoGBAKmS5DQB6IY1fgURPgROVilMrkJvQ0luguLRq+IGH062SM5B5vqntO+yW7Wn\r\nJ4D8JZGnyJ0jwXxTzmFBQsCPm7vQ3VkH1ir4JhlIWJ11Z3p3XMNWNJ5mrDAyEupn\r\nShCFQxW9EDL7efVFztqgyiWw5/uxV4AJQyBgtsF4PijmgT8xAkEA+SlmVXcuzIPy\r\nZTfNXRCWHvzZM9EaRVQXNSYqMHXLRx412gw42ihk/+GIYaw7y5ObjlMosfzzCyot\r\naMMA/KT1TwJBANalpnrDE0BhYuv/ccnxJv/pZ6aJZ4P/gyRV02UUc0WTAGnxU4el\r\nJPtREWCyCjaVq26S7fh4DGotcDhDEkpzei8CQA5aGyHrJo/zPcAk0bh9nxgT2nMI\r\npWm+6UNPenimIFptXA6+S3wNfZvbot51bFBSpVAybBKsjldjS5BQQztKSTMCQQCe\r\nMhYBkjZlE6Fhh7GogOgaYj53GfvF6BISPIMBk1HlrBL5AdhrN4aLBtOE7ZLjaemg\r\nI//pSSj1NCnp/VzErFkXAkA/6q2Th8M4Z2LzL46GeRavLXFd1IQmFULWZAkx5afk\r\n8/anbz31nnA9CFu+oR/jTp7urYsIUQ3y6ksJwGGKHVlQ\r\n-----END RSA PRIVATE KEY-----\r\n", + cert: "-----BEGIN CERTIFICATE-----\r\nMIICfzCCAegCCQC1y0d8DNip4TANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMC\r\nREUxGTAXBgNVBAgMEEJhZGVuV3VlcnRlbWJlcmcxEjAQBgNVBAcMCUthcmxzcnVo\r\nZTERMA8GA1UECgwIaW9Ccm9rZXIxEDAOBgNVBAMMB0JsdWVmb3gxIDAeBgkqhkiG\r\n9w0BCQEWEWRvZ2Fmb3hAZ21haWwuY29tMB4XDTE1MDQyMjIwMjgwM1oXDTE2MDQy\r\nMTIwMjgwM1owgYMxCzAJBgNVBAYTAkRFMRkwFwYDVQQIDBBCYWRlbld1ZXJ0ZW1i\r\nZXJnMRIwEAYDVQQHDAlLYXJsc3J1aGUxETAPBgNVBAoMCGlvQnJva2VyMRAwDgYD\r\nVQQDDAdCbHVlZm94MSAwHgYJKoZIhvcNAQkBFhFkb2dhZm94QGdtYWlsLmNvbTCB\r\nnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0OnVQrqqZdIXRAst+bUD+5/xSgCu\r\nmM/M9OBd4PDbZC6ydyEdfjaNIoyxNimuTjWVdlpUc8m1qJgLzhp320h+zRv8JEPb\r\nGgW9eWwFNN0S3shZg3OUc3sII6EMycFzViCG6nYlFlKoctyDs36Eazaa6Q5uZr9q\r\ncBFMTrhfJgetr4ECAwEAATANBgkqhkiG9w0BAQUFAAOBgQBgp4dhA9HulN7/rh4H\r\n+e+hAYqjWvFpdNqwcWAyopBig9B9WL3OIkzpgTuBmH76JxzJCuZJkjO4HLGzQ3KF\r\nsFU0lvqqoz9osgYmXe1K0fBjIcm/RFazGTHVxv+UgVqQ3KldrlkvR3T2VIRlT5hI\r\n0Y1m6J3YZDMF7D6uc1jrsYHkMQ==\r\n-----END CERTIFICATE-----\r\n" + }; + + this.start = function () { + + if (process.argv[2] === 'ssl') { + net = net || require('tls'); + if (config.webSocket) { + http = http || require('https'); + } + } else { + net = net || require('net'); + if (config.webSocket) { + http = http || require('http'); + } + } + server = new net.Server(sslOptions); + startServer(server, null, port, '127.0.0.1', process.argv[2] === 'ssl', false); + http = http || require('https'); + ws = ws || require('ws'); + wsStream = wsStream || require('websocket-stream'); + serverForWs = http.createServer(sslOptions); + serverWs = new ws.Server({server: serverForWs}); + + startServer(serverWs, serverForWs, port + 1, '127.0.0.1', process.argv[2] === 'ssl', true); + }; + + this.stop = function () { + // destroy all clients (this will emit the 'close' event above) + for (let i in clients) { + clients[i].destroy(); + } + + if (server) { + server.close(() => { + console.log('Server closed.'); + server.unref(); + server = null; + }); + } + + if (serverForWs) { + serverForWs.close(() => { + console.log('WS-Server closed.'); + serverForWs.unref(); + serverForWs = null; + }); + } + }; + + this.start(); + + return this; +} + +if (typeof module !== 'undefined' && module.parent) { + module.exports = Server; +} else { + new Server(); +} diff --git a/test/lib/objects.js b/test/lib/objects.js new file mode 100644 index 0000000..8cfbf75 --- /dev/null +++ b/test/lib/objects.js @@ -0,0 +1,1411 @@ +'use strict'; +const path = require('path'); +const rootDir = path.normalize(__dirname + '/../../'); +let adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; + +const logger = { + info: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } +}; + +function Objects(cb) { + if (!(this instanceof Objects)) return new Objects(cb); + + const _Objects = require(rootDir + 'tmp/node_modules/yunkong2.js-controller/lib/objects'); + this.connected = false; + const that = this; + + that.namespace = 'test'; + + this.objects = new _Objects({ + connection: { + type : 'file', + host : '127.0.0.1', + port : 19001, + user : '', + pass : '', + noFileCache: false, + connectTimeout: 2000 + }, + logger: logger, + connected: () => { + this.connected = true; + if (typeof cb === 'function') cb(); + }, + disconnected: () => { + this.connected = false; + }, + change: (id, obj) => { + if (!id) { + logger.error(that.namespace + ' change ID is empty: ' + JSON.stringify(obj)); + return; + } + + if (id.slice(that.namespace.length) === that.namespace) { + if (typeof options.objectChange === 'function') options.objectChange(id.slice(that.namespace.length + 1), obj); + + // emit 'objectChange' event instantly + setImmediate(() => that.emit('objectChange', id.slice(that.namespace.length + 1), obj)); + } else { + if (typeof options.objectChange === 'function') options.objectChange(id, obj); + + // emit 'objectChange' event instantly + setImmediate(() => that.emit('objectChange', id, obj)); + } + }, + connectTimeout: error => { + if (logger) logger.error(that.namespace + ' no connection to objects DB'); + if (typeof cb === 'function') cb('Connect timeout'); + } + }); + + that._namespaceRegExp = new RegExp('^' + that.namespace); // cache the regex object 'adapter.0' + + that._fixId = function _fixId(id) { + let result = ''; + // If id is an object + if (typeof id === 'object') { + // Add namespace + device + channel + result = that.namespace + '.' + (id.device ? id.device + '.' : '') + (id.channel ? id.channel + '.' : '') + id.state; + } else { + result = id; + if (!that._namespaceRegExp.test(id)) result = that.namespace + '.' + id; + } + return result; + }; + + that.setObject = function setObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!id) { + logger.error(that.namespace + ' setObject id missing!!'); + return; + } + + if (!obj) { + logger.error(that.namespace + ' setObject ' + id + ' object missing!'); + return; + } + + if (obj.hasOwnProperty('type')) { + if (!obj.hasOwnProperty('native')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property native missing!'); + obj.native = {}; + } + // Check property 'common' + if (!obj.hasOwnProperty('common')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common missing!'); + obj.common = {}; + } else if (obj.type === 'state') { + // Try to extend the model for type='state' + // Check property 'role' by 'state' + if (obj.common.hasOwnProperty('role') && defaultObjs[obj.common.role]) { + obj.common = extend(true, defaultObjs[obj.common.role], obj.common); + } else if (!obj.common.hasOwnProperty('role')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.role missing!'); + } + if (!obj.common.hasOwnProperty('type')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.type missing!'); + } + } + + if (!obj.common.hasOwnProperty('name')) { + obj.common.name = id; + logger.debug(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.name missing, using id as name'); + } + + id = that._fixId(id, obj.type); + + if (obj.children || obj.parent) { + logger.warn('Do not use parent or children for ' + id); + } + that.objects.setObject(id, obj, options, callback); + + } else { + logger.error(that.namespace + ' setObject ' + id + ' mandatory property type missing!'); + } + }; + + that.extendObject = function extendObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + id = that._fixId(id, obj.type); + + if (obj.children || obj.parent) { + logger.warn('Do not use parent or children for ' + id); + } + // delete arrays if they should be changed + if (obj && ( + (obj.common && obj.common.members) || + (obj.native && obj.native.repositories) || + (obj.native && obj.native.certificates) || + (obj.native && obj.native.devices)) + ) { + // Read whole object + that.objects.getObject(id, options, (err, oldObj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!oldObj) { + logger.error('Object ' + id + ' not exist!'); + oldObj = {}; + } + if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { + oldObj.native.repositories = []; + } + if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { + oldObj.common.members = []; + } + if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { + oldObj.native.certificates = []; + } + if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { + oldObj.native.devices = []; + } + obj = extend(true, oldObj, obj); + + that.objects.setObject(id, obj, options, callback); + }); + } else { + that.objects.extendObject(id, obj, options, callback); + } + }; + + that.setForeignObject = function setForeignObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.setObject(id, obj, options, callback); + }; + + that.extendForeignObject = function extendForeignObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + // delete arrays if they should be changed + if (obj && ((obj.native && (obj.native.repositories || obj.native.certificates || obj.native.devices)) || + (obj.common && obj.common.members))) { + // Read whole object + that.objects.getObject(id, options, (err, oldObj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!oldObj) { + logger.error('Object ' + id + ' not exist!'); + oldObj = {}; + } + if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { + oldObj.native.repositories = []; + } + if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { + oldObj.common.members = []; + } + if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { + oldObj.native.certificates = []; + } + if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { + oldObj.native.devices = []; + } + obj = extend(true, oldObj, obj); + + that.objects.setObject(id, obj, callback); + }); + } else { + that.objects.extendObject(id, obj, options, callback); + } + }; + + that.getObject = function getObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(that._fixId(id), options, callback); + }; + + // Get the enum tree + that.getEnum = function getEnum(_enum, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!_enum.match('^enum.')) _enum = 'enum.' + _enum; + const result = {}; + + that.objects.getObjectView('system', 'enum', {startkey: _enum + '.', endkey: _enum + '.\u9999'}, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + // Read all + let count = 0; + + for (let t = 0; t < res.rows.length; t++) { + count++; + that.objects.getObject(res.rows[t].id, options, (err, _obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + + if (!err && _obj) result[_obj._id] = _obj; + if (!--count && callback) callback (err, result, _enum); + }); + } + if (!count && callback) callback(err, result); + }); + }; + + // read for given enums the members of them + that.getEnums = function getEnums(_enumList, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + const _enums = {}; + if (_enumList) { + if (typeof _enumList === 'string') _enumList = [_enumList]; + let count = 0; + for (let t = 0; t < _enumList.length; t++) { + count++; + that.getEnum(_enumList[t], options, (list, _enum) => { + _enums[_enum] = list; + if (!--count && callback) callback(_enums); + }); + } + } else { + // Read all enums + that.objects.getObjectView('system', 'enum', {startkey: 'enum.', endkey: 'enum.\u9999'}, options, (err, res) => { + if (err) { + callback(err); + return; + } + const result = {}; + for (let i = 0; i < res.rows.length; i++) { + const parts = res.rows[i].id.split('.', 3); + if (!parts[2]) continue; + if (!result[parts[0] + '.' + parts[1]]) result[parts[0] + '.' + parts[1]] = {}; + result[parts[0] + '.' + parts[1]][res.rows[i].id] = res.rows[i].value; + } + + if (callback) callback(err, result); + }); + } + }; + + that.getForeignObjects = function getForeignObjects(pattern, type, enums, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + let params = {}; + if (pattern && pattern !== '*') { + params = { + startkey: pattern.replace('*', ''), + endkey: pattern.replace('*', '\u9999') + }; + } + if (typeof enums === 'function') { + callback = enums; + enums = null; + } + if (typeof type === 'function') { + callback = type; + type = null; + } + that.objects.getObjectView('system', type || 'state', params, options, (err, res) => { + if (err) { + callback(err); + return; + } + + that.getEnums(enums, (_enums) => { + const list = {}; + for (let i = 0; i < res.rows.length; i++) { + list[res.rows[i].id] = res.rows[i].value; + + if (_enums) { + // get device or channel of this state and check it too + const parts = res.rows[i].id.split('.'); + parts.splice(parts.length - 1, 1); + const channel = parts.join('.'); + parts.splice(parts.length - 1, 1); + const device = parts.join('.'); + + list[res.rows[i].id].enums = {}; + for (const es in _enums) { + if (!_enums.hasOwnProperty(es)) continue; + for (const e in _enums[es]) { + if (!_enums[es].hasOwnProperty(e)) continue; + if (!_enums[es][e] || !_enums[es][e].common || !_enums[es][e].common.members) continue; + if (_enums[es][e].common.members.indexOf(res.rows[i].id) !== -1 || + _enums[es][e].common.members.indexOf(channel) !== -1 || + _enums[es][e].common.members.indexOf(device) !== -1) { + list[res.rows[i].id].enums[e] = _enums[es][e].common.name; + } + } + } + } + } + callback(null, list); + }); + }); + + }; + + that.findForeignObject = function findForeignObject(id, type, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.findObject(id, type, options, callback); + }; + + that.getForeignObject = function getForeignObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(id, options, callback); + }; + + that.delObject = function delObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + id = that._fixId(id); + that.objects.delObject(id, options, callback); + }; + + that.delForeignObject = function delForeignObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.delObject(id, options, callback); + }; + + that.subscribeObjects = function subscribeObjects(pattern, options) { + if (pattern === '*') { + that.objects.subscribe(that.namespace + '.*'); + } else { + pattern = that._fixId(pattern); + that.objects.subscribe(pattern, options); + } + }; + + that.subscribeForeignObjects = function subscribeObjects(pattern, options) { + that.objects.subscribe(pattern, options); + }; + + that.unsubscribeForeignObjects = function unsubscribeForeignObjects(pattern, options) { + if (!pattern) pattern = '*'; + that.objects.unsubscribe(pattern, options); + }; + + that.unsubscribeObjects = function unsubscribeObjects(pattern, options) { + if (pattern === '*') { + that.objects.unsubscribe(that.namespace + '.*', options); + } else { + pattern = that._fixId(pattern); + that.objects.unsubscribe(pattern); + } + }; + + that.setObjectNotExists = function setObjectNotExists(id, object, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + id = that._fixId(id); + + if (object.children || object.parent) { + logger.warn('Do not use parent or children for ' + id); + } + + that.objects.getObject(id, options, (err, obj) => { + if (!obj) { + that.objects.setObject(id, object, callback); + } + }); + }; + + that.setForeignObjectNotExists = function setForeignObjectNotExists(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(id, options, (err, obj) => { + if (!obj) { + that.objects.setObject(id, obj, callback); + } + }); + }; + + that._DCS2ID = function (device, channel, stateOrPoint) { + let id = ''; + if (device) id += device; + if (channel) id += ((id) ? '.' : '') + channel; + + if (stateOrPoint !== true && stateOrPoint !== false) { + if (stateOrPoint) id += ((id) ? '.' : '') + stateOrPoint; + } else if (stateOrPoint === true) { + if (id) id += '.'; + } + return id; + }; + + that.createDevice = function createDevice(deviceName, common, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!deviceName) { + that.log.error('Try to create device with empty name!'); + return; + } + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + if (typeof common === 'function') { + callback = common; + common = {}; + } + common = common || {}; + common.name = common.name || deviceName; + + deviceName = deviceName.replace(/[.\s]+/g, '_'); + _native = _native || {}; + + that.setObjectNotExists(deviceName, { + 'type': 'device', + 'common': common, + 'native': _native + }, options, callback); + }; + + // name of channel must be in format 'channel' + that.createChannel = function createChannel(parentDevice, channelName, roleOrCommon, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!channelName) throw 'Try to create channel without name!'; + + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + + if (typeof roleOrCommon === 'function') { + callback = roleOrCommon; + roleOrCommon = undefined; + } + + let common = {}; + if (typeof roleOrCommon === 'string') { + common = { + role: roleOrCommon + }; + } else if (typeof roleOrCommon === 'object') { + common = roleOrCommon; + } + common.name = common.name || channelName; + + if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + channelName = channelName.replace(/[.\s]+/g, '_'); + channelName = that._DCS2ID(parentDevice, channelName); + + _native = _native || {}; + + const obj = { + 'type': 'channel', + 'common': common, + 'native': _native + }; + + that.setObject(channelName, obj, options, callback); + }; + + that.createState = function createState(parentDevice, parentChannel, stateName, roleOrCommon, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!stateName) throw 'Empty name is not allowed!'; + + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + + if (typeof roleOrCommon === 'function') { + callback = roleOrCommon; + roleOrCommon = undefined; + } + + let common = {}; + if (typeof roleOrCommon === 'string') { + common = { + role: roleOrCommon + }; + } else if (typeof roleOrCommon === 'object') { + common = roleOrCommon; + } + + common.name = common.name || stateName; + _native = _native || {}; + + common.read = (common.read === undefined) ? true : common.read; + common.write = (common.write === undefined) ? false : common.write; + + if (!common.role) { + logger.error('Try to create state ' + (parentDevice ? (parentDevice + '.') : '') + parentChannel + '.' + stateName + ' without role'); + return; + } + + if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + if (parentChannel) parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + stateName = stateName.replace(/[.\s]+/g, '_'); + const id = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}); + + that.setObjectNotExists(id, { + type: 'state', + common: common, + native: _native + }, options, callback); + + if (common.def !== undefined) { + that.setState(id, common.def, options); + } else { + that.setState(id, null, false, options); + } + }; + + that.deleteDevice = function deleteDevice(deviceName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + deviceName = deviceName.replace(/[.\s]+/g, '_'); + if (!that._namespaceRegExp.test(deviceName)) deviceName = that.namespace + '.' + deviceName; + + that.objects.getObjectView('system', 'device', {startkey: deviceName, endkey: deviceName}, options, (err, res) => { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + let cnt = 0; + if (res.rows.length > 1) that.log.warn('Found more than one device ' + deviceName); + + for (let t = 0; t < res.rows.length; t++) { + cnt++; + that.delObject(res.rows[t].id, options, err => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + + if (!--cnt) { + cnt = 0; // just to better understand + that.objects.getObjectView('system', 'channel', {startkey: deviceName + '.', endkey: deviceName + '.\u9999'}, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + for (let k = 0; k < res.rows.length; k++) { + cnt++; + that.deleteChannel(deviceName, res.rows[k].id, options, err => { + if (!(--cnt) && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + if (!cnt && callback) callback(); + }); + } + }); + } + if (!cnt && callback) callback(); + }); + }; + + that.addChannelToEnum = function addChannelToEnum(enumName, addTo, parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(channelName)) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName.replace(/[.\s]+/g, '_'); + + const objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + if (addTo.match(/^enum\./)) { + that.objects.getObject(addTo, options, (err, obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!err && obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, options, err => { + if (callback) callback(err); + }); + } + } + }); + } else { + if (enumName.match(/^enum\./)) enumName = enumName.substring(5); + + that.objects.getObject('enum.' + enumName + '.' + addTo, options, (err, obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + if (obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, options, callback); + } else { + if (callback) callback(); + } + } else { + // Create enum + that.objects.setObject('enum.' + enumName + '.' + addTo, { + common: { + name: addTo, + members: [objId] + }, + type: 'enum' + }, options, callback); + } + }); + } + }; + + that.deleteChannelFromEnum = function deleteChannelFromEnum(enumName, parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (parentDevice.substring(0, that.namespace.length) === that.namespace) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (channelName && channelName.substring(0, that.namespace.length) === that.namespace) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName || ''; + channelName = channelName.replace(/[.\s]+/g, '_'); + + const objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + if (enumName) { + enumName = 'enum.' + enumName + '.'; + } else { + enumName = 'enum.'; + } + + that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (res) { + let count = 0; + for (let i = 0; i < res.rows.length; i++) { + count++; + that.objects.getObject(res.rows[i].id, options, (err, obj) => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + if (!err && obj && obj.common && obj.common.members) { + const pos = obj.common.members.indexOf(objId); + if (pos !== -1) { + obj.common.members.splice(pos, 1); + count++; + that.objects.setObject(obj._id, obj, options, err => { + if (!(--count) && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + } + count--; + if (!count && callback) callback(err); + }); + } + } else if (callback) { + callback (err); + } + }); + }; + + that.deleteChannel = function deleteChannel(parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof channelName === 'function') { + callback = channelName; + channelName = parentDevice; + parentDevice = ''; + } + if (parentDevice && !channelName) { + channelName = parentDevice; + parentDevice = ''; + } else if (parentDevice && typeof channelName === 'function') { + callback = channelName; + channelName = parentDevice; + parentDevice = ''; + } + if (!parentDevice) parentDevice = ''; + that.deleteChannelFromEnum('', parentDevice, channelName); + const _parentDevice = parentDevice; + const _channelName = channelName; + + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (channelName && that._namespaceRegExp.test(channelName)) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName || ''; + channelName = channelName.replace(/[.\s]+/g, '_'); + + channelName = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + logger.info('Delete channel ' + channelName); + + that.objects.getObjectView('system', 'channel', {startkey: channelName, endkey: channelName}, options, (err, res) => { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + let cnt = 0; + if (res.rows.length > 1) { + that.log.warn('Found more than one channel ' + channelName); + } + + for (let t = 0; t < res.rows.length; t++) { + cnt++; + that.delObject(res.rows[t].id, options, err => { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + cnt--; + if (!cnt) { + that.objects.getObjectView('system', 'state', {startkey: channelName + '.', endkey: channelName + '.\u9999'}, options, (err, res) => { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + for (let k = 0; k < res.rows.length; k++) { + that.deleteState(_parentDevice, _channelName, res.rows[k].id, options, err => { + if (!(--cnt) && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + if (!cnt && callback) callback(); + }); + } + }); + } + if (!cnt && callback) callback(); + }); + }; + + that.deleteState = function deleteState(parentDevice, parentChannel, stateName, options, callback) { + if (typeof parentChannel === 'function' && stateName === undefined) { + stateName = parentDevice; + callback = parentChannel; + parentChannel = ''; + parentDevice = ''; + } else + if (parentChannel === undefined && stateName === undefined) { + stateName = parentDevice; + parentDevice = ''; + parentChannel = ''; + } else { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof stateName === 'function') { + callback = stateName; + stateName = parentChannel; + parentChannel = parentDevice; + parentDevice = ''; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + stateName = parentDevice; + parentChannel = ''; + parentDevice = ''; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + stateName = parentDevice; + parentChannel = ''; + parentDevice = ''; + } + } + + that.deleteStateFromEnum('', parentDevice, parentChannel, stateName, options); + + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName || ''; + stateName = stateName.replace(/[.\s]+/g, '_'); + + const _name = that._DCS2ID(parentDevice, parentChannel, stateName); + that.delState(_name, options, function () { + that.delObject(_name, options, callback); + }); + }; + + that.getDevices = function getDevices(callback, options) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObjectView('system', 'device', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999'}, options, (err, obj) => { + if (callback) { + if (obj.rows.length) { + const res = []; + for (let i = 0; i < obj.rows.length; i++) { + res.push(obj.rows[i].value); + } + callback(null, res); + } else { + callback(err, []); + } + } + }); + }; + + that.getChannelsOf = function getChannelsOf(parentDevice, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof parentDevice === 'function') { + callback = parentDevice; + parentDevice = null; + } + if (!parentDevice) parentDevice = ''; + + if (parentDevice && that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + parentDevice = that.namespace + (parentDevice ? ('.' + parentDevice) : ''); + that.objects.getObjectView('system', 'channel', {startkey: parentDevice + '.', endkey: parentDevice + '.\u9999'}, options, (err, obj) => { + if (callback) { + if (obj.rows.length) { + const res = []; + for (let i = 0; i < obj.rows.length; i++) { + res.push(obj.rows[i].value); + } + callback(null, res); + } else { + callback(err, []); + } + } + }); + }; + + that.getChannels = that.getChannelsOf; + + that.getStatesOf = function getStatesOf(parentDevice, parentChannel, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof parentDevice === 'function') { + callback = parentDevice; + parentDevice = null; + parentChannel = null; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + parentChannel = null; + } + + if (!parentDevice) { + parentDevice = ''; + } else { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (!parentChannel) { + parentChannel = ''; + } else if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + + if (parentDevice && parentChannel && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + + const id = that.namespace + '.' + that._DCS2ID(parentDevice, parentChannel, true); + + that.objects.getObjectView('system', 'state', {startkey: id, endkey: id + '\u9999'}, options, (err, obj) => { + if (callback) { + const res = []; + if (obj.rows.length) { + let read = 0; + for (let i = 0; i < obj.rows.length; i++) { + read++; + that.objects.getObject(obj.rows[i].id, (err, subObj) => { + if (subObj) res.push(subObj); + + if (!--read) callback(null, res); + }); + } + } else { + callback(null, res); + } + } + }); + }; + + that.addStateToEnum = function addStateToEnum(enumName, addTo, parentDevice, parentChannel, stateName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName.replace(/[.\s]+/g, '_'); + + const objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}); + + if (addTo.match(/^enum\./)) { + that.objects.getObject(addTo, options, (err, obj) => { + if (!err && obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, options, err => { + if (callback) callback(err); + }); + } + } + }); + } else { + if (enumName.match(/^enum\./)) enumName = enumName.substring(5); + + that.objects.getObject('enum.' + enumName + '.' + addTo, options, (err, obj) => { + if (!err && obj) { + const pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + that.objects.setObject(obj._id, obj, callback); + } else { + if (callback) callback(); + } + } else { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + // Create enum + that.objects.setObject('enum.' + enumName + '.' + addTo, { + common: { + name: addTo, + members: [objId] + }, + type: 'enum' + }, options, callback); + } + }); + } + }; + + that.deleteStateFromEnum = function deleteStateFromEnum(enumName, parentDevice, parentChannel, stateName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName.replace(/[.\s]+/g, '_'); + + const objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}, 'state'); + + if (enumName) { + enumName = 'enum.' + enumName + '.'; + } else { + enumName = 'enum.'; + } + + that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, (err, res) => { + if (!err && res) { + let count = 0; + for (let i = 0; i < res.rows.length; i++) { + count++; + that.objects.getObject(res.rows[i].id, options, (err, obj) => { + if (err) { + if (callback) callback(err); + callback = null; + return; + } + + if (!err && obj && obj.common && obj.common.members) { + const pos = obj.common.members.indexOf(objId); + if (pos !== -1) { + obj.common.members.splice(pos, 1); + count++; + that.objects.setObject(obj._id, obj, err => { + if (!--count && callback) callback(err); + }); + } + } + if (!--count && callback) callback(err); + }); + } + } else if (callback) { + callback (err); + } + }); + }; + + that.chmodFile = function readDir(adapter, path, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.chmodFile(adapter, path, options, callback); + }; + + that.readDir = function readDir(adapter, path, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.readDir(adapter, path, options, callback); + }; + + that.unlink = function unlink(adapter, name, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.unlink(adapter, name, options, callback); + }; + + that.rename = function rename(adapter, oldName, newName, options, callback) { + if (adapter === null) adapter = that.name; + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.rename(adapter, oldName, newName, options, callback); + }; + + that.mkdir = function mkdir(adapter, dirname, options, callback) { + if (adapter === null) adapter = that.name; + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.mkdir(adapter, dirname, options, callback); + }; + + that.readFile = function readFile(adapter, filename, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.readFile(adapter, filename, options, callback); + }; + + that.writeFile = function writeFile(adapter, filename, data, options, callback) { + if (adapter === null) adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.writeFile(adapter, filename, data, options, callback); + }; + + that.formatDate = function formatDate(dateObj, isSeconds, _format) { + if (typeof isSeconds !== 'boolean') { + _format = isSeconds; + isSeconds = false; + } + + let format = _format || that.dateFormat || 'DD.MM.YYYY'; + + if (!dateObj) return ''; + const text = typeof dateObj; + if (text === 'string') { + const pos = dateObj.indexOf('.'); + if (pos !== -1) dateObj = dateObj.substring(0, pos); + return dateObj; + } + if (text !== 'object') dateObj = isSeconds ? new Date(dateObj * 1000) : new Date(dateObj); + + let v; + + // Year + if (format.indexOf('YYYY') !== -1 || format.indexOf('JJJJ') !== -1 || format.indexOf('ГГГГ') !== -1) { + v = dateObj.getFullYear(); + format = format.replace('YYYY', v); + format = format.replace('JJJJ', v); + format = format.replace('ГГГГ', v); + } else if (format.indexOf('YY') !== -1 || format.indexOf('JJ') !== -1 || format.indexOf('ГГ') !== -1) { + v = dateObj.getFullYear() % 100; + format = format.replace('YY', v); + format = format.replace('JJ', v); + format = format.replace('ГГ', v); + } + // Month + if (format.indexOf('MM') !== -1 || format.indexOf('ММ') !== -1) { + v = dateObj.getMonth() + 1; + if (v < 10) v = '0' + v; + format = format.replace('MM', v); + format = format.replace('ММ', v); + } else if (format.indexOf('M') !== -1 || format.indexOf('М') !== -1) { + v = dateObj.getMonth() + 1; + format = format.replace('M', v); + format = format.replace('М', v); + } + + // Day + if (format.indexOf('DD') !== -1 || format.indexOf('TT') !== -1 || format.indexOf('ДД') !== -1) { + v = dateObj.getDate(); + if (v < 10) v = '0' + v; + format = format.replace('DD', v); + format = format.replace('TT', v); + format = format.replace('ДД', v); + } else if (format.indexOf('D') !== -1 || format.indexOf('TT') !== -1 || format.indexOf('Д') !== -1) { + v = dateObj.getDate(); + format = format.replace('D', v); + format = format.replace('T', v); + format = format.replace('Д', v); + } + + // hours + if (format.indexOf('hh') !== -1 || format.indexOf('SS') !== -1 || format.indexOf('чч') !== -1) { + v = dateObj.getHours(); + if (v < 10) v = '0' + v; + format = format.replace('hh', v); + format = format.replace('SS', v); + format = format.replace('чч', v); + } else if (format.indexOf('h') !== -1 || format.indexOf('S') !== -1 || format.indexOf('ч') !== -1) { + v = dateObj.getHours(); + format = format.replace('h', v); + format = format.replace('S', v); + format = format.replace('ч', v); + } + + // minutes + if (format.indexOf('mm') !== -1 || format.indexOf('мм') !== -1) { + v = dateObj.getMinutes(); + if (v < 10) v = '0' + v; + format = format.replace('mm', v); + format = format.replace('мм', v); + } else if (format.indexOf('m') !== -1 || format.indexOf('м') !== -1) { + v = dateObj.getMinutes(); + format = format.replace('m', v); + format = format.replace('v', v); + } + + // seconds + if (format.indexOf('ss') !== -1 || format.indexOf('сс') !== -1) { + v = dateObj.getSeconds(); + if (v < 10) v = '0' + v; + v = v.toString(); + format = format.replace('ss', v); + format = format.replace('cc', v); + } else if (format.indexOf('s') !== -1 || format.indexOf('с') !== -1) { + v = dateObj.getHours().toString(); + format = format.replace('s', v); + format = format.replace('с', v); + } + return format; + }; + + return this; +} + +module.exports = Objects; \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..8c62a69 --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,718 @@ +'use strict'; +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +const fs = require('fs'); +const path = require('path'); +const child_process = require('child_process'); +const rootDir = path.normalize(__dirname + '/../../'); +const pkg = require(rootDir + 'package.json'); +const debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +let adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +let adapterStarted = false; + +function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +const appName = getAppName().toLowerCase(); + +let objects; +let states; + +let pid = null; + +function copyFileSync(source, target) { + + let targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + try { + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + catch (err) { + console.log(`file copy error: ${source}" -> "${targetFile}" (error ignored)`); + } +} + +function copyFolderRecursiveSync(source, target, ignore) { + let files = []; + + let base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + const targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + const curSource = path.join(source, file); + const curTarget = path.join(targetFolder, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, curTarget); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + let f = fs.readFileSync(dataDir + 'objects.json'); + const objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + let f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + const f = fs.readFileSync(dataDir + 'objects.json'); + const objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(() => cb && cb(), 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(() => checkIsAdapterInstalled(cb, counter + 1), 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + const f = fs.readFileSync(dataDir + 'objects.json'); + const objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(() => cb && cb(), 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(() => checkIsControllerInstalled(cb, counter + 1), 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + const startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(error => { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + const _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, () => { + checkIsAdapterInstalled(error => { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', (code, signal) => { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', (code, signal) => { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + let _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, () => { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + let __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, () => { + checkIsControllerInstalled(() => { + // change ports for object and state DBs + const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(() => { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + const client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', () => { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(() => { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(() => { + let _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, () => { + // change ports for object and state DBs + const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(() => { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setImmediate(() => { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + const dirPath = rootDir + 'tmp/log'; + let files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (let i = 0; i < files.length; i++) { + const filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + const dirPath = rootDir + 'tmp/yunkong2-data/sqlite'; + let files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (let i = 0; i < files.length; i++) { + const filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(isInited => { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + const dataDir = rootDir + 'tmp/' + appName + '-data/'; + + let objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + let isObjectConnected; + let isStatesConnected; + + const Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + type : 'file', + host : '127.0.0.1', + port : 19001, + user : '', + pass : '', + noFileCache: false, + connectTimeout: 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: () => { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + const States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: () => { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setImmediate(() => cb(false)); + } + } else { + adapterStarted = false; + pid.on('exit', (code, signal) => { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', (code, signal) => { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + let timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(() => { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(() => { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + const objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + const id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + const objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + const id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/lib/states.js b/test/lib/states.js new file mode 100644 index 0000000..e94ee52 --- /dev/null +++ b/test/lib/states.js @@ -0,0 +1,708 @@ +'use strict'; +const path = require('path'); +const rootDir = path.normalize(__dirname + '/../../'); +let adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; + +const logger = { + info: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } +}; + +function States(cb, stateChange) { + const that = this; + const _States = require(rootDir + 'tmp/node_modules/yunkong2.js-controller/lib/states'); + let callbackId = 0; + + const options = { + stateChange: (id, state) => stateChange && stateChange(id, state) + }; + + that.namespace = 'test'; + + that.states = new _States({ + connection: { + type : 'file', + host : '127.0.0.1', + port : 19000, + options : { + auth_pass : null, + retry_max_delay : 15000 + } + }, + logger: logger, + change: (id, state) => { + if (!id || typeof id !== 'string') { + console.log('Something is wrong! ' + JSON.stringify(id)); + return; + } + + // Clear cache if accidentally got the message about change (Will work for admin and javascript) + if (id.match(/^system\.user\./) || id.match(/^system\.group\./)) { + that.users = []; + } + + // If someone want to have log messages + if (that.logList && id.match(/\.logging$/)) { + that.logRedirect(state ? state.val : false, id.substring(0, id.length - '.logging'.length)); + } else + if (id === 'log.system.adapter.' + that.namespace) { + that.processLog(state); + } else + // If this is messagebox + if (id === 'messagebox.system.adapter.' + that.namespace && state) { + // Read it from fifo list + that.states.delMessage('system.adapter.' + that.namespace, state._id); + const obj = state; + if (obj) { + // If callback stored for this request + if (obj.callback && + obj.callback.ack && + obj.callback.id && + that.callbacks && + that.callbacks['_' + obj.callback.id]) { + // Call callback function + if (that.callbacks['_' + obj.callback.id].cb) { + that.callbacks['_' + obj.callback.id].cb(obj.message); + delete that.callbacks['_' + obj.callback.id]; + } + // delete too old callbacks IDs, like garbage collector + const now = Date.now(); + for (const _id in that.callbacks) { + if (that.callbacks.hasOwnProperty(_id) && now - that.callbacks[_id].time > 3600000) delete that.callbacks[_id]; + } + + } else { + if (options.message) { + // Else inform about new message the adapter + options.message(obj); + } + that.emit('message', obj); + } + } + } else { + if (id.slice(that.namespace.length) === that.namespace) { + if (typeof options.stateChange === 'function') options.stateChange(id.slice(that.namespace.length + 1), state); + // emit 'stateChange' event instantly + setImmediate(() => that.emit('stateChange', id.slice(that.namespace.length + 1), state)); + + } else { + if (typeof options.stateChange === 'function') options.stateChange(id, state); + if (id.substring(0, 4) === 'log.') { + console.log("LOG"); + } + if (that.emit) { + // emit 'stateChange' event instantly + setImmediate(() => that.emit('stateChange', id, state)); + } + } + } + }, + connectTimeout: (error) => { + if (logger) logger.error(that.namespace + ' no connection to states DB'); + if (cb) cb('Timeout'); + } + }); + + // Send message to other adapter instance or all instances of adapter + that.sendTo = function sendTo(objName, command, message, callback) { + if (typeof message === 'undefined') { + message = command; + command = 'send'; + } + const obj = {command: command, message: message, from: 'system.adapter.' + that.namespace}; + + if (!objName.match(/^system\.adapter\./)) objName = 'system.adapter.' + objName; + + that.log.info('sendTo "' + command + '" to ' + objName + ' from system.adapter.' + that.namespace + ': ' + JSON.stringify(message)); + + // If not specific instance + if (!objName.match(/\.[0-9]+$/)) { + // Send to all instances of adapter + that.objects.getObjectView('system', 'instance', {startkey: objName + '.', endkey: objName + '.\u9999'}, (err, _obj) => { + if (_obj) { + for (let i = 0; i < _obj.rows.length; i++) { + that.states.pushMessage(_obj.rows[i].id, obj); + } + } + }); + } else { + if (callback) { + if (typeof callback === 'function') { + // force subscribe even no messagebox enabled + if (!that.common.messagebox && !that.mboxSubscribed) { + that.mboxSubscribed = true; + that.states.subscribeMessage('system.adapter.' + that.namespace); + } + + obj.callback = { + message: message, + id: callbackId++, + ack: false, + time: Date.now() + }; + if (callbackId >= 0xFFFFFFFF) { + callbackId = 1; + } + if (!that.callbacks) that.callbacks = {}; + that.callbacks['_' + obj.callback.id] = {cb: callback}; + + // delete too old callbacks IDs + const now = Date.now(); + for (const _id in that.callbacks) { + if (that.callbacks.hasOwnProperty(_id) && now - that.callbacks[_id].time > 3600000) { + delete that.callbacks[_id]; + } + } + } else { + obj.callback = callback; + obj.callback.ack = true; + } + } + + that.states.pushMessage(objName, obj); + } + }; + + // Send message to specific host or to all hosts + that.sendToHost = function sendToHost(objName, command, message, callback) { + if (typeof message === 'undefined') { + message = command; + command = 'send'; + } + const obj = {command: command, message: message, from: 'system.adapter.' + that.namespace}; + + if (objName && objName.substring(0, 'system.host.'.length) !== 'system.host.') objName = 'system.host.' + objName; + + if (!objName) { + // Send to all hosts + that.objects.getObjectList({startkey: 'system.host.', endkey: 'system.host.' + '\u9999'}, null, (err, res) => { + if (!err && res.rows.length) { + for (let i = 0; i < res.rows.length; i++) { + const parts = res.rows[i].id.split('.'); + // ignore system.host.name.alive and so on + if (parts.length === 3) { + that.states.pushMessage(res.rows[i].id, obj); + } + } + } + }); + } else { + if (callback) { + if (typeof callback === 'function') { + // force subscribe even no messagebox enabled + if (!that.common.messagebox && !that.mboxSubscribed) { + that.mboxSubscribed = true; + that.states.subscribeMessage('system.adapter.' + that.namespace); + } + + obj.callback = { + message: message, + id: callbackId++, + ack: false, + time: Date.now() + }; + if (callbackId >= 0xFFFFFFFF) callbackId = 1; + if (!that.callbacks) that.callbacks = {}; + that.callbacks['_' + obj.callback.id] = {cb: callback}; + } else { + obj.callback = callback; + obj.callback.ack = true; + } + } + + that.states.pushMessage(objName, obj); + } + }; + + that.setState = function setState(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id, 'state'); + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.setState(id, state, callback); + } + }); + } else { + that.states.setState(id, state, callback); + } + }; + + that.setForeignState = function setForeignState(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.setState(id, state, callback); + } + }); + } else { + that.states.setState(id, state, callback); + } + }; + + that.getState = function getState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id, 'state'); + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'getState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.getState(id, callback); + } + }); + } else { + that.states.getState(id, callback); + } + }; + + that.getStateHistory = function getStateHistory(id, start, end, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id, 'state'); + that.getForeignStateHistory(id, start, end, options, callback); + }; + + that.getForeignStateHistory = function getForeignStateHistory(id, start, end, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (typeof start === 'function') { + callback = start; + start = undefined; + end = undefined; + } else if (typeof end === 'function') { + callback = end; + end = undefined; + } + + start = start || Math.round((new Date()).getTime() / 1000) - 31536000; // - 1 year + end = end || Math.round((new Date()).getTime() / 1000) + 5000; + + const history = []; + const docs = []; + + // get data from states + that.log.debug('get states history ' + id + ' ' + start + ' ' + end); + that.getFifo(id, (err, res) => { + if (!err && res) { + let iProblemCount = 0; + for (let i = 0; i < res.length; i++) { + if (!res[i]) { + iProblemCount++; + continue; + } + if (res[i].ts < start) { + continue; + } else if (res[i].ts > end) { + break; + } + history.push(res[i]); + } + if (iProblemCount) that.log.warn('got null states ' + iProblemCount + ' times for ' + id); + + that.log.debug('got ' + res.length + ' datapoints for ' + id); + } else { + if (err !== 'Not exists') { + that.log.error(err); + } else { + that.log.debug('datapoints for ' + id + ' do not yet exist'); + } + } + + // fetch a history document from objectDB + function getObjectsLog(cid, callback) { + that.log.info('getObjectLog ' + cid); + that.getForeignObject(cid, options, (err, res) => { + if (!err && res.common.data) { + for (let i = 0; i < res.common.data.length; i++) { + if (res.common.data[i].ts < start) { + continue; + } else if (res.common.data[i].ts > end) { + break; + } + history.push(res.common.data[i]); + } + } else { + that.log.warn(cid + ' not found'); + } + callback(err); + }); + } + + // queue objects history documents fetching + function queue(ts) { + if (ts < start) { + callback(null, history); + return; + } + const cid = 'history.' + id + '.' + ts2day(ts); + if (docs.indexOf(cid) !== -1) { + getObjectsLog(cid, err => queue(ts - 86400)); // - 1 day + } else { + queue(ts - 86400); // - 1 day + } + } + + // get list of available history documents + that.objects.getObjectList({startkey: 'history.' + id, endkey: 'history.' + id + '\u9999'}, options, (err, res) => { + if (!err && res.rows.length) { + for (let i = 0; i < res.rows.length; i++) { + docs.push(res.rows[i].id); + } + queue(end); + } else { + callback(null, history); + } + }); + }); + }; + + // normally only foreign history has interest, so there is no getHistory and getForeignHistory + that.getHistory = function getHistory(id, options, callback) { + options = options || {}; + options.end = options.end || Math.round((new Date()).getTime() / 1000) + 5000; + if (!options.count && !options.start) { + options.start = options.start || Math.round((new Date()).getTime() / 1000) - 604800; // - 1 week + } + + if (!options.instance) { + if (!that.defaultHistory) { + // read default history instance from system.config + return getDefaultHistory(() => that.getHistory(id, options, callback)); + } else { + options.instance = that.defaultHistory; + } + } + + that.sendTo(options.instance || 'history.0', 'getHistory', {id: id, options: options}, res => { + setImmediate(() => callback(res.error, res.result, res.step)); + }); + }; + + // Convert ID adapter.instance.device.channel.state + // Convert ID to {device: D, channel: C, state: S} + that.idToDCS = function idToDCS(id) { + if (!id) return null; + const parts = id.split('.'); + if (parts[0] + '.' + parts[1] !== that.namespace) { + that.log.warn("Try to decode id not from this adapter"); + return null; + } + return {device: parts[2], channel: parts[3], state: parts[4]}; + }; + + that.getForeignState = function getForeignState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'getState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.getState(id, callback); + } + }); + } else { + that.states.getState(id, callback); + } + }; + + that.delForeignState = function delForeignState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'delState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.delState(id, callback); + } + }); + } else { + that.states.delState(id, callback); + } + + }; + + that.delState = function delState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + id = that._fixId(id); + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'delState', err => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.delState(id, callback); + } + }); + } else { + that.states.delState(id, callback); + } + }; + + that.getStates = function getStates(pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + pattern = that._fixId(pattern, 'state'); + that.getForeignStates(pattern, options, callback); + }; + + that.getForeignStates = function getForeignStates(pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + const list = {}; + if (typeof pattern === 'function') { + callback = pattern; + pattern = '*'; + } + + if (typeof callback !== 'function') { + logger.error('getForeignStates invalid callback for ' + pattern); + return; + } + + if (typeof pattern === 'object') { + that.states.getStates(pattern, (err, arr) => { + if (err) { + callback(err); + return; + } + for (let i = 0; i < pattern.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[pattern[i]] = arr[i] || {}; + } + callback(null, list); + }); + return; + } + const keys = []; + let params = {}; + if (pattern && pattern !== '*') { + params = { + startkey: pattern.replace('*', ''), + endkey: pattern.replace('*', '\u9999') + }; + } + that.objects.getObjectView('system', 'state', params, options, (err, res) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + for (let i = 0; i < res.rows.length; i++) { + keys.push(res.rows[i].id); + } + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(keys, options, 'getState', (err, keys) => { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + that.states.getStates(keys, function (err, arr) { + if (err) { + callback(err); + return; + } + for (let i = 0; i < res.rows.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[keys[i]] = arr[i] || {}; + } + if (typeof callback === 'function') callback(null, list); + }); + }); + } else { + that.states.getStates(keys, function (err, arr) { + if (err) { + callback(err); + return; + } + for (let i = 0; i < res.rows.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[keys[i]] = arr[i] || {}; + } + if (typeof callback === 'function') callback(null, list); + }); + } + }); + }; + + that.subscribeForeignStates = function subscribeForeignStates(pattern, options) { + if (!pattern) pattern = '*'; + that.states.subscribe(pattern, options); + }; + + that.unsubscribeForeignStates = function unsubscribeForeignStates(pattern, options) { + if (!pattern) pattern = '*'; + that.states.unsubscribe(pattern, options); + }; + + that.subscribeStates = function subscribeStates(pattern, options) { + // Exception. Threat the '*' case automatically + if (!pattern || pattern === '*') { + that.states.subscribe(that.namespace + '.*', options); + } else { + pattern = that._fixId(pattern, 'state'); + that.states.subscribe(pattern, options); + } + }; + + that.unsubscribeStates = function unsubscribeStates(pattern, options) { + if (!pattern || pattern === '*') { + that.states.unsubscribe(that.namespace + '.*', options); + } else { + pattern = that._fixId(pattern, 'state'); + that.states.unsubscribe(pattern, options); + } + }; + + that.pushFifo = function pushFifo(id, state, callback) { + that.states.pushFifo(id, state, callback); + }; + + that.trimFifo = function trimFifo(id, start, end, callback) { + that.states.trimFifo(id, start, end, callback); + }; + + that.getFifoRange = function getFifoRange(id, start, end, callback) { + that.states.getFifoRange(id, start, end, callback); + }; + + that.getFifo = function getFifo(id, callback) { + that.states.getFifo(id, callback); + }; + + that.lenFifo = function lenFifo(id, callback) { + that.states.lenFifo(id, callback); + }; + + that.subscribeFifo = function subscribeFifo(pattern) { + that.states.subscribeFifo(pattern); + }; + + that.getSession = function getSession(id, callback) { + that.states.getSession(id, callback); + }; + that.setSession = function setSession(id, ttl, data, callback) { + that.states.setSession(id, ttl, data, callback); + }; + that.destroySession = function destroySession(id, callback) { + that.states.destroySession(id, callback); + }; + + that.getMessage = function getMessage(callback) { + that.states.getMessage('system.adapter.' + that.namespace, callback); + }; + + that.lenMessage = function lenMessage(callback) { + that.states.lenMessage('system.adapter.' + that.namespace, callback); + }; + + // Write binary block into redis, e.g image + that.setBinaryState = function setBinaryState(id, binary, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + that.states.setBinaryState(id, binary, callback); + }; + + // Read binary block fromredis, e.g. image + that.getBinaryState = function getBinaryState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + that.states.getBinaryState(id, callback); + }; + + logger.debug(that.namespace + ' statesDB connected'); + + if (typeof cb === 'function') { + setImmediate(() => cb(), 0); + } + + return this; +} + +module.exports = States; \ No newline at end of file diff --git a/test/testClient.js b/test/testClient.js new file mode 100644 index 0000000..5220c56 --- /dev/null +++ b/test/testClient.js @@ -0,0 +1,223 @@ +'use strict'; +const expect = require('chai').expect; +const setup = require(__dirname + '/lib/setup'); + +let objects = null; +let states = null; +let MqttServer; +let mqttClient = null; +let mqttServer = null; +let connected = false; +let lastReceivedTopic; +let lastReceivedMessage; + +const rules = { + '/mqtt/0/test1': 'mqtt.0.test1', + 'mqtt/0/test2': 'mqtt.0.test2', + 'test3': 'mqtt.0.test3', + 'te s t4': 'mqtt.0.te_s_t4', + 'system/adapter/admin/upload': 'system.adapter.admin.upload', + '/system/adapter/admin/upload': 'system.adapter.admin.upload' +}; + +function checkMqtt2Adapter(id, _expectedId, _it, _done) { + _it.timeout(1000); + const value = 'Roger' + Math.round(Math.random() * 100); + const mqttid = id; + if (!_expectedId) { + id = id.replace(/\//g, '.').replace(/\s/g, '_'); + if (id[0] === '.') id = id.substring(1); + } else { + id = _expectedId; + } + if (id.indexOf('.') === -1) id = 'mqtt.0.' + id; + + mqttClient.publish(mqttid, value); + + setTimeout(() => { + objects.getObject(id, (err, obj) => { + expect(obj).to.be.not.null.and.not.undefined; + expect(obj._id).to.be.equal(id); + expect(obj.type).to.be.equal('state'); + + if (mqttid.indexOf('mqtt') !== -1) { + expect(obj.native.topic).to.be.equal(mqttid); + } + + states.getState(id, (err, state) => { + expect(state).to.be.not.null.and.not.undefined; + expect(state.val).to.be.equal(value); + expect(state.ack).to.be.true; + _done(); + }); + }); + }, 500); +} + +function checkAdapter2Mqtt(id, mqttid, _it, _done) { + const value = 'NewRoger' + Math.round(Math.random() * 100); + _it.timeout(5000); + + console.log('Send ' + id); + + states.setState(id, { + val: value, + ack: false + }, function (err, id) { + setTimeout(function () { + expect(lastReceivedTopic).to.be.equal(mqttid); + expect(lastReceivedMessage).to.be.equal(value); + _done(); + }, 200); + }); +} + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.mqtt.0.connected', (err, state) => { + if (err) console.error(err); + if (state && state.val) { + connected = state.val; + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 500); + } + }); +} + +function checkConnectionToServer(value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection to server for ' + value); + return; + } + + states.getState('mqtt.0.info.connection', (err, state) => { + if (err) console.error(err); + if (state && state.val == value) { + connected = state.val; + cb && cb(); + } else { + setTimeout(function () { + checkConnectionToServer(value, cb, counter + 1); + }, 1000); + } + }); +} + +describe('Test MQTT client', function() { + before('MQTT client: Start js-controller', function (_done) { // let FUNCTION and not => here + this.timeout(600000); // because of first install from npm + let clientConnected = false; + let brokerStarted = false; + setup.adapterStarted = false; + + setup.setupController(function () { + const config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.publish = 'mqtt.0.*'; + config.native.user = 'user'; + config.native.pass = '*\u0006\u0015\u0001\u0004'; + setup.setAdapterConfig(config.common, config.native); + + setup.startController((_objects, _states) => { + objects = _objects; + states = _states; + brokerStarted = true; + if (_done && clientConnected) { + _done(); + _done = null; + } + }); + }); + + // start mqtt server + MqttServer = require(__dirname + '/lib/mqttServer.js'); + const MqttClient = require(__dirname + '/lib/mqttClient.js'); + + mqttServer = new MqttServer({user: 'user', pass: 'pass1'}); + + // Start client to emit topics + mqttClient = new MqttClient(() => { + // on connected + //console.log('Test MQTT Client is connected to MQTT broker'); + clientConnected = true; + if (_done && brokerStarted && clientConnected) { + _done(); + _done = null; + } + }, function (topic, message) { + console.log((new Date()).getTime() + ' emitter received ' + topic + ': ' + message.toString()); + //console.log('Test MQTT Client received "' + topic + '": ' + message); + // on receive + lastReceivedTopic = topic; + lastReceivedMessage = message ? message.toString() : null; + }, {name: 'Emitter', user: 'user', pass: 'pass1'}); + }); + + it('MQTT client: Check if connected to MQTT broker', done => { + if (!connected) { + checkConnectionOfAdapter(done); + } else { + done(); + } + }).timeout(3000); + + it('MQTT client: wait', done => { + setTimeout(function () { + done(); + }, 1000); + }).timeout(4000); + + for (let rr in rules) { + (function(id, topic) { + it('MQTT client: Check receive ' + id, function (done) { // let FUNCTION here + checkMqtt2Adapter(id, topic, this, done); + }); + })(rr, rules[rr]); + } + + for (let r in rules) { + (function(id, topic) { + if (topic.indexOf('mqtt') !== -1) { + it('MQTT client: Check send ' + topic, function (done) { // let FUNCTION here + checkAdapter2Mqtt(topic, id, this, done); + }); + } + })(r, rules[r]); + } + + it('MQTT client: check reconnect if server is down', done => { + mqttServer.stop(); + connected = false; + + checkConnectionToServer(false, error => { + expect(error).to.be.not.ok; + mqttServer = new MqttServer(); + checkConnectionToServer(true, error => { + expect(error).to.be.not.ok; + done(); + }); + }); + }).timeout(20000); + + after('MQTT client: Stop js-controller', function (_done) { // let FUNCTION and not => here + this.timeout(6000); + mqttServer.stop(); + mqttClient.stop(); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + _done(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..02881f8 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,92 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +'use strict'; +const expect = require('chai').expect; +const fs = require('fs'); + +describe('Test package.json and io-package.json', () => { + it('Test package files', done => { + console.log(); + + const fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + const ioPackage = JSON.parse(fileContentIOPackage); + + const fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + const npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.'); + console.log(); + } + + if (ioPackage.common.name.indexOf('vis-') !== 0) { + if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + console.log('WARNING: Admin3 support is missing! Please add it'); + console.log(); + } + if (ioPackage.common.materialize) { + expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + } + } + + const licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + const fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +}); diff --git a/test/testServer.js b/test/testServer.js new file mode 100644 index 0000000..b3fb15e --- /dev/null +++ b/test/testServer.js @@ -0,0 +1,263 @@ +'use strict'; +const expect = require('chai').expect; +const 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; + +const rules = { + '/mqtt/0/test1': 'mqtt.0.test1', + 'mqtt/0/test2': 'mqtt.0.test2', + 'test3': 'mqtt.0.test3', + 'te s t4': 'mqtt.0.te_s_t4', + 'system/adapter/admin/upload': 'system.adapter.admin.upload', + '/system/adapter/admin/upload': 'system.adapter.admin.upload' +}; + +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((new Date()).getTime() + ' emitter received ' + topic + ': ' + message.toString()); + // on receive + lastReceivedTopic1 = topic; + lastReceivedMessage1 = message ? message.toString() : null; + }, {name: 'Emitter', 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((new Date()).getTime() + ' detector received ' + topic + ': ' + message.toString()); + // on receive + lastReceivedTopic2 = topic; + lastReceivedMessage2 = message ? message.toString() : null; + console.log(JSON.stringify(lastReceivedMessage2)); + }, {name: 'Detector', user: 'user', pass: 'pass1'}); +} + +function checkMqtt2Adapter(id, _expectedId, _it, _done) { + _it.timeout(1000); + const value = 'Roger' + Math.round(Math.random() * 100); + const mqttid = id; + if (!_expectedId) { + id = id.replace(/\//g, '.').replace(/\s/g, '_'); + if (id[0] === '.') id = id.substring(1); + } else { + id = _expectedId; + } + if (id.indexOf('.') === -1) id = 'mqtt.0.' + id; + + lastReceivedMessage1 = null; + lastReceivedTopic1 = null; + lastReceivedTopic2 = null; + lastReceivedMessage2 = null; + + mqttClientEmitter.publish(mqttid, value, function (err) { + expect(err).to.be.undefined; + + setTimeout(() => { + /*expect(lastReceivedTopic2).to.be.equal(mqttid); + expect(lastReceivedMessage2).to.be.equal(value);*/ + + objects.getObject(id, function (err, obj) { + expect(obj).to.be.not.null.and.not.undefined; + expect(obj._id).to.be.equal(id); + expect(obj.type).to.be.equal('state'); + + if (mqttid.indexOf('mqtt') != -1) { + expect(obj.native.topic).to.be.equal(mqttid); + } + + states.getState(id, (err, state) => { + expect(state).to.be.not.null.and.not.undefined; + expect(state.val).to.be.equal(value); + expect(state.ack).to.be.true; + _done(); + }); + }); + }, 100); + }); +} + +function checkAdapter2Mqtt(id, mqttid, _it, _done) { + const value = 'NewRoger' + Math.round(Math.random() * 100); + _it.timeout(5000); + + console.log(new Date().getTime() + ' 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); + _done(); + }, 200); + } else { + expect(lastReceivedTopic1).to.be.equal(mqttid); + expect(lastReceivedMessage1).to.be.equal(value); + _done(); + } + }, 200); + }); +} + +function checkConnection(value, done, counter) { + counter = counter || 0; + if (counter > 20) { + done && done('Cannot check ' + value); + return; + } + + states.getState('mqtt.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('MQTT server: Test mqtt server', () => { + before('MQTT server: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(() => { + const config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.publish = 'mqtt.0.*'; + config.native.type = 'server'; + config.native.user = 'user'; + config.native.pass = '*\u0006\u0015\u0001\u0004'; + 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('MQTT server: Check if connected to MQTT broker', done => { + if (!connected) { + checkConnection(true, done); + } else { + done(); + } + }).timeout(2000); + + for (const r in rules) { + (function(id, topic) { + it('MQTT server: Check receive ' + id, function (done) { // let FUNCTION here + checkMqtt2Adapter(id, topic, this, done); + }); + })(r, rules[r]); + } + + // give time to client to receive all messages + it('wait', done => { + setTimeout(() => { + done(); + }, 2000); + }).timeout(3000); + + for (const r in rules) { + (function(id, topic) { + if (topic.indexOf('mqtt') !== -1) { + it('MQTT server: Check send ' + topic, function (done) { // let FUNCTION here + checkAdapter2Mqtt(topic, id, this, done); + }); + } + })(r, rules[r]); + } + + it('MQTT server: detector must receive /mqtt/0/test1', done => { + const mqttid = '/mqtt/0/test1'; + const value = 'AABB'; + mqttClientEmitter.publish(mqttid, JSON.stringify({val: value, ack: false}), err => { + expect(err).to.be.undefined; + + setTimeout(() => { + expect(lastReceivedTopic2).to.be.equal(mqttid); + expect(lastReceivedMessage2).to.be.equal(value); + done(); + }, 100); + }); + }); + + it('MQTT 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('MQTT server: Stop js-controller', function (done) { + this.timeout(5000); + mqttClientEmitter.stop(); + mqttClientDetector.stop(); + setup.stopController(() => { + done(); + }); + }); +}); diff --git a/test/testSimServer.js b/test/testSimServer.js new file mode 100644 index 0000000..eb5a290 --- /dev/null +++ b/test/testSimServer.js @@ -0,0 +1,300 @@ +'use strict'; +const expect = require('chai').expect; +const Adapter = require('./lib/adapterSim'); +const Server = require('../lib/server'); +const Client = require('./lib/mqttClient'); + +let port = 1883; + +describe('MQTT server', () => { + let adapter; + let server; + let states = {}; + + before('MQTT server: Start MQTT server', done => { + adapter = new Adapter({ + port: ++port, + defaultQoS: 1, + onchange: true + }); + server = new Server(adapter, states); + done(); + }); + + it('MQTT server: Check if connected to MQTT broker', done => { + let client = new Client(isConnected => { + if (done) { + expect(isConnected).to.be.true; + client.destroy(); + done(); + done = null; + } + }, + null, + { + url: 'localhost:' + port, + clientId: 'testClient1', + } + ); + }); + + it('MQTT server: Check if subscribes stored', () => { + let client; + const data = 1; + return new Promise(resolve => { + client = new Client(isConnected => { + if (isConnected) { + client.subscribe('aaa'); + setTimeout(() => client.destroy(), 200); // let time to send it out + } else { + adapter.setForeignState('mqtt.0.aaa', data); + server.onStateChange('mqtt.0.aaa', {val: data, ack: false}); + setTimeout(() => resolve(), 100); + } + }, + null, + { + url: 'localhost:' + port, + clean: false, + clientId: 'testClient2', + resubscribe: false + } + ); + }) + .then(() => { + new Promise(resolve => { + client = new Client( + () => { + + }, + (topic, message) => { + if (topic === 'aaa') { + expect(topic).to.be.equal('aaa'); + expect(message.toString()).to.be.equal(data.toString()); + client.destroy(); + resolve(); + } + }, + { + url: 'localhost:' + port, + clean: false, + clientId: 'testClient2', + resubscribe: false + } + ); + }) + }); + }); + + it('MQTT server: Check if QoS1 retransmitted', done => { + let client; + const data = 1; + let sendPacket; + let count = 0; + const id = 'aaa2'; + let allowPuback = false; + let receiveFunc; + new Promise(resolve => { + client = new Client(isConnected => { + if (isConnected) { + client.subscribe(id, {qos: 1}); + setTimeout(() => resolve(), 100); + } + }, + (topic, data) => receiveFunc && receiveFunc(topic, data), + { + url: 'localhost:' + port, + clean: false, + clientId: 'testClient3', + resubscribe: false + } + ); + sendPacket = client.client._sendPacket; + client.client._sendPacket = function (packet, cb) { + // ignore puback + if (packet.cmd === 'puback' && !allowPuback) { + count++; + cb && cb(); + return; + } + sendPacket.call(this, packet, cb); + }; + }) + .then(() => { + return new Promise(resolve => { + adapter.setForeignState('mqtt.0.' + id, data); + server.onStateChange('mqtt.0.' + id, {val: data, ack: false}); + setTimeout(() => resolve(), 1000); + }); + }) + .then(() => { + console.log(`[${new Date().toISOString()} continue tests`); + expect(count).to.be.equal(1); + allowPuback = true; + receiveFunc = () => { + client.destroy(); + done(); + }; + }); + }).timeout(5000); + + it('MQTT server: Check if QoS2 retransmitted', done => { + let receiverClient; + let emitterClient; + const data = 1; + const id = 'aaa3'; + let sendPacket; + let count = 0; + let allowPubrec = false; + let receiveFunc; + new Promise(resolve => { + receiverClient = new Client(isConnected => { + if (isConnected) { + receiverClient.subscribe(id, {qos: 2}); + setTimeout(() => resolve(), 100); + } + }, + (topic, data) => receiveFunc && receiveFunc(topic, data), + { + url: 'localhost:' + port, + clean: false, + clientId: 'receiverClient', + resubscribe: false + } + ); + emitterClient = new Client(null, null, + { + url: 'localhost:' + port, + clean: true, + clientId: 'emitterClient', + resubscribe: false + } + ); + sendPacket = receiverClient.client._sendPacket; + receiverClient.client._sendPacket = function (packet, cb) { + // ignore pubrec + if (packet.cmd === 'pubrec' && !allowPubrec) { + count++; + cb && cb(); + return; + } + sendPacket.call(this, packet, cb); + }; + }) + .then(() => { + return new Promise(resolve => { + emitterClient.publish(id, data.toString(), 2); // Send QoS 2 + setTimeout(() => resolve(), 100); + }); + }) + .then(() => { + expect(count).to.be.equal(1); + allowPubrec = true; + receiveFunc = () => { + receiverClient.destroy(); + emitterClient.destroy(); + done(); + }; + }); + }).timeout(5000); + + it('MQTT server: Check if message with QoS1 received', done => { + let receiverClient; + let emitterClient; + const data = 1; + const id = 'aaa4'; + let receiveFunc; + new Promise(resolve => { + receiverClient = new Client(isConnected => { + if (isConnected) { + receiverClient.subscribe(id, {qos: 1}); + setTimeout(() => resolve(), 100); + } + }, + (topic, data, packet) => receiveFunc && receiveFunc(topic, data, packet), + { + url: 'localhost:' + port, + clean: false, + clientId: 'receiverClient', + resubscribe: false + } + ); + emitterClient = new Client(null, null, + { + url: 'localhost:' + port, + clean: true, + clientId: 'emitterClient', + resubscribe: false + } + ); + }) + .then(() => { + return new Promise(resolve => { + receiveFunc = (topic, data, packet) => { + expect(data).to.be.ok; + expect(topic).to.be.ok; + expect(packet.qos).to.be.equal(1); + receiverClient.destroy(); + emitterClient.destroy(); + done(); + }; + emitterClient.publish(id, data.toString(), 1); // Send QoS 2 + setTimeout(() => resolve(), 100); + }); + }); + }).timeout(1000); + + // check unsubscribe + it('MQTT server: Check if unsubscribes works', () => { + let client; + const data = 1; + let count = 0; + return new Promise(resolve => { + client = new Client(isConnected => { + if (isConnected) { + client.subscribe('aaa6'); + setTimeout(() => { + adapter.setForeignState('mqtt.0.aaa6', data); + server.onStateChange('mqtt.0.aaa6', {val: data, ack: false}); + }, 500); + } + }, + (id, topic, packet) => { + if (id.indexOf('aaa6') !== -1) { + console.log('Received ' + topic.toString()); + count++; + expect(count).to.be.equal(1); + setTimeout(() => resolve(), 100); + } + }, + { + url: 'localhost:' + port, + clean: true, + clientId: 'testClient6', + resubscribe: false + } + ); + }) + .then(() => { + return new Promise(resolve => { + client.unsubscribe('aaa6'); + client.unsubscribe('#'); + setTimeout(() => { + console.log('Resend data'); + adapter.setForeignState('mqtt.0.aaa6', 2); + server.onStateChange('mqtt.0.aaa6', {val: 2, ack: false}); + // wait 1 second to not receive the update + setTimeout(() => { + console.log('Done'); + client.destroy(); + resolve(); + }, 1000); + }, 300); + }); + }); + }).timeout(3000); + + after('MQTT server: Stop MQTT server', done => { + server.destroy(done); + }); +});