├── .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 | --------------------------------------------------------------------------------