@flowfuse/nr-mqtt-nodes 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +21 -0
- package/.github/dependabot.yml +15 -0
- package/.github/workflows/project-automation.yml +10 -0
- package/.github/workflows/publish.yml +51 -0
- package/.github/workflows/release-publish.yml +21 -0
- package/CHANGELOG.md +3 -0
- package/LICENSE +178 -0
- package/README.md +29 -0
- package/nodes/ff-mqtt.html +488 -0
- package/nodes/ff-mqtt.js +1826 -0
- package/nodes/icons/ff-logo.svg +6 -0
- package/nodes/lib/TeamBrokerApi.js +83 -0
- package/nodes/lib/proxyHelper.js +301 -0
- package/nodes/lib/util.js +112 -0
- package/nodes/locales/en-US/ff-mqtt.html +121 -0
- package/nodes/locales/en-US/ff-mqtt.json +136 -0
- package/npmignore +5 -0
- package/package.json +54 -0
- package/test/unit/ff-mqtt_spec.js +920 -0
package/nodes/ff-mqtt.js
ADDED
|
@@ -0,0 +1,1826 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is a fork of the MQTT nodes from Node-RED.
|
|
3
|
+
The original code can be found at:
|
|
4
|
+
https://github.com/node-red/node-red/blob/abceb1185bc81e869c11918bed3fb17e0bc83dd5/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js
|
|
5
|
+
Below is the copyright notice for the original code.
|
|
6
|
+
The copyright notice for this fork is the same as the original.
|
|
7
|
+
### Changes:
|
|
8
|
+
- Hide advanced features
|
|
9
|
+
- Remove the config node for MQTT broker
|
|
10
|
+
- Remove the dynamic connection control
|
|
11
|
+
- lint errors fixed up
|
|
12
|
+
*/
|
|
13
|
+
/* eslint-disable brace-style */
|
|
14
|
+
const ProxyHelper = require('./lib/proxyHelper.js')
|
|
15
|
+
const { createNrMqttId, createNrMqttClientId } = require('./lib/util.js')
|
|
16
|
+
const TeamBrokerApi = require('./lib/TeamBrokerApi.js')
|
|
17
|
+
const os = require('os')
|
|
18
|
+
const Got = require('got').default
|
|
19
|
+
|
|
20
|
+
module.exports = function (RED) {
|
|
21
|
+
'use strict'
|
|
22
|
+
const forgeSettings = RED.settings.flowforge || {}
|
|
23
|
+
const mqttSettings = forgeSettings.mqttNodes || {}
|
|
24
|
+
const featureEnabled = !!mqttSettings.featureEnabled
|
|
25
|
+
const teamId = forgeSettings.teamID
|
|
26
|
+
const deviceId = forgeSettings.deviceId || ''
|
|
27
|
+
const projectId = forgeSettings.instanceID || forgeSettings.projectID || ''
|
|
28
|
+
// ensure settings are present
|
|
29
|
+
if (!teamId || (!deviceId && !projectId)) {
|
|
30
|
+
throw new Error('FlowFuse MQTT nodes cannot be loaded outside of an FlowFuse EE environment')
|
|
31
|
+
}
|
|
32
|
+
const HA_INSTANCE = !!deviceId && mqttSettings?.ha >= 2
|
|
33
|
+
// It is not unreasonable to expect `projectID` and `applicationID` are set for an instance
|
|
34
|
+
// owned device, however an application owned device should not have a projectID.
|
|
35
|
+
// therefore, assume project owned if `projectID` is set
|
|
36
|
+
// eslint-disable-next-line no-unused-vars
|
|
37
|
+
const DEVICE_OWNER_TYPE = deviceId ? (forgeSettings.projectID ? 'instance' : 'application') : null
|
|
38
|
+
// Generate a unique ID based on the hostname
|
|
39
|
+
// This is then used when in HA/sharedSubscription mode to ensure the instance has
|
|
40
|
+
// a unique clientId that is stable across restarts
|
|
41
|
+
const haInstanceId = HA_INSTANCE ? crypto.createHash('md5').update(os.hostname()).digest('hex').substring(0, 4) : null
|
|
42
|
+
|
|
43
|
+
let instanceType = ''
|
|
44
|
+
let instanceId = ''
|
|
45
|
+
if (deviceId) {
|
|
46
|
+
instanceType = 'device'
|
|
47
|
+
instanceId = deviceId
|
|
48
|
+
} else if (projectId) {
|
|
49
|
+
instanceType = 'instance'
|
|
50
|
+
instanceId = projectId
|
|
51
|
+
} else {
|
|
52
|
+
// throw error?
|
|
53
|
+
throw new Error('FlowFuse MQTT nodes cannot be loaded due to missing instance information')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TEAM_CLIENT_AUTH_ID = createNrMqttId(forgeSettings.teamID, instanceType, instanceId) // e.g., 'mq:remote:teamId:deviceId' or 'mq:hosted:teamId:projectId:haId'
|
|
57
|
+
const TEAM_CLIENT_CLIENT_ID = createNrMqttClientId(forgeSettings.teamID, instanceType, instanceId, haInstanceId) // e.g., 'mq:remote:deviceId' or 'mq:hosted:projectId:haId'
|
|
58
|
+
const ALLOW_DYNAMIC_CONNECT_OPTIONS = false // disable dynamic connect options for this type of node (fixed broker connection)
|
|
59
|
+
const got = Got.extend({
|
|
60
|
+
agent: ProxyHelper.getHTTPProxyAgent(forgeSettings.forgeURL, { timeout: 4000 })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** @type {MQTTBrokerNode} */
|
|
64
|
+
const sharedBroker = new MQTTBrokerNode(mqttSettings)
|
|
65
|
+
|
|
66
|
+
/* Monitor link status and attept to relink if node has users but is unlinked */
|
|
67
|
+
let linkTryCount = 0
|
|
68
|
+
const MAX_LINK_ATTEMPTS = 5
|
|
69
|
+
sharedBroker.linkMonitorInterval = setInterval(async function () {
|
|
70
|
+
if (Object.keys(sharedBroker.users).length < 1) {
|
|
71
|
+
return // no users registered (yet)
|
|
72
|
+
}
|
|
73
|
+
if (sharedBroker.linked && !sharedBroker.linkFailed) {
|
|
74
|
+
clearInterval(sharedBroker.linkMonitorInterval)
|
|
75
|
+
sharedBroker.linkMonitorInterval = null
|
|
76
|
+
}
|
|
77
|
+
if (sharedBroker.linkFailed) {
|
|
78
|
+
try {
|
|
79
|
+
linkTryCount++
|
|
80
|
+
await sharedBroker.link()
|
|
81
|
+
linkTryCount = 0
|
|
82
|
+
} catch (_err) {
|
|
83
|
+
if (linkTryCount >= MAX_LINK_ATTEMPTS) {
|
|
84
|
+
clearInterval(sharedBroker.linkMonitorInterval)
|
|
85
|
+
sharedBroker.linkMonitorInterval = null
|
|
86
|
+
sharedBroker.warn('Maximum Failed Link Attempts. Restart or redeploy to re-establish the connection.')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, (Math.floor(Math.random() * 10000) + 55000)) // 55-65 seconds
|
|
91
|
+
|
|
92
|
+
const mqtt = require('mqtt')
|
|
93
|
+
const isUtf8 = require('is-utf8')
|
|
94
|
+
|
|
95
|
+
const knownMediaTypes = {
|
|
96
|
+
'text/css': 'string',
|
|
97
|
+
'text/html': 'string',
|
|
98
|
+
'text/plain': 'string',
|
|
99
|
+
'application/json': 'json',
|
|
100
|
+
'application/octet-stream': 'buffer',
|
|
101
|
+
'application/pdf': 'buffer',
|
|
102
|
+
'application/x-gtar': 'buffer',
|
|
103
|
+
'application/x-gzip': 'buffer',
|
|
104
|
+
'application/x-tar': 'buffer',
|
|
105
|
+
'application/xml': 'string',
|
|
106
|
+
'application/zip': 'buffer',
|
|
107
|
+
'audio/aac': 'buffer',
|
|
108
|
+
'audio/ac3': 'buffer',
|
|
109
|
+
'audio/basic': 'buffer',
|
|
110
|
+
'audio/mp4': 'buffer',
|
|
111
|
+
'audio/ogg': 'buffer',
|
|
112
|
+
'image/bmp': 'buffer',
|
|
113
|
+
'image/gif': 'buffer',
|
|
114
|
+
'image/jpeg': 'buffer',
|
|
115
|
+
'image/tiff': 'buffer',
|
|
116
|
+
'image/png': 'buffer'
|
|
117
|
+
}
|
|
118
|
+
// #region "Supporting functions"
|
|
119
|
+
function matchTopic (ts, t) {
|
|
120
|
+
if (ts === '#') {
|
|
121
|
+
return true
|
|
122
|
+
} else if (ts.startsWith('$share')) {
|
|
123
|
+
/* The following allows shared subscriptions (as in MQTT v5)
|
|
124
|
+
http://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html#_Toc514345522
|
|
125
|
+
|
|
126
|
+
4.8.2 describes shares like:
|
|
127
|
+
$share/{ShareName}/{filter}
|
|
128
|
+
$share is a literal string that marks the Topic Filter as being a Shared Subscription Topic Filter.
|
|
129
|
+
{ShareName} is a character string that does not include "/", "+" or "#"
|
|
130
|
+
{filter} The remainder of the string has the same syntax and semantics as a Topic Filter in a non-shared subscription. Refer to section 4.7.
|
|
131
|
+
*/
|
|
132
|
+
ts = ts.replace(/^\$share\/[^#+/]+\/(.*)/g, '$1')
|
|
133
|
+
}
|
|
134
|
+
// eslint-disable-next-line no-useless-escape
|
|
135
|
+
const re = new RegExp('^' + ts.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g, '\\$1').replace(/\+/g, '[^/]+').replace(/\/#$/, '(\/.*)?') + '$')
|
|
136
|
+
return re.test(t)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Helper function for setting integer property values in the MQTT V5 properties object
|
|
141
|
+
* @param {object} src Source object containing properties
|
|
142
|
+
* @param {object} dst Destination object to set/add properties
|
|
143
|
+
* @param {string} propName The property name to set in the Destination object
|
|
144
|
+
* @param {integer} [minVal] The minimum value. If the src value is less than minVal, it will NOT be set in the destination
|
|
145
|
+
* @param {integer} [maxVal] The maximum value. If the src value is greater than maxVal, it will NOT be set in the destination
|
|
146
|
+
* @param {integer} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
|
|
147
|
+
*/
|
|
148
|
+
function setIntProp (src, dst, propName, minVal, maxVal, def) {
|
|
149
|
+
if (hasProperty(src, propName)) {
|
|
150
|
+
const v = parseInt(src[propName])
|
|
151
|
+
if (isNaN(v)) return
|
|
152
|
+
if (minVal != null) {
|
|
153
|
+
if (v < minVal) return
|
|
154
|
+
}
|
|
155
|
+
if (maxVal != null) {
|
|
156
|
+
if (v > maxVal) return
|
|
157
|
+
}
|
|
158
|
+
dst[propName] = v
|
|
159
|
+
} else {
|
|
160
|
+
if (typeof def !== 'undefined') dst[propName] = def
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Test a topic string is valid for subscription
|
|
166
|
+
* @param {string} topic
|
|
167
|
+
* @returns `true` if it is a valid topic
|
|
168
|
+
*/
|
|
169
|
+
function isValidSubscriptionTopic (topic) {
|
|
170
|
+
return /^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(topic)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Test a topic string is valid for publishing
|
|
175
|
+
* @param {string} topic
|
|
176
|
+
* @returns `true` if it is a valid topic
|
|
177
|
+
*/
|
|
178
|
+
function isValidPublishTopic (topic) {
|
|
179
|
+
if (topic.length === 0) return false
|
|
180
|
+
// eslint-disable-next-line no-useless-escape
|
|
181
|
+
return !/[\+#\b\f\n\r\t\v\0]/.test(topic)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Helper function for setting string property values in the MQTT V5 properties object
|
|
186
|
+
* @param {object} src Source object containing properties
|
|
187
|
+
* @param {object} dst Destination object to set/add properties
|
|
188
|
+
* @param {string} propName The property name to set in the Destination object
|
|
189
|
+
* @param {string} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
|
|
190
|
+
*/
|
|
191
|
+
function setStrProp (src, dst, propName, def) {
|
|
192
|
+
if (src[propName] && typeof src[propName] === 'string') {
|
|
193
|
+
dst[propName] = src[propName]
|
|
194
|
+
} else {
|
|
195
|
+
if (typeof def !== 'undefined') dst[propName] = def
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Helper function for setting boolean property values in the MQTT V5 properties object
|
|
201
|
+
* @param {object} src Source object containing properties
|
|
202
|
+
* @param {object} dst Destination object to set/add properties
|
|
203
|
+
* @param {string} propName The property name to set in the Destination object
|
|
204
|
+
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
|
|
205
|
+
*/
|
|
206
|
+
function setBoolProp (src, dst, propName, def) {
|
|
207
|
+
if (src[propName] != null) {
|
|
208
|
+
if (src[propName] === 'true' || src[propName] === true) {
|
|
209
|
+
dst[propName] = true
|
|
210
|
+
} else if (src[propName] === 'false' || src[propName] === false) {
|
|
211
|
+
dst[propName] = false
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
if (typeof def !== 'undefined') dst[propName] = def
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Helper function for copying the MQTT v5 srcUserProperties object (parameter1) to the properties object (parameter2).
|
|
220
|
+
* Any property in srcUserProperties that is NOT a key/string pair will be silently discarded.
|
|
221
|
+
* NOTE: if no sutable properties are present, the userProperties object will NOT be added to the properties object
|
|
222
|
+
* @param {object} srcUserProperties An object with key/value string pairs
|
|
223
|
+
* @param {object} properties A properties object in which userProperties will be copied to
|
|
224
|
+
*/
|
|
225
|
+
function setUserProperties (srcUserProperties, properties) {
|
|
226
|
+
if (srcUserProperties && typeof srcUserProperties === 'object') {
|
|
227
|
+
const _clone = {}
|
|
228
|
+
let count = 0
|
|
229
|
+
const keys = Object.keys(srcUserProperties)
|
|
230
|
+
if (!keys || !keys.length) return null
|
|
231
|
+
keys.forEach(key => {
|
|
232
|
+
const val = srcUserProperties[key]
|
|
233
|
+
if (typeof val === 'string') {
|
|
234
|
+
count++
|
|
235
|
+
_clone[key] = val
|
|
236
|
+
} else if (val !== undefined && val !== null) {
|
|
237
|
+
try {
|
|
238
|
+
_clone[key] = JSON.stringify(val)
|
|
239
|
+
count++
|
|
240
|
+
} catch (err) {
|
|
241
|
+
// Silently drop property
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
if (count) properties.userProperties = _clone
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Helper function for copying the MQTT v5 buffer type properties
|
|
251
|
+
* NOTE: if src[propName] is not a buffer, dst[propName] will NOT be assigned a value (unless def is set)
|
|
252
|
+
* @param {object} src Source object containing properties
|
|
253
|
+
* @param {object} dst Destination object to set/add properties
|
|
254
|
+
* @param {string} propName The property name to set in the Destination object
|
|
255
|
+
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the Source object
|
|
256
|
+
*/
|
|
257
|
+
function setBufferProp (src, dst, propName, def) {
|
|
258
|
+
if (!dst) return
|
|
259
|
+
if (src && dst) {
|
|
260
|
+
const buf = src[propName]
|
|
261
|
+
if (buf && typeof Buffer.isBuffer(buf)) {
|
|
262
|
+
dst[propName] = Buffer.from(buf)
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
if (typeof def !== 'undefined') dst[propName] = def
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Helper function for applying changes to an objects properties ONLY when the src object actually has the property.
|
|
271
|
+
* This avoids setting a `dst` property null/undefined when the `src` object doesnt have the named property.
|
|
272
|
+
* @param {object} src Source object containing properties
|
|
273
|
+
* @param {object} dst Destination object to set property
|
|
274
|
+
* @param {string} propName The property name to set in the Destination object
|
|
275
|
+
* @param {boolean} force force the dst property to be updated/created even if src property is empty
|
|
276
|
+
*/
|
|
277
|
+
function setIfHasProperty (src, dst, propName, force) {
|
|
278
|
+
if (src && dst && propName) {
|
|
279
|
+
const ok = force || hasProperty(src, propName)
|
|
280
|
+
if (ok) {
|
|
281
|
+
dst[propName] = src[propName]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Helper function to test an object has a property
|
|
288
|
+
* @param {object} obj Object to test
|
|
289
|
+
* @param {string} propName Name of property to find
|
|
290
|
+
* @returns true if object has property `propName`
|
|
291
|
+
*/
|
|
292
|
+
function hasProperty (obj, propName) {
|
|
293
|
+
// JavaScript does not protect the property name hasOwnProperty
|
|
294
|
+
// Object.prototype.hasOwnProperty.call is the recommended/safer test
|
|
295
|
+
return Object.prototype.hasOwnProperty.call(obj, propName)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Handle the payload / packet recieved in MQTT In and MQTT Sub nodes
|
|
300
|
+
*/
|
|
301
|
+
function subscriptionHandler (node, datatype, topic, payload, packet) {
|
|
302
|
+
const msg = { topic, payload: null, qos: packet.qos, retain: packet.retain }
|
|
303
|
+
const v5 = (node && node.brokerConn)
|
|
304
|
+
? node.brokerConn.v5()
|
|
305
|
+
: Object.prototype.hasOwnProperty.call(packet, 'properties')
|
|
306
|
+
if (v5 && packet.properties) {
|
|
307
|
+
setStrProp(packet.properties, msg, 'responseTopic')
|
|
308
|
+
setBufferProp(packet.properties, msg, 'correlationData')
|
|
309
|
+
setStrProp(packet.properties, msg, 'contentType')
|
|
310
|
+
setIntProp(packet.properties, msg, 'messageExpiryInterval', 0)
|
|
311
|
+
setBoolProp(packet.properties, msg, 'payloadFormatIndicator')
|
|
312
|
+
setStrProp(packet.properties, msg, 'reasonString')
|
|
313
|
+
setUserProperties(packet.properties.userProperties, msg)
|
|
314
|
+
}
|
|
315
|
+
const v5isUtf8 = v5 ? msg.payloadFormatIndicator === true : null
|
|
316
|
+
const v5HasMediaType = v5 ? !!msg.contentType : null
|
|
317
|
+
const v5MediaTypeLC = v5 ? (msg.contentType + '').toLowerCase() : null
|
|
318
|
+
|
|
319
|
+
if (datatype === 'buffer') {
|
|
320
|
+
// payload = payload;
|
|
321
|
+
} else if (datatype === 'base64') {
|
|
322
|
+
payload = payload.toString('base64')
|
|
323
|
+
} else if (datatype === 'utf8') {
|
|
324
|
+
payload = payload.toString('utf8')
|
|
325
|
+
} else if (datatype === 'json') {
|
|
326
|
+
if (v5isUtf8 || isUtf8(payload)) {
|
|
327
|
+
try {
|
|
328
|
+
payload = JSON.parse(payload.toString())
|
|
329
|
+
} catch (e) {
|
|
330
|
+
node.error(RED._('ff-mqtt.errors.invalid-json-parse'), { payload, topic, qos: packet.qos, retain: packet.retain }); return
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
node.error((RED._('ff-mqtt.errors.invalid-json-string')), { payload, topic, qos: packet.qos, retain: packet.retain }); return
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// "auto" (legacy) or "auto-detect" (new default)
|
|
337
|
+
if (v5isUtf8 || v5HasMediaType) {
|
|
338
|
+
const outputType = knownMediaTypes[v5MediaTypeLC]
|
|
339
|
+
switch (outputType) {
|
|
340
|
+
case 'string':
|
|
341
|
+
payload = payload.toString()
|
|
342
|
+
break
|
|
343
|
+
case 'buffer':
|
|
344
|
+
// no change
|
|
345
|
+
break
|
|
346
|
+
case 'json':
|
|
347
|
+
try {
|
|
348
|
+
// since v5 type states this should be JSON, parse it & error out if NOT JSON
|
|
349
|
+
payload = payload.toString()
|
|
350
|
+
const obj = JSON.parse(payload)
|
|
351
|
+
if (datatype === 'auto-detect') {
|
|
352
|
+
payload = obj // as mode is "auto-detect", return the parsed JSON
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
node.error(RED._('ff-mqtt.errors.invalid-json-parse'), { payload, topic, qos: packet.qos, retain: packet.retain }); return
|
|
356
|
+
}
|
|
357
|
+
break
|
|
358
|
+
default:
|
|
359
|
+
if (v5isUtf8 || isUtf8(payload)) {
|
|
360
|
+
payload = payload.toString() // auto String
|
|
361
|
+
if (datatype === 'auto-detect') {
|
|
362
|
+
try {
|
|
363
|
+
payload = JSON.parse(payload) // auto to parsed object (attempt)
|
|
364
|
+
} catch (e) {
|
|
365
|
+
/* mute error - it simply isnt JSON, just leave payload as a string */
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
break
|
|
370
|
+
}
|
|
371
|
+
} else if (isUtf8(payload)) {
|
|
372
|
+
payload = payload.toString() // auto String
|
|
373
|
+
if (datatype === 'auto-detect') {
|
|
374
|
+
try {
|
|
375
|
+
payload = JSON.parse(payload)
|
|
376
|
+
} catch (e) {
|
|
377
|
+
/* mute error - it simply isnt JSON, just leave payload as a string */
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} // else {
|
|
381
|
+
// leave as buffer
|
|
382
|
+
// }
|
|
383
|
+
}
|
|
384
|
+
msg.payload = payload
|
|
385
|
+
if (node.brokerConn && (node.brokerConn.broker === 'localhost' || node.brokerConn.broker === '127.0.0.1')) {
|
|
386
|
+
msg._topic = topic
|
|
387
|
+
}
|
|
388
|
+
node.send(msg)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Send an mqtt message to broker
|
|
393
|
+
* @param {MQTTOutNode} node the owner node
|
|
394
|
+
* @param {object} msg The msg to prepare for publishing
|
|
395
|
+
* @param {function} done callback when done
|
|
396
|
+
*/
|
|
397
|
+
function doPublish (node, msg, done) {
|
|
398
|
+
try {
|
|
399
|
+
done = typeof done === 'function' ? done : function noop () {}
|
|
400
|
+
const v5 = node.brokerConn.options && +node.brokerConn.options.protocolVersion === 5
|
|
401
|
+
|
|
402
|
+
// Sanitise the `msg` object properties ready for publishing
|
|
403
|
+
if (msg.qos) {
|
|
404
|
+
msg.qos = parseInt(msg.qos)
|
|
405
|
+
if ((msg.qos !== 0) && (msg.qos !== 1) && (msg.qos !== 2)) {
|
|
406
|
+
msg.qos = null
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* If node properties exists, override/set that to property in msg */
|
|
411
|
+
if (node.topic) { msg.topic = node.topic }
|
|
412
|
+
msg.qos = Number(node.qos || msg.qos || 0)
|
|
413
|
+
msg.retain = node.retain || msg.retain || false
|
|
414
|
+
msg.retain = ((msg.retain === true) || (msg.retain === 'true')) || false
|
|
415
|
+
|
|
416
|
+
if (v5) {
|
|
417
|
+
if (node.userProperties) {
|
|
418
|
+
msg.userProperties = node.userProperties
|
|
419
|
+
}
|
|
420
|
+
if (node.responseTopic) {
|
|
421
|
+
msg.responseTopic = node.responseTopic
|
|
422
|
+
}
|
|
423
|
+
if (node.correlationData) {
|
|
424
|
+
msg.correlationData = node.correlationData
|
|
425
|
+
}
|
|
426
|
+
if (node.contentType) {
|
|
427
|
+
msg.contentType = node.contentType
|
|
428
|
+
}
|
|
429
|
+
if (node.messageExpiryInterval) {
|
|
430
|
+
msg.messageExpiryInterval = node.messageExpiryInterval
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (hasProperty(msg, 'payload')) {
|
|
434
|
+
// send the message
|
|
435
|
+
node.brokerConn.publish(msg, function (err) {
|
|
436
|
+
if (err && err.warn) {
|
|
437
|
+
node.warn(err)
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
done(err)
|
|
441
|
+
})
|
|
442
|
+
} else {
|
|
443
|
+
done()
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
done(error)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function updateStatus (node, allNodes) {
|
|
451
|
+
let setStatus = setStatusDisconnected
|
|
452
|
+
if (node.connecting) {
|
|
453
|
+
setStatus = setStatusConnecting
|
|
454
|
+
} else if (node.connected) {
|
|
455
|
+
setStatus = setStatusConnected
|
|
456
|
+
}
|
|
457
|
+
setStatus(node, allNodes)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function setStatusFeatureDisabled (node, allNodes) {
|
|
461
|
+
if (allNodes) {
|
|
462
|
+
for (const id in node.users) {
|
|
463
|
+
if (hasProperty(node.users, id)) {
|
|
464
|
+
node.users[id].status({ fill: 'red', shape: 'ring', text: 'common.status.featureDisabled' })
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
node.status({ fill: 'red', shape: 'ring', text: 'common.status.featureDisabled' })
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function setStatusDisconnected (node, allNodes) {
|
|
473
|
+
if (allNodes) {
|
|
474
|
+
for (const id in node.users) {
|
|
475
|
+
if (hasProperty(node.users, id)) {
|
|
476
|
+
node.users[id].status({ fill: 'red', shape: 'ring', text: 'common.status.disconnected' })
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
node.status({ fill: 'red', shape: 'ring', text: 'common.status.disconnected' })
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function setStatusConnecting (node, allNodes) {
|
|
485
|
+
if (allNodes) {
|
|
486
|
+
for (const id in node.users) {
|
|
487
|
+
if (hasProperty(node.users, id)) {
|
|
488
|
+
node.users[id].status({ fill: 'yellow', shape: 'ring', text: 'common.status.connecting' })
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
node.status({ fill: 'yellow', shape: 'ring', text: 'common.status.connecting' })
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function setStatusConnected (node, allNodes) {
|
|
497
|
+
if (allNodes) {
|
|
498
|
+
for (const id in node.users) {
|
|
499
|
+
if (hasProperty(node.users, id)) {
|
|
500
|
+
node.users[id].status({ fill: 'green', shape: 'dot', text: 'common.status.connected' })
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
node.status({ fill: 'green', shape: 'dot', text: 'common.status.connected' })
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Perform the connect action
|
|
510
|
+
* @param {MQTTInNode|MQTTOutNode} node
|
|
511
|
+
* @param {Object} msg
|
|
512
|
+
* @param {Function} done
|
|
513
|
+
*/
|
|
514
|
+
function handleConnectAction (node, msg, done) {
|
|
515
|
+
const actionData = ALLOW_DYNAMIC_CONNECT_OPTIONS && (typeof msg.broker === 'object' ? msg.broker : null)
|
|
516
|
+
if (!featureEnabled || !mqttSettings.broker) {
|
|
517
|
+
node.error('Team Broker is not enabled in this FlowFuse EE environment', msg)
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
if (node.brokerConn.canConnect()) {
|
|
521
|
+
// Not currently connected/connecting - trigger the connect
|
|
522
|
+
if (actionData) {
|
|
523
|
+
node.brokerConn.setOptions(actionData)
|
|
524
|
+
}
|
|
525
|
+
node.brokerConn.connect(function () {
|
|
526
|
+
done()
|
|
527
|
+
})
|
|
528
|
+
} else {
|
|
529
|
+
// Already Connected/Connecting
|
|
530
|
+
if (!actionData) {
|
|
531
|
+
// All is good - already connected and no broker override provided
|
|
532
|
+
done()
|
|
533
|
+
} else if (actionData.force) {
|
|
534
|
+
// The force flag tells us to cycle the connection.
|
|
535
|
+
node.brokerConn.disconnect(function () {
|
|
536
|
+
node.brokerConn.setOptions(actionData)
|
|
537
|
+
node.brokerConn.connect(function () {
|
|
538
|
+
done()
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
} else {
|
|
542
|
+
// Without force flag, we will refuse to cycle an active connection
|
|
543
|
+
done(new Error(RED._('ff-mqtt.errors.invalid-action-alreadyconnected')))
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Perform the disconnect action
|
|
550
|
+
* @param {MQTTInNode|MQTTOutNode} node
|
|
551
|
+
* @param {Function} done
|
|
552
|
+
*/
|
|
553
|
+
function handleDisconnectAction (node, done) {
|
|
554
|
+
node.brokerConn.disconnect(function () {
|
|
555
|
+
done()
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
const unsubscribeCandidates = {}
|
|
559
|
+
// #endregion "Supporting functions"
|
|
560
|
+
|
|
561
|
+
// #region "Broker node"
|
|
562
|
+
function MQTTBrokerNode (n) {
|
|
563
|
+
/** @type {MQTTBrokerNode} */
|
|
564
|
+
const node = this
|
|
565
|
+
node._initialised = false
|
|
566
|
+
node._initialising = false
|
|
567
|
+
|
|
568
|
+
Object.defineProperty(node, 'initialised', {
|
|
569
|
+
get: function () {
|
|
570
|
+
return node._initialised
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
Object.defineProperty(node, 'initialising', {
|
|
574
|
+
get: function () {
|
|
575
|
+
return node._initialising
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
Object.defineProperty(node, 'linked', {
|
|
579
|
+
get: function () {
|
|
580
|
+
return node._linked
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
Object.defineProperty(node, 'linkFailed', {
|
|
584
|
+
get: function () {
|
|
585
|
+
return node._linkFailed
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
node.users = {}
|
|
590
|
+
// Config node state
|
|
591
|
+
node.brokerurl = ''
|
|
592
|
+
node.connected = false
|
|
593
|
+
node.connecting = false
|
|
594
|
+
node.closing = false
|
|
595
|
+
node.options = {}
|
|
596
|
+
node.queue = []
|
|
597
|
+
node.subscriptions = {}
|
|
598
|
+
node.clientListeners = []
|
|
599
|
+
/** @type {mqtt.MqttClient} */
|
|
600
|
+
node.client = null
|
|
601
|
+
node.linkPromise = null
|
|
602
|
+
node._linked = mqttSettings.linked || false
|
|
603
|
+
node._linkFailed = false
|
|
604
|
+
|
|
605
|
+
node.link = async function () {
|
|
606
|
+
if (node.linkPromise) {
|
|
607
|
+
return node.linkPromise // already linking, return the existing promise
|
|
608
|
+
}
|
|
609
|
+
if (!featureEnabled || !mqttSettings.broker) {
|
|
610
|
+
return false
|
|
611
|
+
}
|
|
612
|
+
const teamBrokerApi = TeamBrokerApi.TeamBrokerApi(got, {
|
|
613
|
+
forgeURL: forgeSettings.forgeURL,
|
|
614
|
+
teamId,
|
|
615
|
+
instanceType,
|
|
616
|
+
instanceId,
|
|
617
|
+
token: mqttSettings.token || ''
|
|
618
|
+
})
|
|
619
|
+
try {
|
|
620
|
+
node.linkPromise = teamBrokerApi.link(mqttSettings.broker.password)
|
|
621
|
+
await node.linkPromise
|
|
622
|
+
node.linkPromise = null // reset the link promise
|
|
623
|
+
node._linkFailed = false
|
|
624
|
+
node._linked = true
|
|
625
|
+
if (node._initialised === false) {
|
|
626
|
+
node.initialise()
|
|
627
|
+
node.connect()
|
|
628
|
+
}
|
|
629
|
+
return node._linked
|
|
630
|
+
} catch (err) {
|
|
631
|
+
const code = err.code || err.statusCode || err.status || 'unknown'
|
|
632
|
+
const name = err.name || 'UnknownError'
|
|
633
|
+
const error = new Error(`Failed to link to FlowFuse broker: ${name} (${code})`, { cause: err })
|
|
634
|
+
node._linkFailed = true
|
|
635
|
+
node._linked = false
|
|
636
|
+
throw error
|
|
637
|
+
} finally {
|
|
638
|
+
node.linkPromise = null // reset the link promise
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
node.initialise = function () {
|
|
643
|
+
try {
|
|
644
|
+
if (node._initialising) {
|
|
645
|
+
return // already initialising, simply return
|
|
646
|
+
}
|
|
647
|
+
node._initialising = true
|
|
648
|
+
|
|
649
|
+
if (!featureEnabled || !mqttSettings.broker) {
|
|
650
|
+
throw new Error('Team Broker is not enabled in this FlowFuse EE environment')
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const settings = {
|
|
654
|
+
broker: mqttSettings.broker.url || ''
|
|
655
|
+
}
|
|
656
|
+
node.credentials = {
|
|
657
|
+
user: TEAM_CLIENT_AUTH_ID,
|
|
658
|
+
password: mqttSettings.broker.password || ''
|
|
659
|
+
}
|
|
660
|
+
settings.username = TEAM_CLIENT_AUTH_ID
|
|
661
|
+
settings.password = mqttSettings.broker.password || ''
|
|
662
|
+
settings.clientid = TEAM_CLIENT_CLIENT_ID
|
|
663
|
+
settings.autoConnect = mqttSettings.autoConnect !== false
|
|
664
|
+
settings.usetls = mqttSettings.usetls || false
|
|
665
|
+
settings.compatmode = mqttSettings.compatmode || false
|
|
666
|
+
settings.protocolVersion = mqttSettings.protocolVersion || 5
|
|
667
|
+
settings.keepalive = mqttSettings.keepalive || 60
|
|
668
|
+
settings.cleansession = mqttSettings.cleansession !== false // default to true
|
|
669
|
+
settings.topicAliasMaximum = mqttSettings.topicAliasMaximum || 0
|
|
670
|
+
node.setOptions(settings, true) // initial options
|
|
671
|
+
node._initialised = true
|
|
672
|
+
} catch (error) {
|
|
673
|
+
node._initialised = false
|
|
674
|
+
node.error(error)
|
|
675
|
+
throw error // re-throw the error to stop initialisation
|
|
676
|
+
} finally {
|
|
677
|
+
node._initialising = false
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
node.setOptions = function (opts, init) {
|
|
682
|
+
if (!opts || typeof opts !== 'object') {
|
|
683
|
+
return // nothing to change, simply return
|
|
684
|
+
}
|
|
685
|
+
// apply property changes (only if the property exists in the opts object)
|
|
686
|
+
setIfHasProperty(opts, node, 'url', init)
|
|
687
|
+
setIfHasProperty(opts, node, 'broker', init)
|
|
688
|
+
setIfHasProperty(opts, node, 'port', init)
|
|
689
|
+
setIfHasProperty(opts, node, 'clientid', init)
|
|
690
|
+
setIfHasProperty(opts, node, 'autoConnect', init)
|
|
691
|
+
setIfHasProperty(opts, node, 'usetls', init)
|
|
692
|
+
setIfHasProperty(opts, node, 'verifyservercert', init)
|
|
693
|
+
setIfHasProperty(opts, node, 'compatmode', init)
|
|
694
|
+
setIfHasProperty(opts, node, 'protocolVersion', init)
|
|
695
|
+
setIfHasProperty(opts, node, 'keepalive', init)
|
|
696
|
+
setIfHasProperty(opts, node, 'cleansession', init)
|
|
697
|
+
setIfHasProperty(opts, node, 'autoUnsubscribe', init)
|
|
698
|
+
setIfHasProperty(opts, node, 'topicAliasMaximum', init)
|
|
699
|
+
setIfHasProperty(opts, node, 'maximumPacketSize', init)
|
|
700
|
+
setIfHasProperty(opts, node, 'receiveMaximum', init)
|
|
701
|
+
// https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
|
|
702
|
+
if (hasProperty(opts, 'userProperties')) {
|
|
703
|
+
node.userProperties = opts.userProperties
|
|
704
|
+
} else if (hasProperty(opts, 'userProps')) {
|
|
705
|
+
node.userProperties = opts.userProps
|
|
706
|
+
}
|
|
707
|
+
if (hasProperty(opts, 'sessionExpiry')) {
|
|
708
|
+
node.sessionExpiryInterval = opts.sessionExpiry
|
|
709
|
+
} else if (hasProperty(opts, 'sessionExpiryInterval')) {
|
|
710
|
+
node.sessionExpiryInterval = opts.sessionExpiryInterval
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function createLWT (topic, payload, qos, retain, v5opts, v5SubPropName) {
|
|
714
|
+
let message
|
|
715
|
+
if (topic) {
|
|
716
|
+
message = {
|
|
717
|
+
topic,
|
|
718
|
+
payload: payload || '',
|
|
719
|
+
qos: Number(qos || 0),
|
|
720
|
+
retain: retain === 'true' || retain === true
|
|
721
|
+
}
|
|
722
|
+
if (v5opts) {
|
|
723
|
+
let v5Properties = message
|
|
724
|
+
if (v5SubPropName) {
|
|
725
|
+
v5Properties = message[v5SubPropName] = {}
|
|
726
|
+
}
|
|
727
|
+
// re-align local prop name to mqttjs std
|
|
728
|
+
if (hasProperty(v5opts, 'respTopic')) { v5opts.responseTopic = v5opts.respTopic }
|
|
729
|
+
if (hasProperty(v5opts, 'correl')) { v5opts.correlationData = v5opts.correl }
|
|
730
|
+
if (hasProperty(v5opts, 'expiry')) { v5opts.messageExpiryInterval = v5opts.expiry }
|
|
731
|
+
if (hasProperty(v5opts, 'delay')) { v5opts.willDelayInterval = v5opts.delay }
|
|
732
|
+
if (hasProperty(v5opts, 'userProps')) { v5opts.userProperties = v5opts.userProps }
|
|
733
|
+
// setup v5 properties
|
|
734
|
+
if (typeof v5opts.userProperties === 'string' && /^ *{/.test(v5opts.userProperties)) {
|
|
735
|
+
try {
|
|
736
|
+
setUserProperties(JSON.parse(v5opts.userProps), v5Properties)
|
|
737
|
+
} catch (err) {}
|
|
738
|
+
} else if (typeof v5opts.userProperties === 'object') {
|
|
739
|
+
setUserProperties(v5opts.userProperties, v5Properties)
|
|
740
|
+
}
|
|
741
|
+
setStrProp(v5opts, v5Properties, 'contentType')
|
|
742
|
+
setStrProp(v5opts, v5Properties, 'responseTopic')
|
|
743
|
+
setBufferProp(v5opts, v5Properties, 'correlationData')
|
|
744
|
+
setIntProp(v5opts, v5Properties, 'messageExpiryInterval')
|
|
745
|
+
setIntProp(v5opts, v5Properties, 'willDelayInterval')
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return message
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (init) {
|
|
752
|
+
if (hasProperty(opts, 'birthTopic')) {
|
|
753
|
+
node.birthMessage = createLWT(opts.birthTopic, opts.birthPayload, opts.birthQos, opts.birthRetain, opts.birthMsg, '')
|
|
754
|
+
}
|
|
755
|
+
if (hasProperty(opts, 'closeTopic')) {
|
|
756
|
+
node.closeMessage = createLWT(opts.closeTopic, opts.closePayload, opts.closeQos, opts.closeRetain, opts.closeMsg, '')
|
|
757
|
+
}
|
|
758
|
+
if (hasProperty(opts, 'willTopic')) {
|
|
759
|
+
// will v5 properties must be set in the "properties" sub object
|
|
760
|
+
node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, 'properties')
|
|
761
|
+
}
|
|
762
|
+
} else {
|
|
763
|
+
// update options
|
|
764
|
+
if (hasProperty(opts, 'birth')) {
|
|
765
|
+
if (typeof opts.birth !== 'object') { opts.birth = {} }
|
|
766
|
+
node.birthMessage = createLWT(opts.birth.topic, opts.birth.payload, opts.birth.qos, opts.birth.retain, opts.birth.properties, '')
|
|
767
|
+
}
|
|
768
|
+
if (hasProperty(opts, 'close')) {
|
|
769
|
+
if (typeof opts.close !== 'object') { opts.close = {} }
|
|
770
|
+
node.closeMessage = createLWT(opts.close.topic, opts.close.payload, opts.close.qos, opts.close.retain, opts.close.properties, '')
|
|
771
|
+
}
|
|
772
|
+
if (hasProperty(opts, 'will')) {
|
|
773
|
+
if (typeof opts.will !== 'object') { opts.will = {} }
|
|
774
|
+
// will v5 properties must be set in the "properties" sub object
|
|
775
|
+
node.options.will = createLWT(opts.will.topic, opts.will.payload, opts.will.qos, opts.will.retain, opts.will.properties, 'properties')
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (node.credentials) {
|
|
780
|
+
node.username = node.credentials.user
|
|
781
|
+
node.password = node.credentials.password
|
|
782
|
+
}
|
|
783
|
+
if (!init & hasProperty(opts, 'username')) {
|
|
784
|
+
node.username = opts.username
|
|
785
|
+
}
|
|
786
|
+
if (!init & hasProperty(opts, 'password')) {
|
|
787
|
+
node.password = opts.password
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// If the config node is missing certain options (it was probably deployed prior to an update to the node code),
|
|
791
|
+
// select/generate sensible options for the new fields
|
|
792
|
+
if (typeof node.usetls === 'undefined') {
|
|
793
|
+
node.usetls = false
|
|
794
|
+
}
|
|
795
|
+
if (typeof node.verifyservercert === 'undefined') {
|
|
796
|
+
node.verifyservercert = false
|
|
797
|
+
}
|
|
798
|
+
if (typeof node.keepalive === 'undefined') {
|
|
799
|
+
node.keepalive = 60
|
|
800
|
+
} else if (typeof node.keepalive === 'string') {
|
|
801
|
+
node.keepalive = Number(node.keepalive)
|
|
802
|
+
}
|
|
803
|
+
if (typeof node.cleansession === 'undefined') {
|
|
804
|
+
node.cleansession = true
|
|
805
|
+
}
|
|
806
|
+
if (typeof node.autoUnsubscribe !== 'boolean') {
|
|
807
|
+
node.autoUnsubscribe = true
|
|
808
|
+
}
|
|
809
|
+
// use url or build a url from usetls://broker:port
|
|
810
|
+
if (node.url && node.brokerurl !== node.url) {
|
|
811
|
+
node.brokerurl = node.url
|
|
812
|
+
} else {
|
|
813
|
+
// if the broker is ws:// or wss:// or tcp://
|
|
814
|
+
if ((typeof node.broker === 'string') && node.broker.indexOf('://') > -1) {
|
|
815
|
+
node.brokerurl = node.broker
|
|
816
|
+
// Only for ws or wss, check if proxy env var for additional configuration
|
|
817
|
+
if (node.brokerurl.indexOf('wss://') > -1 || node.brokerurl.indexOf('ws://') > -1) {
|
|
818
|
+
// check if proxy is set in env
|
|
819
|
+
const agent = ProxyHelper.getWSProxyAgent(node.brokerurl)
|
|
820
|
+
if (agent) {
|
|
821
|
+
node.options.wsOptions = node.options.wsOptions || {}
|
|
822
|
+
node.options.wsOptions.agent = agent
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
// construct the std mqtt:// url
|
|
827
|
+
if (node.usetls) {
|
|
828
|
+
node.brokerurl = 'mqtts://'
|
|
829
|
+
} else {
|
|
830
|
+
node.brokerurl = 'mqtt://'
|
|
831
|
+
}
|
|
832
|
+
if (node.broker !== '') {
|
|
833
|
+
// Check for an IPv6 address
|
|
834
|
+
if (/(?:^|(?<=\s))(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(?=\s|$)/.test(node.broker)) {
|
|
835
|
+
node.brokerurl = node.brokerurl + '[' + node.broker + ']:'
|
|
836
|
+
} else {
|
|
837
|
+
node.brokerurl = node.brokerurl + node.broker + ':'
|
|
838
|
+
}
|
|
839
|
+
// port now defaults to 1883 if unset.
|
|
840
|
+
if (!node.port) {
|
|
841
|
+
node.brokerurl = node.brokerurl + '1883'
|
|
842
|
+
} else {
|
|
843
|
+
node.brokerurl = node.brokerurl + node.port
|
|
844
|
+
}
|
|
845
|
+
} else {
|
|
846
|
+
node.brokerurl = node.brokerurl + 'localhost:1883'
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Ensure cleansession set if clientid not supplied
|
|
852
|
+
if (!node.cleansession && !node.clientid) {
|
|
853
|
+
node.cleansession = true
|
|
854
|
+
node.warn(RED._('ff-mqtt.errors.nonclean-missingclientid'))
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Build options for passing to the MQTT.js API
|
|
858
|
+
node.options.username = node.username
|
|
859
|
+
node.options.password = node.password
|
|
860
|
+
node.options.keepalive = node.keepalive
|
|
861
|
+
node.options.clean = node.cleansession
|
|
862
|
+
node.options.clientId = node.clientid || 'nodered' + RED.util.generateId()
|
|
863
|
+
node.options.reconnectPeriod = RED.settings.mqttReconnectTime || 5000
|
|
864
|
+
delete node.options.protocolId // V4+ default
|
|
865
|
+
delete node.options.protocolVersion // V4 default
|
|
866
|
+
delete node.options.properties// V5 only
|
|
867
|
+
|
|
868
|
+
if (node.compatmode === 'true' || node.compatmode === true || +node.protocolVersion === 3) {
|
|
869
|
+
node.options.protocolId = 'MQIsdp'// V3 compat only
|
|
870
|
+
node.options.protocolVersion = 3
|
|
871
|
+
} else if (+node.protocolVersion === 5) {
|
|
872
|
+
delete node.options.protocolId
|
|
873
|
+
node.options.protocolVersion = 5
|
|
874
|
+
node.options.properties = {}
|
|
875
|
+
node.options.properties.requestResponseInformation = true
|
|
876
|
+
node.options.properties.requestProblemInformation = true
|
|
877
|
+
if (node.userProperties && /^ *{/.test(node.userProperties)) {
|
|
878
|
+
try {
|
|
879
|
+
setUserProperties(JSON.parse(node.userProperties), node.options.properties)
|
|
880
|
+
} catch (err) {}
|
|
881
|
+
}
|
|
882
|
+
if (node.sessionExpiryInterval && node.sessionExpiryInterval !== '0') {
|
|
883
|
+
setIntProp(node, node.options.properties, 'sessionExpiryInterval')
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// Ensure will payload, if set, is a string
|
|
887
|
+
if (node.options.will && Object.hasOwn(node.options.will, 'payload')) {
|
|
888
|
+
let payload = node.options.will.payload
|
|
889
|
+
if (payload === null || typeof payload === 'undefined') {
|
|
890
|
+
payload = ''
|
|
891
|
+
} else if (!Buffer.isBuffer(payload)) {
|
|
892
|
+
if (typeof payload === 'object') {
|
|
893
|
+
payload = JSON.stringify(payload)
|
|
894
|
+
} else if (typeof payload !== 'string') {
|
|
895
|
+
payload = '' + payload
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
node.options.will.payload = payload
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (node.usetls && n.tls) {
|
|
902
|
+
const tlsNode = RED.nodes.getNode(n.tls)
|
|
903
|
+
if (tlsNode) {
|
|
904
|
+
tlsNode.addTLSOptions(node.options)
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// If there's no rejectUnauthorized already, then this could be an
|
|
909
|
+
// old config where this option was provided on the broker node and
|
|
910
|
+
// not the tls node
|
|
911
|
+
if (typeof node.options.rejectUnauthorized === 'undefined') {
|
|
912
|
+
node.options.rejectUnauthorized = (node.verifyservercert === 'true' || node.verifyservercert === true)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
node.v5 = () => node.options && +node.options.protocolVersion === 5
|
|
916
|
+
node.subscriptionIdentifiersAvailable = () => node.v5() && node.serverProperties && node.serverProperties.subscriptionIdentifiersAvailable
|
|
917
|
+
|
|
918
|
+
// n.autoConnect = !(n.autoConnect === 'false' || n.autoConnect === false)
|
|
919
|
+
// node.setOptions(n, true)
|
|
920
|
+
|
|
921
|
+
// Define functions called by MQTT in and out nodes
|
|
922
|
+
node.registerAsync = async function (mqttNode) {
|
|
923
|
+
node.users[mqttNode.id] = mqttNode
|
|
924
|
+
if (!featureEnabled || !mqttSettings.broker) {
|
|
925
|
+
setStatusFeatureDisabled(node, true)
|
|
926
|
+
}
|
|
927
|
+
if (Object.keys(node.users).length === 1) {
|
|
928
|
+
if (!featureEnabled || !mqttSettings.broker) {
|
|
929
|
+
node.error('Team Broker is not enabled in this FlowFuse EE environment')
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
if (!node.linked) {
|
|
934
|
+
node.log('Auto linking FlowFuse MQTT node')
|
|
935
|
+
await node.link()
|
|
936
|
+
}
|
|
937
|
+
} catch (err) {
|
|
938
|
+
const error = new Error('Failed to link FlowFuse MQTT node', { cause: err })
|
|
939
|
+
node.error(error)
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
node.initialise()
|
|
943
|
+
node.connect()
|
|
944
|
+
} catch (err) {
|
|
945
|
+
const error = new Error('Failed to initialize and connect FlowFuse MQTT node', { cause: err })
|
|
946
|
+
node.error(error)
|
|
947
|
+
setStatusDisconnected(node, true)
|
|
948
|
+
} finally {
|
|
949
|
+
// update nodes status
|
|
950
|
+
setTimeout(function () {
|
|
951
|
+
updateStatus(node, true)
|
|
952
|
+
}, 1)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
node.deregister = function (mqttNode, done, autoDisconnect) {
|
|
958
|
+
setStatusDisconnected(mqttNode, false)
|
|
959
|
+
delete node.users[mqttNode.id]
|
|
960
|
+
if (autoDisconnect && !node.closing && (node.connected || node.connecting) && Object.keys(node.users).length === 0) {
|
|
961
|
+
node.disconnect(done)
|
|
962
|
+
} else {
|
|
963
|
+
done()
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
node.deregisterAsync = async function (mqttNode, autoDisconnect) {
|
|
967
|
+
return new Promise((resolve, reject) => {
|
|
968
|
+
node.deregister(mqttNode, (err) => {
|
|
969
|
+
if (err) {
|
|
970
|
+
reject(err)
|
|
971
|
+
} else {
|
|
972
|
+
resolve()
|
|
973
|
+
}
|
|
974
|
+
}, autoDisconnect)
|
|
975
|
+
})
|
|
976
|
+
}
|
|
977
|
+
node.canConnect = function () {
|
|
978
|
+
return !node.connected && !node.connecting && featureEnabled && mqttSettings.broker
|
|
979
|
+
}
|
|
980
|
+
node.connect = function (callback) {
|
|
981
|
+
if (node.canConnect()) {
|
|
982
|
+
node.closing = false
|
|
983
|
+
node.connecting = true
|
|
984
|
+
setStatusConnecting(node, true)
|
|
985
|
+
try {
|
|
986
|
+
node.serverProperties = {}
|
|
987
|
+
if (node.client) {
|
|
988
|
+
// belt and braces to avoid left over clients
|
|
989
|
+
node.client.end(true)
|
|
990
|
+
node._clientRemoveListeners()
|
|
991
|
+
}
|
|
992
|
+
node.client = mqtt.connect(node.brokerurl, node.options)
|
|
993
|
+
node.client.setMaxListeners(0)
|
|
994
|
+
let callbackDone = false // prevent re-connects causing node._clientOn('connect' firing callback multiple times
|
|
995
|
+
// Register successful connect or reconnect handler
|
|
996
|
+
node._clientOn('connect', function (connack) {
|
|
997
|
+
node.closing = false
|
|
998
|
+
node.connecting = false
|
|
999
|
+
node.connected = true
|
|
1000
|
+
if (!callbackDone && typeof callback === 'function') {
|
|
1001
|
+
callback()
|
|
1002
|
+
}
|
|
1003
|
+
callbackDone = true
|
|
1004
|
+
node.topicAliases = {}
|
|
1005
|
+
node.log(RED._('ff-mqtt.state.connected', { broker: (node.clientid ? node.clientid + '@' : '') + node.brokerurl }))
|
|
1006
|
+
if (+node.options.protocolVersion === 5 && connack && hasProperty(connack, 'properties')) {
|
|
1007
|
+
if (typeof connack.properties === 'object') {
|
|
1008
|
+
// clean & assign all props sent from server.
|
|
1009
|
+
setIntProp(connack.properties, node.serverProperties, 'topicAliasMaximum', 0)
|
|
1010
|
+
setIntProp(connack.properties, node.serverProperties, 'receiveMaximum', 0)
|
|
1011
|
+
setIntProp(connack.properties, node.serverProperties, 'sessionExpiryInterval', 0, 0xFFFFFFFF)
|
|
1012
|
+
setIntProp(connack.properties, node.serverProperties, 'maximumQoS', 0, 2)
|
|
1013
|
+
setBoolProp(connack.properties, node.serverProperties, 'retainAvailable', true)
|
|
1014
|
+
setBoolProp(connack.properties, node.serverProperties, 'wildcardSubscriptionAvailable', true)
|
|
1015
|
+
setBoolProp(connack.properties, node.serverProperties, 'subscriptionIdentifiersAvailable', true)
|
|
1016
|
+
setBoolProp(connack.properties, node.serverProperties, 'sharedSubscriptionAvailable')
|
|
1017
|
+
setIntProp(connack.properties, node.serverProperties, 'maximumPacketSize', 0)
|
|
1018
|
+
setIntProp(connack.properties, node.serverProperties, 'serverKeepAlive')
|
|
1019
|
+
setStrProp(connack.properties, node.serverProperties, 'responseInformation')
|
|
1020
|
+
setStrProp(connack.properties, node.serverProperties, 'serverReference')
|
|
1021
|
+
setStrProp(connack.properties, node.serverProperties, 'assignedClientIdentifier')
|
|
1022
|
+
setStrProp(connack.properties, node.serverProperties, 'reasonString')
|
|
1023
|
+
setUserProperties(connack.properties, node.serverProperties)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
setStatusConnected(node, true)
|
|
1027
|
+
// Remove any existing listeners before resubscribing to avoid duplicates in the event of a re-connection
|
|
1028
|
+
node._clientRemoveListeners('message')
|
|
1029
|
+
|
|
1030
|
+
// Re-subscribe to stored topics
|
|
1031
|
+
for (const s in node.subscriptions) {
|
|
1032
|
+
if (hasProperty(node.subscriptions, s)) {
|
|
1033
|
+
for (const r in node.subscriptions[s]) {
|
|
1034
|
+
if (hasProperty(node.subscriptions[s], r)) {
|
|
1035
|
+
node.subscribe(node.subscriptions[s][r])
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Send any birth message
|
|
1042
|
+
if (node.birthMessage) {
|
|
1043
|
+
setTimeout(() => {
|
|
1044
|
+
node.publish(node.birthMessage)
|
|
1045
|
+
}, 1)
|
|
1046
|
+
}
|
|
1047
|
+
})
|
|
1048
|
+
node._clientOn('reconnect', function () {
|
|
1049
|
+
setStatusConnecting(node, true)
|
|
1050
|
+
})
|
|
1051
|
+
// Broker Disconnect - V5 event
|
|
1052
|
+
node._clientOn('disconnect', function (packet) {
|
|
1053
|
+
// Emitted after receiving disconnect packet from broker. MQTT 5.0 feature.
|
|
1054
|
+
const rc = (packet && packet.properties && packet.reasonCode) || packet.reasonCode
|
|
1055
|
+
const rs = (packet && packet.properties && packet.properties.reasonString) || ''
|
|
1056
|
+
const details = {
|
|
1057
|
+
broker: (node.clientid ? node.clientid + '@' : '') + node.brokerurl,
|
|
1058
|
+
reasonCode: rc,
|
|
1059
|
+
reasonString: rs
|
|
1060
|
+
}
|
|
1061
|
+
node.connected = false
|
|
1062
|
+
node.log(RED._('ff-mqtt.state.broker-disconnected', details))
|
|
1063
|
+
setStatusDisconnected(node, true)
|
|
1064
|
+
})
|
|
1065
|
+
// Register disconnect handlers
|
|
1066
|
+
node._clientOn('close', function () {
|
|
1067
|
+
if (node.connected) {
|
|
1068
|
+
node.connected = false
|
|
1069
|
+
node.log(RED._('ff-mqtt.state.disconnected', { broker: (node.clientid ? node.clientid + '@' : '') + node.brokerurl }))
|
|
1070
|
+
setStatusDisconnected(node, true)
|
|
1071
|
+
} else if (node.connecting) {
|
|
1072
|
+
node.log(RED._('ff-mqtt.state.connect-failed', { broker: (node.clientid ? node.clientid + '@' : '') + node.brokerurl }))
|
|
1073
|
+
}
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
// Register connect error handler
|
|
1077
|
+
// The client's own reconnect logic will take care of errors
|
|
1078
|
+
// eslint-disable-next-line n/handle-callback-err
|
|
1079
|
+
node._clientOn('error', function (error) {
|
|
1080
|
+
})
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
// eslint-disable-next-line no-console
|
|
1083
|
+
console.log(err)
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
node.disconnect = function (callback) {
|
|
1089
|
+
const _callback = function () {
|
|
1090
|
+
if (node.connected || node.connecting) {
|
|
1091
|
+
setStatusDisconnected(node, true)
|
|
1092
|
+
}
|
|
1093
|
+
if (node.client) { node._clientRemoveListeners() }
|
|
1094
|
+
node.connecting = false
|
|
1095
|
+
node.connected = false
|
|
1096
|
+
callback && typeof callback === 'function' && callback()
|
|
1097
|
+
}
|
|
1098
|
+
if (!node.client) { return _callback() }
|
|
1099
|
+
if (node.closing) { return _callback() }
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Call end and wait for the client to end (or timeout)
|
|
1103
|
+
* @param {mqtt.MqttClient} client The broker client
|
|
1104
|
+
* @param {number} ms The time to wait for the client to end
|
|
1105
|
+
* @returns
|
|
1106
|
+
*/
|
|
1107
|
+
const waitEnd = (client, ms) => {
|
|
1108
|
+
return new Promise((resolve, reject) => {
|
|
1109
|
+
node.closing = true
|
|
1110
|
+
if (!client) {
|
|
1111
|
+
resolve()
|
|
1112
|
+
} else {
|
|
1113
|
+
const t = setTimeout(() => {
|
|
1114
|
+
// clean end() has exceeded WAIT_END, lets force end!
|
|
1115
|
+
client && client.end(true)
|
|
1116
|
+
resolve()
|
|
1117
|
+
}, ms)
|
|
1118
|
+
client.end(() => {
|
|
1119
|
+
clearTimeout(t)
|
|
1120
|
+
resolve()
|
|
1121
|
+
})
|
|
1122
|
+
}
|
|
1123
|
+
})
|
|
1124
|
+
}
|
|
1125
|
+
if (node.connected && node.closeMessage) {
|
|
1126
|
+
node.publish(node.closeMessage, function (_err) {
|
|
1127
|
+
waitEnd(node.client, 2000).then(() => {
|
|
1128
|
+
_callback()
|
|
1129
|
+
}).catch((e) => {
|
|
1130
|
+
_callback()
|
|
1131
|
+
})
|
|
1132
|
+
})
|
|
1133
|
+
} else {
|
|
1134
|
+
waitEnd(node.client, 2000).then(() => {
|
|
1135
|
+
_callback()
|
|
1136
|
+
}).catch((e) => {
|
|
1137
|
+
_callback()
|
|
1138
|
+
})
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
node.subscriptionIds = {}
|
|
1142
|
+
node.subid = 1
|
|
1143
|
+
|
|
1144
|
+
// typedef for subscription object:
|
|
1145
|
+
/**
|
|
1146
|
+
* @typedef {Object} Subscription
|
|
1147
|
+
* @property {String} topic - topic to subscribe to
|
|
1148
|
+
* @property {Object} [options] - options object
|
|
1149
|
+
* @property {Number} [options.qos] - quality of service
|
|
1150
|
+
* @property {Number} [options.nl] - no local
|
|
1151
|
+
* @property {Number} [options.rap] - retain as published
|
|
1152
|
+
* @property {Number} [options.rh] - retain handling
|
|
1153
|
+
* @property {Number} [options.properties] - MQTT 5.0 properties
|
|
1154
|
+
* @property {Number} [options.properties.subscriptionIdentifier] - MQTT 5.0 subscription identifier
|
|
1155
|
+
* @property {Number} [options.properties.userProperties] - MQTT 5.0 user properties
|
|
1156
|
+
* @property {Function} callback
|
|
1157
|
+
* @property {String} ref - reference to the node that created the subscription
|
|
1158
|
+
*/
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Create a subscription object
|
|
1162
|
+
* @param {String} _topic - topic to subscribe to
|
|
1163
|
+
* @param {Object} _options - options object
|
|
1164
|
+
* @param {String} _ref - reference to the node that created the subscription
|
|
1165
|
+
* @returns {Subscription}
|
|
1166
|
+
*/
|
|
1167
|
+
function createSubscriptionObject (_topic, _options, _ref, _brokerId) {
|
|
1168
|
+
/** @type {Subscription} */
|
|
1169
|
+
const subscription = {}
|
|
1170
|
+
const ref = _ref || 0
|
|
1171
|
+
let options
|
|
1172
|
+
let qos = 1 // default to QoS 1 (AWS and several other brokers don't support QoS 2)
|
|
1173
|
+
|
|
1174
|
+
// if options is an object, then clone it
|
|
1175
|
+
if (typeof _options === 'object') {
|
|
1176
|
+
options = RED.util.cloneMessage(_options || {})
|
|
1177
|
+
qos = _options.qos
|
|
1178
|
+
} else if (typeof _options === 'number') {
|
|
1179
|
+
qos = _options
|
|
1180
|
+
}
|
|
1181
|
+
options = options || {}
|
|
1182
|
+
|
|
1183
|
+
// sanitise qos
|
|
1184
|
+
if (typeof qos === 'number' && qos >= 0 && qos <= 2) {
|
|
1185
|
+
options.qos = qos
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
subscription.topic = _topic
|
|
1189
|
+
subscription.qos = qos
|
|
1190
|
+
subscription.options = RED.util.cloneMessage(options)
|
|
1191
|
+
subscription.ref = ref
|
|
1192
|
+
subscription.brokerId = _brokerId
|
|
1193
|
+
return subscription
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* If topic is a subscription object, then use that, otherwise look up the topic in
|
|
1198
|
+
* the subscriptions object. If the topic is not found, then create a new subscription
|
|
1199
|
+
* object and add it to the subscriptions object.
|
|
1200
|
+
* @param {Subscription|String} topic
|
|
1201
|
+
* @param {*} options
|
|
1202
|
+
* @param {*} callback
|
|
1203
|
+
* @param {*} ref
|
|
1204
|
+
*/
|
|
1205
|
+
node.subscribe = function (topic, options, callback, ref) {
|
|
1206
|
+
/** @type {Subscription} */
|
|
1207
|
+
let subscription
|
|
1208
|
+
let doCompare = false
|
|
1209
|
+
let changesFound = false
|
|
1210
|
+
|
|
1211
|
+
// function signature 1: subscribe(subscription: Subscription)
|
|
1212
|
+
if (typeof topic === 'object' && topic !== null) {
|
|
1213
|
+
subscription = topic
|
|
1214
|
+
topic = subscription.topic
|
|
1215
|
+
options = subscription.options
|
|
1216
|
+
ref = subscription.ref
|
|
1217
|
+
callback = subscription.callback
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// function signature 2: subscribe(topic: String, options: Object, callback: Function, ref: String)
|
|
1221
|
+
else if (typeof topic === 'string') {
|
|
1222
|
+
// since this is a call where all params are provided, it might be
|
|
1223
|
+
// a node change (modification) so we need to check for changes
|
|
1224
|
+
doCompare = true
|
|
1225
|
+
subscription = node.subscriptions[topic] && node.subscriptions[topic][ref]
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// bad function call
|
|
1229
|
+
else {
|
|
1230
|
+
console.warn('Invalid call to node.subscribe')
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
const thisBrokerId = node.type === 'mqtt-broker' ? node.id : node.broker
|
|
1234
|
+
|
|
1235
|
+
// unsubscribe topics where the broker has changed
|
|
1236
|
+
const oldBrokerSubs = (unsubscribeCandidates[ref] || []).filter(sub => sub.brokerId !== thisBrokerId)
|
|
1237
|
+
oldBrokerSubs.forEach(sub => {
|
|
1238
|
+
/** @type {MQTTBrokerNode} */
|
|
1239
|
+
const _brokerConn = node // RED.nodes.getNode(sub.brokerId)
|
|
1240
|
+
if (_brokerConn) {
|
|
1241
|
+
_brokerConn.unsubscribe(sub.topic, sub.ref, true)
|
|
1242
|
+
}
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
// if subscription is found (or sent in as a parameter), then check for changes.
|
|
1246
|
+
// if there are any changes requested, tidy up the old subscription
|
|
1247
|
+
if (subscription) {
|
|
1248
|
+
if (doCompare) {
|
|
1249
|
+
// compare the current sub to the passed in parameters. Use RED.util.compareObjects against
|
|
1250
|
+
// only the minimal set of properties to identify if the subscription has changed
|
|
1251
|
+
const currentSubscription = createSubscriptionObject(subscription.topic, subscription.options, subscription.ref)
|
|
1252
|
+
const newSubscription = createSubscriptionObject(topic, options, ref)
|
|
1253
|
+
changesFound = RED.util.compareObjects(currentSubscription, newSubscription) === false
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (changesFound) {
|
|
1258
|
+
if (subscription.handler) {
|
|
1259
|
+
node._clientRemoveListeners('message', subscription.handler)
|
|
1260
|
+
subscription.handler = null
|
|
1261
|
+
}
|
|
1262
|
+
const _brokerConn = node // RED.nodes.getNode(subscription.brokerId)
|
|
1263
|
+
if (_brokerConn) {
|
|
1264
|
+
_brokerConn.unsubscribe(subscription.topic, subscription.ref, true)
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// clean up the unsubscribe candidate list
|
|
1269
|
+
delete unsubscribeCandidates[ref]
|
|
1270
|
+
|
|
1271
|
+
// determine if this is an existing subscription
|
|
1272
|
+
const existingSubscription = typeof subscription === 'object' && subscription !== null
|
|
1273
|
+
|
|
1274
|
+
// if existing subscription is not found or has changed, create a new subscription object
|
|
1275
|
+
if (existingSubscription === false || changesFound) {
|
|
1276
|
+
subscription = createSubscriptionObject(topic, options, ref, node.id)
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// setup remainder of subscription properties and event handling
|
|
1280
|
+
node.subscriptions[topic] = node.subscriptions[topic] || {}
|
|
1281
|
+
node.subscriptions[topic][ref] = subscription
|
|
1282
|
+
if (!node.subscriptionIds[topic]) {
|
|
1283
|
+
node.subscriptionIds[topic] = node.subid++
|
|
1284
|
+
}
|
|
1285
|
+
subscription.options = subscription.options || {}
|
|
1286
|
+
subscription.options.properties = options.properties || {}
|
|
1287
|
+
subscription.options.properties.subscriptionIdentifier = node.subscriptionIds[topic]
|
|
1288
|
+
subscription.callback = callback
|
|
1289
|
+
|
|
1290
|
+
// if the client is connected, then setup the handler and subscribe
|
|
1291
|
+
if (node.connected) {
|
|
1292
|
+
const subIdsAvailable = node.subscriptionIdentifiersAvailable()
|
|
1293
|
+
|
|
1294
|
+
if (!subscription.handler) {
|
|
1295
|
+
subscription.handler = function (mtopic, mpayload, mpacket) {
|
|
1296
|
+
const sops = subscription.options ? subscription.options.properties : {}
|
|
1297
|
+
const pops = mpacket.properties || {}
|
|
1298
|
+
if (subIdsAvailable && pops.subscriptionIdentifier && sops.subscriptionIdentifier && (pops.subscriptionIdentifier !== sops.subscriptionIdentifier)) {
|
|
1299
|
+
// do nothing as subscriptionIdentifier does not match
|
|
1300
|
+
} else if (matchTopic(topic, mtopic)) {
|
|
1301
|
+
subscription.callback && subscription.callback(mtopic, mpayload, mpacket)
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
node._clientOn('message', subscription.handler)
|
|
1306
|
+
// if the broker doesn't support subscription identifiers, then don't send them (AWS support)
|
|
1307
|
+
if (subscription.options.properties && subscription.options.properties.subscriptionIdentifier && subIdsAvailable !== true) {
|
|
1308
|
+
delete subscription.options.properties.subscriptionIdentifier
|
|
1309
|
+
}
|
|
1310
|
+
node.client.subscribe(topic, subscription.options, function (err, granted) {
|
|
1311
|
+
if (err) {
|
|
1312
|
+
node.error(RED._('ff-mqtt.errors.subscribe-failed', { topic, error: err.message }), { topic })
|
|
1313
|
+
}
|
|
1314
|
+
})
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
node.unsubscribe = function (topic, ref, removeClientSubscription) {
|
|
1319
|
+
ref = ref || 0
|
|
1320
|
+
const unsub = removeClientSubscription || node.autoUnsubscribe !== false
|
|
1321
|
+
const sub = node.subscriptions[topic]
|
|
1322
|
+
let brokerId = node.id
|
|
1323
|
+
if (sub) {
|
|
1324
|
+
if (sub[ref]) {
|
|
1325
|
+
brokerId = sub[ref].brokerId || brokerId
|
|
1326
|
+
if (node.client && sub[ref].handler) {
|
|
1327
|
+
node._clientRemoveListeners('message', sub[ref].handler)
|
|
1328
|
+
sub[ref].handler = null
|
|
1329
|
+
}
|
|
1330
|
+
if (unsub) {
|
|
1331
|
+
delete sub[ref]
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
// if instructed to remove the actual MQTT client subscription
|
|
1335
|
+
if (unsub) {
|
|
1336
|
+
// if there are no more subscriptions for the topic, then remove the topic
|
|
1337
|
+
if (Object.keys(sub).length === 0) {
|
|
1338
|
+
try {
|
|
1339
|
+
node.client.unsubscribe(topic)
|
|
1340
|
+
} catch (_err) {
|
|
1341
|
+
// do nothing
|
|
1342
|
+
} finally {
|
|
1343
|
+
// remove unsubscribe candidate as it is now REALLY unsubscribed
|
|
1344
|
+
delete node.subscriptions[topic]
|
|
1345
|
+
delete node.subscriptionIds[topic]
|
|
1346
|
+
if (unsubscribeCandidates[ref]) {
|
|
1347
|
+
unsubscribeCandidates[ref] = unsubscribeCandidates[ref].filter(sub => sub.topic !== topic)
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
} else {
|
|
1352
|
+
// if instructed to not remove the client subscription, then add it to the candidate list
|
|
1353
|
+
// of subscriptions to be removed when the the same ref is used in a subsequent subscribe
|
|
1354
|
+
// and the topic has changed
|
|
1355
|
+
unsubscribeCandidates[ref] = unsubscribeCandidates[ref] || []
|
|
1356
|
+
unsubscribeCandidates[ref].push({
|
|
1357
|
+
topic,
|
|
1358
|
+
ref,
|
|
1359
|
+
brokerId
|
|
1360
|
+
})
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
node.topicAliases = {}
|
|
1365
|
+
|
|
1366
|
+
node.publish = function (msg, done) {
|
|
1367
|
+
if (node.connected) {
|
|
1368
|
+
if (msg.payload === null || msg.payload === undefined) {
|
|
1369
|
+
msg.payload = ''
|
|
1370
|
+
} else if (!Buffer.isBuffer(msg.payload)) {
|
|
1371
|
+
if (typeof msg.payload === 'object') {
|
|
1372
|
+
msg.payload = JSON.stringify(msg.payload)
|
|
1373
|
+
} else if (typeof msg.payload !== 'string') {
|
|
1374
|
+
msg.payload = '' + msg.payload
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const options = {
|
|
1378
|
+
qos: msg.qos || 0,
|
|
1379
|
+
retain: msg.retain || false
|
|
1380
|
+
}
|
|
1381
|
+
let topicOK = hasProperty(msg, 'topic') && (typeof msg.topic === 'string') && (isValidPublishTopic(msg.topic))
|
|
1382
|
+
// https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
|
|
1383
|
+
if (+node.options.protocolVersion === 5) {
|
|
1384
|
+
const bsp = node.serverProperties || {}
|
|
1385
|
+
if (msg.userProperties && typeof msg.userProperties !== 'object') {
|
|
1386
|
+
delete msg.userProperties
|
|
1387
|
+
}
|
|
1388
|
+
if (hasProperty(msg, 'topicAlias') && !isNaN(Number(msg.topicAlias))) {
|
|
1389
|
+
msg.topicAlias = parseInt(msg.topicAlias)
|
|
1390
|
+
} else {
|
|
1391
|
+
delete msg.topicAlias
|
|
1392
|
+
}
|
|
1393
|
+
options.properties = options.properties || {}
|
|
1394
|
+
setStrProp(msg, options.properties, 'responseTopic')
|
|
1395
|
+
setBufferProp(msg, options.properties, 'correlationData')
|
|
1396
|
+
setStrProp(msg, options.properties, 'contentType')
|
|
1397
|
+
setIntProp(msg, options.properties, 'messageExpiryInterval', 0)
|
|
1398
|
+
setUserProperties(msg.userProperties, options.properties)
|
|
1399
|
+
setIntProp(msg, options.properties, 'topicAlias', 1, bsp.topicAliasMaximum || 0)
|
|
1400
|
+
setBoolProp(msg, options.properties, 'payloadFormatIndicator')
|
|
1401
|
+
// FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
|
|
1402
|
+
|
|
1403
|
+
// check & sanitise topic
|
|
1404
|
+
if (topicOK && options.properties.topicAlias) {
|
|
1405
|
+
const aliasValid = (bsp.topicAliasMaximum && bsp.topicAliasMaximum >= options.properties.topicAlias)
|
|
1406
|
+
if (!aliasValid) {
|
|
1407
|
+
done('Invalid topicAlias')
|
|
1408
|
+
return
|
|
1409
|
+
}
|
|
1410
|
+
if (node.topicAliases[options.properties.topicAlias] === msg.topic) {
|
|
1411
|
+
msg.topic = ''
|
|
1412
|
+
} else {
|
|
1413
|
+
node.topicAliases[options.properties.topicAlias] = msg.topic
|
|
1414
|
+
}
|
|
1415
|
+
} else if (!msg.topic && options.properties.responseTopic) {
|
|
1416
|
+
msg.topic = msg.responseTopic
|
|
1417
|
+
topicOK = isValidPublishTopic(msg.topic)
|
|
1418
|
+
delete msg.responseTopic // prevent responseTopic being resent?
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (topicOK) {
|
|
1423
|
+
node.client.publish(msg.topic, msg.payload, options, function (err) {
|
|
1424
|
+
if (done) {
|
|
1425
|
+
done(err)
|
|
1426
|
+
} else if (err) {
|
|
1427
|
+
node.error(err, msg)
|
|
1428
|
+
}
|
|
1429
|
+
})
|
|
1430
|
+
} else {
|
|
1431
|
+
const error = new Error(RED._('ff-mqtt.errors.invalid-topic'))
|
|
1432
|
+
error.warn = true
|
|
1433
|
+
if (done) {
|
|
1434
|
+
done(error)
|
|
1435
|
+
} else {
|
|
1436
|
+
node.warn(error, msg)
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// no `on` or `close` handlers for the static broker node
|
|
1443
|
+
// node.on('close', function (done) {
|
|
1444
|
+
// node.disconnect(function () {
|
|
1445
|
+
// done()
|
|
1446
|
+
// })
|
|
1447
|
+
// })
|
|
1448
|
+
|
|
1449
|
+
// fake the node.status function if it is not already defined
|
|
1450
|
+
if (typeof node.status !== 'function') {
|
|
1451
|
+
/** @type {function} */
|
|
1452
|
+
node.status = (options) => options
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Provide logging functions for this shared broker client via RED.log
|
|
1456
|
+
const decorateDebugMsg = (msg) => {
|
|
1457
|
+
if (typeof msg === 'object' && msg.message) {
|
|
1458
|
+
msg = msg.message
|
|
1459
|
+
}
|
|
1460
|
+
if (!msg.includes('FlowFuse MQTT')) {
|
|
1461
|
+
msg = `FlowFuse MQTT Nodes Client: ${msg}`
|
|
1462
|
+
}
|
|
1463
|
+
return msg
|
|
1464
|
+
}
|
|
1465
|
+
// mimic the node.warn function if it is not already defined
|
|
1466
|
+
if (typeof node.warn !== 'function') {
|
|
1467
|
+
/** @type {function} */
|
|
1468
|
+
node.warn = (msg) => {
|
|
1469
|
+
RED.log.warn(decorateDebugMsg(msg))
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
// mimic the node.error function if it is not already defined
|
|
1473
|
+
if (typeof node.error !== 'function') {
|
|
1474
|
+
/** @type {function} */
|
|
1475
|
+
node.error = (msg, _msg) => {
|
|
1476
|
+
RED.log.error(decorateDebugMsg(msg), _msg)
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
// mimic the node.log function if it is not already defined
|
|
1480
|
+
if (typeof node.log !== 'function') {
|
|
1481
|
+
/** @type {function} */
|
|
1482
|
+
node.log = (msg) => {
|
|
1483
|
+
RED.log.info(decorateDebugMsg(msg))
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Add event handlers to the MQTT.js client and track them so that
|
|
1489
|
+
* we do not remove any handlers that the MQTT client uses internally.
|
|
1490
|
+
* Use {@link node._clientRemoveListeners `node._clientRemoveListeners`} to remove handlers
|
|
1491
|
+
* @param {string} event The name of the event
|
|
1492
|
+
* @param {function} handler The handler for this event
|
|
1493
|
+
*/
|
|
1494
|
+
node._clientOn = function (event, handler) {
|
|
1495
|
+
node.clientListeners.push({ event, handler })
|
|
1496
|
+
node.client.on(event, handler)
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Remove event handlers from the MQTT.js client & only the events
|
|
1501
|
+
* that we attached in {@link node._clientOn `node._clientOn`}.
|
|
1502
|
+
* * If `event` is omitted, then all events matching `handler` are removed
|
|
1503
|
+
* * If `handler` is omitted, then all events named `event` are removed
|
|
1504
|
+
* * If both parameters are omitted, then all events are removed
|
|
1505
|
+
* @param {string} [event] The name of the event (optional)
|
|
1506
|
+
* @param {function} [handler] The handler for this event (optional)
|
|
1507
|
+
*/
|
|
1508
|
+
node._clientRemoveListeners = function (event, handler) {
|
|
1509
|
+
node.clientListeners = node.clientListeners.filter((l) => {
|
|
1510
|
+
if (event && event !== l.event) { return true }
|
|
1511
|
+
if (handler && handler !== l.handler) { return true }
|
|
1512
|
+
node.client.removeListener(l.event, l.handler)
|
|
1513
|
+
return false // found and removed, filter out this one
|
|
1514
|
+
})
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// #endregion "Broker node"
|
|
1519
|
+
|
|
1520
|
+
// #region MQTTIn node
|
|
1521
|
+
function MQTTInNode (n) {
|
|
1522
|
+
RED.nodes.createNode(this, n)
|
|
1523
|
+
/** @type {MQTTInNode} */const node = this
|
|
1524
|
+
|
|
1525
|
+
/** @type {MQTTBrokerNode} */node.brokerConn = sharedBroker // RED.nodes.getNode(node.broker)
|
|
1526
|
+
|
|
1527
|
+
node.dynamicSubs = {}
|
|
1528
|
+
node.isDynamic = hasProperty(n, 'inputs') && +n.inputs === 1
|
|
1529
|
+
node.inputs = n.inputs
|
|
1530
|
+
node.topic = n.topic
|
|
1531
|
+
node.qos = parseInt(n.qos)
|
|
1532
|
+
node.subscriptionIdentifier = n.subscriptionIdentifier// https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901117
|
|
1533
|
+
node.nl = n.nl
|
|
1534
|
+
node.rap = n.rap
|
|
1535
|
+
node.rh = n.rh
|
|
1536
|
+
|
|
1537
|
+
const Actions = {
|
|
1538
|
+
CONNECT: 'connect',
|
|
1539
|
+
DISCONNECT: 'disconnect',
|
|
1540
|
+
SUBSCRIBE: 'subscribe',
|
|
1541
|
+
UNSUBSCRIBE: 'unsubscribe',
|
|
1542
|
+
GETSUBS: 'getSubscriptions'
|
|
1543
|
+
}
|
|
1544
|
+
const allowableActions = Object.values(Actions)
|
|
1545
|
+
|
|
1546
|
+
if (isNaN(node.qos) || node.qos < 0 || node.qos > 2) {
|
|
1547
|
+
node.qos = 2
|
|
1548
|
+
}
|
|
1549
|
+
if (!node.isDynamic && !isValidSubscriptionTopic(node.topic)) {
|
|
1550
|
+
return node.warn(RED._('ff-mqtt.errors.invalid-topic'))
|
|
1551
|
+
}
|
|
1552
|
+
node.datatype = n.datatype || 'utf8'
|
|
1553
|
+
if (node.brokerConn) {
|
|
1554
|
+
setStatusDisconnected(node)
|
|
1555
|
+
if (node.topic || node.isDynamic) {
|
|
1556
|
+
node.brokerConn.registerAsync(node).then(() => {
|
|
1557
|
+
if (!node.isDynamic) {
|
|
1558
|
+
const options = { qos: node.qos }
|
|
1559
|
+
const v5 = node.brokerConn.options && +node.brokerConn.options.protocolVersion === 5
|
|
1560
|
+
if (v5) {
|
|
1561
|
+
setIntProp(node, options, 'rh', 0, 2, 0)
|
|
1562
|
+
if (node.nl === 'true' || node.nl === true) options.nl = true
|
|
1563
|
+
else if (node.nl === 'false' || node.nl === false) options.nl = false
|
|
1564
|
+
if (node.rap === 'true' || node.rap === true) options.rap = true
|
|
1565
|
+
else if (node.rap === 'false' || node.rap === false) options.rap = false
|
|
1566
|
+
}
|
|
1567
|
+
node._topic = node.topic // store the original topic incase node is later changed
|
|
1568
|
+
node.brokerConn.subscribe(node.topic, options, function (topic, payload, packet) {
|
|
1569
|
+
subscriptionHandler(node, node.datatype, topic, payload, packet)
|
|
1570
|
+
}, node.id)
|
|
1571
|
+
}
|
|
1572
|
+
if (node.brokerConn.connected) {
|
|
1573
|
+
node.status({ fill: 'green', shape: 'dot', text: 'common.status.connected' })
|
|
1574
|
+
}
|
|
1575
|
+
}).catch((err) => {
|
|
1576
|
+
node.error(err)
|
|
1577
|
+
setStatusDisconnected(node, true)
|
|
1578
|
+
})
|
|
1579
|
+
} else {
|
|
1580
|
+
node.error(RED._('ff-mqtt.errors.not-defined'))
|
|
1581
|
+
}
|
|
1582
|
+
node.on('input', function (msg, send, done) {
|
|
1583
|
+
const v5 = node.brokerConn.options && +node.brokerConn.options.protocolVersion === 5
|
|
1584
|
+
const action = msg.action
|
|
1585
|
+
|
|
1586
|
+
if (!allowableActions.includes(action)) {
|
|
1587
|
+
done(new Error(RED._('ff-mqtt.errors.invalid-action-action')))
|
|
1588
|
+
return
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if (action === Actions.CONNECT) {
|
|
1592
|
+
handleConnectAction(node, msg, done)
|
|
1593
|
+
} else if (action === Actions.DISCONNECT) {
|
|
1594
|
+
handleDisconnectAction(node, done)
|
|
1595
|
+
} else if (action === Actions.SUBSCRIBE || action === Actions.UNSUBSCRIBE) {
|
|
1596
|
+
const subscriptions = []
|
|
1597
|
+
let actionData
|
|
1598
|
+
// coerce msg.topic into an array of strings or objects (for later iteration)
|
|
1599
|
+
if (action === Actions.UNSUBSCRIBE && msg.topic === true) {
|
|
1600
|
+
actionData = Object.values(node.dynamicSubs)
|
|
1601
|
+
} else if (Array.isArray(msg.topic)) {
|
|
1602
|
+
actionData = msg.topic
|
|
1603
|
+
} else if (typeof msg.topic === 'string' || typeof msg.topic === 'object') {
|
|
1604
|
+
actionData = [msg.topic]
|
|
1605
|
+
} else {
|
|
1606
|
+
done(new Error(RED._('ff-mqtt.errors.invalid-action-badsubscription')))
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
// ensure each subscription is an object with topic etc
|
|
1610
|
+
for (let index = 0; index < actionData.length; index++) {
|
|
1611
|
+
let subscription = actionData[index]
|
|
1612
|
+
if (typeof subscription === 'string') {
|
|
1613
|
+
subscription = { topic: subscription }
|
|
1614
|
+
}
|
|
1615
|
+
if (!subscription.topic || !isValidSubscriptionTopic(subscription.topic)) {
|
|
1616
|
+
done(new Error(RED._('ff-mqtt.errors.invalid-topic')))
|
|
1617
|
+
return
|
|
1618
|
+
}
|
|
1619
|
+
subscriptions.push(subscription)
|
|
1620
|
+
}
|
|
1621
|
+
if (action === Actions.UNSUBSCRIBE) {
|
|
1622
|
+
subscriptions.forEach(function (sub) {
|
|
1623
|
+
node.brokerConn.unsubscribe(sub.topic, node.id, true)
|
|
1624
|
+
delete node.dynamicSubs[sub.topic]
|
|
1625
|
+
})
|
|
1626
|
+
// user can access current subscriptions through the complete node is so desired
|
|
1627
|
+
msg.subscriptions = Object.values(node.dynamicSubs)
|
|
1628
|
+
done()
|
|
1629
|
+
} else if (action === Actions.SUBSCRIBE) {
|
|
1630
|
+
subscriptions.forEach(function (sub) {
|
|
1631
|
+
// always unsubscribe before subscribe to prevent multiple subs to same topic
|
|
1632
|
+
if (node.dynamicSubs[sub.topic]) {
|
|
1633
|
+
node.brokerConn.unsubscribe(sub.topic, node.id, true)
|
|
1634
|
+
delete node.dynamicSubs[sub.topic]
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// prepare options. Default qos 2 & rap flag true (same as 'mqtt in' node ui defaults when adding to editor)
|
|
1638
|
+
const options = {}
|
|
1639
|
+
setIntProp(sub, options, 'qos', 0, 2, 2)// default to qos 2 (same as 'mqtt in' default)
|
|
1640
|
+
sub.qos = options.qos
|
|
1641
|
+
if (v5) {
|
|
1642
|
+
setIntProp(sub, options, 'rh', 0, 2, 0) // default rh to 0:send retained messages (same as 'mqtt in' default)
|
|
1643
|
+
sub.rh = options.rh
|
|
1644
|
+
setBoolProp(sub, options, 'rap', true) // default rap to true:Keep retain flag of original publish (same as 'mqtt in' default)
|
|
1645
|
+
sub.rap = options.rap
|
|
1646
|
+
if (sub.nl === 'true' || sub.nl === true) {
|
|
1647
|
+
options.nl = true
|
|
1648
|
+
sub.nl = true
|
|
1649
|
+
} else if (sub.nl === 'false' || sub.nl === false) {
|
|
1650
|
+
options.nl = false
|
|
1651
|
+
sub.nl = false
|
|
1652
|
+
} else {
|
|
1653
|
+
delete sub.nl
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// subscribe to sub.topic & hook up subscriptionHandler
|
|
1658
|
+
node.brokerConn.subscribe(sub.topic, options, function (topic, payload, packet) {
|
|
1659
|
+
subscriptionHandler(node, sub.datatype || node.datatype, topic, payload, packet)
|
|
1660
|
+
}, node.id)
|
|
1661
|
+
node.dynamicSubs[sub.topic] = sub // save for later unsubscription & 'list' action
|
|
1662
|
+
})
|
|
1663
|
+
// user can access current subscriptions through the complete node is so desired
|
|
1664
|
+
msg.subscriptions = Object.values(node.dynamicSubs)
|
|
1665
|
+
done()
|
|
1666
|
+
}
|
|
1667
|
+
} else if (action === Actions.GETSUBS) {
|
|
1668
|
+
// send list of subscriptions in payload
|
|
1669
|
+
msg.topic = 'subscriptions'
|
|
1670
|
+
msg.payload = Object.values(node.dynamicSubs)
|
|
1671
|
+
send(msg)
|
|
1672
|
+
done()
|
|
1673
|
+
}
|
|
1674
|
+
})
|
|
1675
|
+
|
|
1676
|
+
node.on('close', async function (removed, done) {
|
|
1677
|
+
try {
|
|
1678
|
+
if (node.linkPromise) {
|
|
1679
|
+
await node.linkPromise
|
|
1680
|
+
}
|
|
1681
|
+
} catch (_error) {
|
|
1682
|
+
// do nothing, just ensure that the linkPromise is resolved before closing
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
if (node.isDynamic) {
|
|
1686
|
+
Object.keys(node.dynamicSubs).forEach(function (topic) {
|
|
1687
|
+
node.brokerConn.unsubscribe(topic, node.id, removed)
|
|
1688
|
+
})
|
|
1689
|
+
node.dynamicSubs = {}
|
|
1690
|
+
} else {
|
|
1691
|
+
node.brokerConn.unsubscribe(node.topic, node.id, removed)
|
|
1692
|
+
}
|
|
1693
|
+
if (node.brokerConn) {
|
|
1694
|
+
await node.brokerConn.deregisterAsync(node, removed)
|
|
1695
|
+
node.brokerConn = null
|
|
1696
|
+
}
|
|
1697
|
+
} finally {
|
|
1698
|
+
done()
|
|
1699
|
+
}
|
|
1700
|
+
})
|
|
1701
|
+
} else {
|
|
1702
|
+
node.error(RED._('ff-mqtt.errors.missing-config'))
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
RED.nodes.registerType('ff-mqtt-in', MQTTInNode, {
|
|
1707
|
+
settings: {
|
|
1708
|
+
ffMqttInFeatureEnabled: {
|
|
1709
|
+
value: featureEnabled,
|
|
1710
|
+
exportable: true // make available in the editor
|
|
1711
|
+
},
|
|
1712
|
+
ffMqttInForgeUrl: {
|
|
1713
|
+
value: forgeSettings.forgeURL,
|
|
1714
|
+
exportable: true // make available in the editor
|
|
1715
|
+
},
|
|
1716
|
+
ffMqttInInstanceId: {
|
|
1717
|
+
value: instanceId,
|
|
1718
|
+
exportable: true // make available in the editor
|
|
1719
|
+
},
|
|
1720
|
+
ffMqttInInstanceType: {
|
|
1721
|
+
value: instanceType,
|
|
1722
|
+
exportable: true // make available in the editor
|
|
1723
|
+
},
|
|
1724
|
+
ffMqttInUserTeamBrokerClientUrl: {
|
|
1725
|
+
value: `${forgeSettings.forgeURL}/team-by-id/${teamId}/brokers/team-broker/client?searchQuery=${instanceId}`,
|
|
1726
|
+
exportable: true // make available in the editor
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
})
|
|
1730
|
+
// #endregion "MQTTIn node"
|
|
1731
|
+
|
|
1732
|
+
// #region "MQTTOut node"
|
|
1733
|
+
function MQTTOutNode (n) {
|
|
1734
|
+
RED.nodes.createNode(this, n)
|
|
1735
|
+
const node = this
|
|
1736
|
+
node.topic = n.topic
|
|
1737
|
+
node.qos = n.qos || null
|
|
1738
|
+
node.retain = n.retain
|
|
1739
|
+
|
|
1740
|
+
node.responseTopic = n.respTopic// https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901114
|
|
1741
|
+
node.correlationData = n.correl// https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901115
|
|
1742
|
+
node.contentType = n.contentType// https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901118
|
|
1743
|
+
node.messageExpiryInterval = n.expiry // https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901112
|
|
1744
|
+
try {
|
|
1745
|
+
if (/^ *{/.test(n.userProps)) {
|
|
1746
|
+
// setup this.userProperties
|
|
1747
|
+
setUserProperties(JSON.parse(n.userProps), node)// https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
|
|
1748
|
+
}
|
|
1749
|
+
} catch (err) {}
|
|
1750
|
+
// node.topicAlias = n.topicAlias; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901113
|
|
1751
|
+
// node.payloadFormatIndicator = n.payloadFormatIndicator; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901111
|
|
1752
|
+
// node.subscriptionIdentifier = n.subscriptionIdentifier;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901117
|
|
1753
|
+
|
|
1754
|
+
/** @type {MQTTBrokerNode} */node.brokerConn = sharedBroker // RED.nodes.getNode(node.broker)
|
|
1755
|
+
|
|
1756
|
+
const Actions = {
|
|
1757
|
+
CONNECT: 'connect',
|
|
1758
|
+
DISCONNECT: 'disconnect'
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
if (node.brokerConn) {
|
|
1762
|
+
setStatusDisconnected(node)
|
|
1763
|
+
node.on('input', function (msg, send, done) {
|
|
1764
|
+
if (msg.action) {
|
|
1765
|
+
if (msg.action === Actions.CONNECT) {
|
|
1766
|
+
handleConnectAction(node, msg, done)
|
|
1767
|
+
} else if (msg.action === Actions.DISCONNECT) {
|
|
1768
|
+
handleDisconnectAction(node, done)
|
|
1769
|
+
} else {
|
|
1770
|
+
done(new Error(RED._('ff-mqtt.errors.invalid-action-action')))
|
|
1771
|
+
}
|
|
1772
|
+
} else {
|
|
1773
|
+
doPublish(node, msg, done)
|
|
1774
|
+
}
|
|
1775
|
+
})
|
|
1776
|
+
if (node.brokerConn.connected) {
|
|
1777
|
+
node.status({ fill: 'green', shape: 'dot', text: 'common.status.connected' })
|
|
1778
|
+
}
|
|
1779
|
+
node.brokerConn.registerAsync(node)
|
|
1780
|
+
node.on('close', async function (removed, done) {
|
|
1781
|
+
try {
|
|
1782
|
+
if (node.linkPromise) {
|
|
1783
|
+
await node.linkPromise
|
|
1784
|
+
}
|
|
1785
|
+
} catch (_error) {
|
|
1786
|
+
// do nothing, just ensure that the linkPromise is resolved before closing
|
|
1787
|
+
}
|
|
1788
|
+
try {
|
|
1789
|
+
if (node.brokerConn) {
|
|
1790
|
+
await node.brokerConn.deregisterAsync(node, removed)
|
|
1791
|
+
node.brokerConn = null
|
|
1792
|
+
}
|
|
1793
|
+
} finally {
|
|
1794
|
+
done()
|
|
1795
|
+
}
|
|
1796
|
+
})
|
|
1797
|
+
} else {
|
|
1798
|
+
node.error(RED._('ff-mqtt.errors.missing-config'))
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
RED.nodes.registerType('ff-mqtt-out', MQTTOutNode, {
|
|
1802
|
+
settings: {
|
|
1803
|
+
ffMqttOutFeatureEnabled: {
|
|
1804
|
+
value: featureEnabled,
|
|
1805
|
+
exportable: true // make available in the editor
|
|
1806
|
+
},
|
|
1807
|
+
ffMqttOutForgeUrl: {
|
|
1808
|
+
value: forgeSettings.forgeURL,
|
|
1809
|
+
exportable: true // make available in the editor
|
|
1810
|
+
},
|
|
1811
|
+
ffMqttOutInstanceId: {
|
|
1812
|
+
value: instanceId,
|
|
1813
|
+
exportable: true // make available in the editor
|
|
1814
|
+
},
|
|
1815
|
+
ffMqttOutInstanceType: {
|
|
1816
|
+
value: instanceType,
|
|
1817
|
+
exportable: true // make available in the editor
|
|
1818
|
+
},
|
|
1819
|
+
ffMqttOutUserTeamBrokerClientUrl: {
|
|
1820
|
+
value: `${forgeSettings.forgeURL}/team-by-id/${teamId}/brokers/team-broker/client?searchQuery=${instanceId}`,
|
|
1821
|
+
exportable: true // make available in the editor
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
})
|
|
1825
|
+
// #endregion "MQTTOut node"
|
|
1826
|
+
}
|