├── .gitignore
├── icons
├── stsmqtt.png
└── stsmqtt_lg.png
├── package.json
├── README.md
├── locales
└── en-US
│ └── sts-mqtt-node.json
├── sts-mqtt-node.js
└── sts-mqtt-node.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/icons/stsmqtt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenseTecnic/node-red-contrib-sts-mqtt/master/icons/stsmqtt.png
--------------------------------------------------------------------------------
/icons/stsmqtt_lg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SenseTecnic/node-red-contrib-sts-mqtt/master/icons/stsmqtt_lg.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-red-contrib-sts-mqtt",
3 | "version": "0.0.4",
4 | "description": "Simplified version of the Node-RED MQTT node for Sense tecnic MQTT platform",
5 | "contributors": [
6 | {
7 | "name": "Michael Qiu"
8 | },
9 | {
10 | "name": "Nick O'Leary"
11 | },
12 | {
13 | "name": "Dave Conway-Jones"
14 | }
15 | ],
16 | "dependencies": {
17 | "mqtt": "1.13.0",
18 | "is-utf8": "0.2.1"
19 | },
20 | "keywords": [
21 | "node-red",
22 | "mqtt",
23 | "sts-mqtt"
24 | ],
25 | "license": "Apache-2.0",
26 | "repository": {
27 | "type": "git",
28 | "url": " "
29 | },
30 | "node-red": {
31 | "nodes": {
32 | "sts-mqtt-node":"sts-mqtt-node.js"
33 | }
34 | },
35 | "maintainers": [
36 | {
37 | "name": "Michael Qiu",
38 | "email": "mqiu@sensetecnic.com"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node-red-contrib-sts-mqtt
2 |
3 | These nodes were modified from the original IBM node-red mqtt nodes to simplify the broker configuration in using along with the Sense Tecnic MQTT(STS-MQTT) platform. Most of the modification was intended to minimize the steps that users need to perform to connect to STS-MQTT, in which aims to provide stable MQTT connections with less connection errors.
4 |
5 | Node: Each connection would require a unique client ID set up on sts-mqtt server
6 |
7 | ### STS-MQTT-in node
8 |
9 | STS-MQTT-in node subscribes to a specified topic on STS-MQTT. If correct user credentials are provided, this node will return any messages from this topic.
10 |
11 | In the Topic field, enter `users/{username}/{topic name}` to subscribe. For example, if user Tom would like to subscribes to topic "temperatureSensor", he would enter `users/tom/temperatureSensor` in the topic field.
12 |
13 | Note: Please ensure that the username, client ID, topic all exist on [STS-MQTT](https://mqtt.sensetecnic.com).
14 |
15 | ### STS-MQTT-out node
16 |
17 | STS-MQTT-out node publishes to a specified topic on STS-MQTT. If correct user credentials are provided, this node will send in `msg.payload` to this topic.
18 |
19 | In the Topic field, enter `users/{username}/{topic name}` to subscribe. For example, if user Tom would like to subscribes to topic "temperatureSensor", he would enter `users/tom/temperatureSensor` in the topic field.
20 |
21 | Note: Please ensure that the username, client ID, topic all exist on [STS-MQTT](https://mqtt.sensetecnic.com).
22 |
--------------------------------------------------------------------------------
/locales/en-US/sts-mqtt-node.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "label": {
4 | "payload": "Payload",
5 | "topic": "Topic",
6 | "name": "Name",
7 | "username": "Username",
8 | "client-key":"Client Key"
9 | },
10 | "status": {
11 | "connected": "connected",
12 | "not-connected": "not connected",
13 | "disconnected": "disconnected",
14 | "connecting": "connecting",
15 | "error": "error",
16 | "ok": "OK"
17 | },
18 | "notification": {
19 | "error": "Error: __message__",
20 | "errors": {
21 | "not-deployed": "node not deployed",
22 | "no-response": "no response from server",
23 | "unexpected": "unexpected error (__status__) __message__"
24 | }
25 | },
26 | "errors": {
27 | "nooverride": "Warning: msg properties can no longer override set node properties. See bit.ly/nr-override-msg-props"
28 | }
29 | },
30 |
31 | "mqtt": {
32 | "label": {
33 | "broker": "Server",
34 | "qos": "QoS",
35 | "clientid": "Client ID",
36 | "port": "Port",
37 | "setup-birth-msg": "Set up Birth Message",
38 | "setup-will-msg": "Set up Will Message",
39 | "use-tls": "Enable secure (SSL/TLS) connection"
40 | },
41 | "tabs-label": {
42 | "connection": "Connection",
43 | "advanced": "Advanced"
44 | },
45 | "placeholder": {
46 | "clientid": "Enter the client ID you have setup",
47 | "will-topic": "",
48 | "birth-topic": ""
49 | },
50 | "state": {
51 | "connected": "Connected to broker: __broker__",
52 | "disconnected": "Disconnected from broker: __broker__",
53 | "connect-failed": "Connection failed to broker: __broker__"
54 | },
55 | "retain": "Retain",
56 | "true": "true",
57 | "false": "false",
58 | "errors": {
59 | "not-defined": "topic not defined",
60 | "missing-config": "missing broker configuration",
61 | "invalid-topic": "Invalid topic specified",
62 | "nonclean-missingclientid": "No client ID set, using clean session"
63 | }
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/sts-mqtt-node.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2013, 2016 IBM Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | module.exports = function(RED) {
18 | "use strict";
19 | var mqtt = require("mqtt");
20 | var isUtf8 = require('is-utf8');
21 |
22 | // Load settings from Node-RED settings file
23 |
24 | var DEFAULT_HOST = "mqtt.sensetecnic.com";
25 | var DEFAULT_PORT = 8883;
26 |
27 | try {
28 | DEFAULT_HOST = RED.settings.sensetecnic.mqtt.host;
29 | } catch (err) {/* fallback to default values */}
30 |
31 | try {
32 | DEFAULT_PORT = RED.settings.sensetecnic.mqtt.port;
33 | } catch (err) {/* fallback to default values */}
34 |
35 | function matchTopic(ts,t) {
36 | if (ts == "#") {
37 | return true;
38 | }
39 | var re = new RegExp("^"+ts.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$");
40 | return re.test(t);
41 | }
42 |
43 | function STSMQTTBrokerNode(n) {
44 | RED.nodes.createNode(this,n);
45 |
46 | // Configuration options passed by Node Red
47 | this.broker = (n.broker === "") ? DEFAULT_HOST: n.broker;
48 | this.port = n.port;
49 |
50 | this.clientid = n.clientid;
51 | this.usetls = n.usetls;
52 | this.verifyservercert = n.verifyservercert;
53 |
54 | this.enableWillMsg = n.enableWillMsg;
55 | this.enableBirthMsg = n.enableBirthMsg;
56 |
57 | // Config node state
58 | this.brokerurl = "";
59 | this.connected = false;
60 | this.connecting = false;
61 | this.closing = false;
62 | this.options = {};
63 | this.queue = [];
64 | this.subscriptions = {};
65 |
66 | if(n.enableBirthMsg && n.birthTopic) {
67 | this.birthMessage = {
68 | topic: n.birthTopic,
69 | payload: n.birthPayload || "",
70 | qos: Number(n.birthQos||0),
71 | retain: n.birthRetain=="true"|| n.birthRetain===true
72 | }
73 | } else {
74 | this.birthMessage = {}
75 | }
76 |
77 | if (this.credentials) {
78 | this.username = this.credentials.user;
79 | this.password = this.credentials.clientKey;
80 | }
81 |
82 | // this.usetls = true;
83 | this.compatmode = true;
84 | this.verifyservercert = false;
85 | this.keepalive = 60;
86 | this.cleansession = true;
87 |
88 | // Create the URL to pass in to the MQTT.js library
89 | if (this.brokerurl === "") {
90 | if (this.usetls) {
91 | this.brokerurl="mqtts://";
92 | } else {
93 | this.brokerurl="mqtt://";
94 | }
95 | if (this.broker !== "") {
96 | this.brokerurl = this.brokerurl+this.broker+":"+this.port;
97 | } else {
98 | this.brokerurl = this.brokerurl+"localhost:1883";
99 | }
100 | }
101 |
102 | // Build options for passing to the MQTT.js API
103 | this.options.clientId = this.clientid;
104 | this.options.username = this.username;
105 | this.options.password = this.password;
106 | this.options.keepalive = this.keepalive;
107 | this.options.clean = this.cleansession;
108 | this.options.reconnectPeriod = RED.settings.mqttReconnectTime||5000;
109 | this.options.protocolId = 'MQIsdp';
110 | this.options.protocolVersion = 3;
111 |
112 | // If there's no rejectUnauthorized already, then this could be an
113 | // old config where this option was provided on the broker node and
114 | // not the tls node
115 | if (typeof this.options.rejectUnauthorized === 'undefined') {
116 | this.options.rejectUnauthorized = (this.verifyservercert == "true" || this.verifyservercert === true);
117 | }
118 |
119 | if (n.enableWillMsg && n.willTopic) {
120 | this.options.will = {
121 | topic: n.willTopic,
122 | payload: n.willPayload || "",
123 | qos: Number(n.willQos||0),
124 | retain: n.willRetain=="true"|| n.willRetain===true
125 | };
126 | }
127 |
128 | // Define functions called by MQTT in and out nodes
129 | var node = this;
130 | this.users = {};
131 |
132 | this.register = function(mqttNode){
133 | node.users[mqttNode.id] = mqttNode;
134 | if (Object.keys(node.users).length === 1) {
135 | node.connect();
136 | }
137 | };
138 |
139 | this.deregister = function(mqttNode,done){
140 | delete node.users[mqttNode.id];
141 | if (node.closing) {
142 | return done();
143 | }
144 | if (Object.keys(node.users).length === 0) {
145 | if (node.client && node.client.connected) {
146 | return node.client.end(done);
147 | } else {
148 | node.client.end();
149 | return done();
150 | }
151 | }
152 | done();
153 | };
154 |
155 | this.connect = function () {
156 | if (!node.connected && !node.connecting) {
157 | node.connecting = true;
158 | node.client = mqtt.connect(node.brokerurl ,node.options);
159 | node.client.setMaxListeners(0);
160 | // Register successful connect or reconnect handler
161 | node.client.on('connect', function () {
162 | node.connecting = false;
163 | node.connected = true;
164 | node.log(RED._("mqtt.state.connected",{broker:(node.clientid?node.clientid+"@":"")+node.brokerurl}));
165 | for (var id in node.users) {
166 | if (node.users.hasOwnProperty(id)) {
167 | node.users[id].status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
168 | }
169 | }
170 | // Remove any existing listeners before resubscribing to avoid duplicates in the event of a re-connection
171 | node.client.removeAllListeners('message');
172 |
173 | // Re-subscribe to stored topics
174 | for (var s in node.subscriptions) {
175 | if (node.subscriptions.hasOwnProperty(s)) {
176 | var topic = s;
177 | var qos = 0;
178 | for (var r in node.subscriptions[s]) {
179 | if (node.subscriptions[s].hasOwnProperty(r)) {
180 | qos = Math.max(qos,node.subscriptions[s][r].qos);
181 | node.client.on('message',node.subscriptions[s][r].handler);
182 | }
183 | }
184 | var options = {qos: qos};
185 | node.client.subscribe(topic, options);
186 | }
187 | }
188 |
189 | // Send any birth message
190 | if (node.enableBirthMsg && node.birthMessage) {
191 | node.publish(node.birthMessage);
192 | }
193 | });
194 | node.client.on("reconnect", function() {
195 | for (var id in node.users) {
196 | if (node.users.hasOwnProperty(id)) {
197 | node.users[id].status({fill:"yellow",shape:"ring",text:"node-red:common.status.connecting"});
198 | }
199 | }
200 | })
201 | // Register disconnect handlers
202 | node.client.on('close', function () {
203 | if (node.connected) {
204 | node.connected = false;
205 | node.log(RED._("mqtt.state.disconnected",{broker:(node.clientid?node.clientid+"@":"")+node.brokerurl}));
206 | for (var id in node.users) {
207 | if (node.users.hasOwnProperty(id)) {
208 | node.users[id].status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
209 | }
210 | }
211 | } else if (node.connecting) {
212 | node.log(RED._("mqtt.state.connect-failed",{broker:(node.clientid?node.clientid+"@":"")+node.brokerurl}));
213 | }
214 | });
215 |
216 | // Register connect error handler
217 | node.client.on('error', function (error) {
218 | if (node.connecting) {
219 | node.client.end();
220 | node.connecting = false;
221 | }
222 | });
223 | }
224 | };
225 |
226 | this.subscribe = function (topic,qos,callback,ref) {
227 | ref = ref||0;
228 | node.subscriptions[topic] = node.subscriptions[topic]||{};
229 | var sub = {
230 | topic:topic,
231 | qos:qos,
232 | handler:function(mtopic,mpayload, mpacket) {
233 | if (matchTopic(topic,mtopic)) {
234 | callback(mtopic,mpayload, mpacket);
235 | }
236 | },
237 | ref: ref
238 | };
239 | node.subscriptions[topic][ref] = sub;
240 | if (node.connected) {
241 | node.client.on('message',sub.handler);
242 | var options = {};
243 | options.qos = qos;
244 | node.client.subscribe(topic, options);
245 | }
246 | };
247 |
248 | this.unsubscribe = function (topic, ref) {
249 | ref = ref||0;
250 | var sub = node.subscriptions[topic];
251 | if (sub) {
252 | if (sub[ref]) {
253 | node.client.removeListener('message',sub[ref].handler);
254 | delete sub[ref];
255 | }
256 | if (Object.keys(sub).length === 0) {
257 | delete node.subscriptions[topic];
258 | if (node.connected){
259 | node.client.unsubscribe(topic);
260 | }
261 | }
262 | }
263 | };
264 |
265 | this.publish = function (msg) {
266 | if (node.connected) {
267 | if (!Buffer.isBuffer(msg.payload)) {
268 | if (typeof msg.payload === "object") {
269 | msg.payload = JSON.stringify(msg.payload);
270 | } else if (typeof msg.payload !== "string") {
271 | msg.payload = "" + msg.payload;
272 | }
273 | }
274 |
275 | var options = {
276 | qos: msg.qos || 0,
277 | retain: msg.retain || false
278 | };
279 | node.client.publish(msg.topic, msg.payload, options, function (err){return});
280 | }
281 | };
282 |
283 | this.on('close', function(done) {
284 | this.closing = true;
285 | if (this.connected) {
286 | this.client.once('close', function() {
287 | done();
288 | });
289 | this.client.end();
290 | } else if (this.connecting) {
291 | node.client.end();
292 | done();
293 | } else {
294 | done();
295 | }
296 | });
297 |
298 | }
299 |
300 | RED.nodes.registerType("sts-mqtt-broker", STSMQTTBrokerNode,{
301 | credentials: {
302 | user: {type:"text"},
303 | clientKey: {type: "password"}
304 | }
305 | });
306 |
307 | function STSMQTTInNode(n) {
308 | RED.nodes.createNode(this,n);
309 | this.topic = n.topic;
310 | this.qos = parseInt(n.qos);
311 | if (isNaN(this.qos) || this.qos < 0 || this.qos > 2) {
312 | this.qos = 2;
313 | }
314 | this.broker = n.broker;
315 | this.brokerConn = RED.nodes.getNode(this.broker);
316 | if (!/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(this.topic)) {
317 | return this.warn(RED._("mqtt.errors.invalid-topic"));
318 | }
319 | var node = this;
320 | if (this.brokerConn) {
321 | this.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
322 | if (this.topic) {
323 | node.brokerConn.register(this);
324 | this.brokerConn.subscribe(this.topic,this.qos,function(topic,payload,packet) {
325 | if (isUtf8(payload)) { payload = payload.toString(); }
326 | try {
327 | var msg = {topic:topic,payload:JSON.parse(payload), qos: packet.qos, retain: packet.retain};
328 | } catch(e) {
329 | var msg = {topic:topic,payload:payload, qos: packet.qos, retain: packet.retain};
330 | }
331 | if ((node.brokerConn.broker === "localhost")||(node.brokerConn.broker === "127.0.0.1")) {
332 | msg._topic = topic;
333 | }
334 | node.send(msg);
335 | }, this.id);
336 | if (this.brokerConn.connected) {
337 | node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
338 | }
339 | }
340 | else {
341 | this.error(RED._("mqtt.errors.not-defined"));
342 | }
343 | this.on('close', function(done) {
344 | if (node.brokerConn) {
345 | node.brokerConn.unsubscribe(node.topic,node.id);
346 | node.brokerConn.deregister(node,done);
347 | }
348 | });
349 | } else {
350 | this.error(RED._("mqtt.errors.missing-config"));
351 | }
352 | }
353 | RED.nodes.registerType("sts-mqtt-in",STSMQTTInNode);
354 |
355 | function STSMQTTOutNode(n) {
356 | RED.nodes.createNode(this,n);
357 | this.topic = n.topic;
358 | this.qos = n.qos || null;
359 | this.retain = n.retain;
360 | this.broker = n.broker;
361 | this.brokerConn = RED.nodes.getNode(this.broker);
362 | var node = this;
363 |
364 | if (this.brokerConn) {
365 | this.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
366 | this.on("input",function(msg) {
367 | if (msg.qos) {
368 | msg.qos = parseInt(msg.qos);
369 | if ((msg.qos !== 0) && (msg.qos !== 1) && (msg.qos !== 2)) {
370 | msg.qos = null;
371 | }
372 | }
373 | msg.qos = Number(node.qos || msg.qos || 0);
374 | msg.retain = node.retain || msg.retain || false;
375 | msg.retain = ((msg.retain === true) || (msg.retain === "true")) || false;
376 | if (node.topic) {
377 | msg.topic = node.topic;
378 | }
379 | if ( msg.hasOwnProperty("payload")) {
380 | if (msg.hasOwnProperty("topic") && (typeof msg.topic === "string") && (msg.topic !== "")) { // topic must exist
381 | this.brokerConn.publish(msg); // send the message
382 | }
383 | else { node.warn(RED._("mqtt.errors.invalid-topic")); }
384 | }
385 | });
386 | if (this.brokerConn.connected) {
387 | node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
388 | }
389 | node.brokerConn.register(node);
390 | this.on('close', function(done) {
391 | node.brokerConn.deregister(node,done);
392 | });
393 | } else {
394 | this.error(RED._("mqtt.errors.missing-config"));
395 | }
396 | }
397 | RED.nodes.registerType("sts-mqtt-out",STSMQTTOutNode);
398 |
399 | RED.httpAdmin.get('/defaultMqttServer', function(req, res) {
400 | res.send(JSON.stringify({
401 | server: DEFAULT_HOST,
402 | port: DEFAULT_PORT
403 | }));
404 | });
405 |
406 | };
407 |
--------------------------------------------------------------------------------
/sts-mqtt-node.html:
--------------------------------------------------------------------------------
1 |
13 |
14 |
36 |
37 |
48 |
49 |
75 |
76 |
104 |
105 |
116 |
117 |
140 |
141 |
246 |
247 |
252 |
253 |
352 |
--------------------------------------------------------------------------------