node-red-sensecap-paas
Version:
SenseCAP PaaS
1,154 lines (1,100 loc) • 56.6 kB
JavaScript
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function (RED) {
"use strict";
var mqtt = require("mqtt");
var HttpsProxyAgent = require("https-proxy-agent");
var url = require("url");
//#region "Supporting functions"
function matchTopic(ts, t) {
if (ts == "#") {
return true;
} else if (ts.startsWith("$share")) {
/* The following allows shared subscriptions (as in MQTT v5)
http://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html#_Toc514345522
4.8.2 describes shares like:
$share/{ShareName}/{filter}
$share is a literal string that marks the Topic Filter as being a Shared Subscription Topic Filter.
{ShareName} is a character string that does not include "/", "+" or "#"
{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.
*/
ts = ts.replace(/^\$share\/[^#+/]+\/(.*)/g, "$1");
}
var re = new RegExp(
"^" +
ts
.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g, "\\$1")
.replace(/\+/g, "[^/]+")
.replace(/\/#$/, "(/.*)?") +
"$"
);
return re.test(t);
}
/**
* Helper function for setting integer property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {integer} [minVal] The minimum value. If the src value is less than minVal, it will NOT be set in the destination
* @param {integer} [maxVal] The maximum value. If the src value is greater than maxVal, it will NOT be set in the destination
* @param {integer} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setIntProp(src, dst, propName, minVal, maxVal, def) {
if (hasProperty(src, propName)) {
var v = parseInt(src[propName]);
if (isNaN(v)) return;
if (minVal != null) {
if (v < minVal) return;
}
if (maxVal != null) {
if (v > maxVal) return;
}
dst[propName] = v;
} else {
if (def != undefined) dst[propName] = def;
}
}
/**
* Test a topic string is valid for subscription
* @param {string} topic
* @returns `true` if it is a valid topic
*/
function isValidSubscriptionTopic(topic) {
return /^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(topic);
}
/**
* Test a topic string is valid for publishing
* @param {string} topic
* @returns `true` if it is a valid topic
*/
function isValidPublishTopic(topic) {
return !/[\+#\b\f\n\r\t\v\0]/.test(topic);
}
/**
* Helper function for setting string property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {string} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setStrProp(src, dst, propName, def) {
if (src[propName] && typeof src[propName] == "string") {
dst[propName] = src[propName];
} else {
if (def != undefined) dst[propName] = def;
}
}
/**
* Helper function for setting boolean property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setBoolProp(src, dst, propName, def) {
if (src[propName] != null) {
if (src[propName] === "true" || src[propName] === true) {
dst[propName] = true;
} else if (src[propName] === "false" || src[propName] === false) {
dst[propName] = false;
}
} else {
if (def != undefined) dst[propName] = def;
}
}
/**
* Helper function for copying the MQTT v5 srcUserProperties object (parameter1) to the properties object (parameter2).
* Any property in srcUserProperties that is NOT a key/string pair will be silently discarded.
* NOTE: if no sutable properties are present, the userProperties object will NOT be added to the properties object
* @param {object} srcUserProperties An object with key/value string pairs
* @param {object} properties A properties object in which userProperties will be copied to
*/
function setUserProperties(srcUserProperties, properties) {
if (srcUserProperties && typeof srcUserProperties == "object") {
let _clone = {};
let count = 0;
let keys = Object.keys(srcUserProperties);
if (!keys || !keys.length) return null;
keys.forEach((key) => {
let val = srcUserProperties[key];
if (typeof val == "string") {
count++;
_clone[key] = val;
}
});
if (count) properties.userProperties = _clone;
}
}
/**
* Helper function for copying the MQTT v5 buffer type properties
* NOTE: if src[propName] is not a buffer, dst[propName] will NOT be assigned a value (unless def is set)
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the Source object
*/
function setBufferProp(src, dst, propName, def) {
if (!dst) return;
if (src && dst) {
var buf = src[propName];
if (buf && typeof Buffer.isBuffer(buf)) {
dst[propName] = Buffer.from(buf);
}
} else {
if (def != undefined) dst[propName] = def;
}
}
/**
* Helper function for applying changes to an objects properties ONLY when the src object actually has the property.
* This avoids setting a `dst` property null/undefined when the `src` object doesnt have the named property.
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set property
* @param {string} propName The property name to set in the Destination object
* @param {boolean} force force the dst property to be updated/created even if src property is empty
*/
function setIfHasProperty(src, dst, propName, force) {
if (src && dst && propName) {
const ok = force || hasProperty(src, propName);
if (ok) {
dst[propName] = src[propName];
}
}
}
/**
* Helper function to test an object has a property
* @param {object} obj Object to test
* @param {string} propName Name of property to find
* @returns true if object has property `propName`
*/
function hasProperty(obj, propName) {
//JavaScript does not protect the property name hasOwnProperty
//Object.prototype.hasOwnProperty.call is the recommended/safer test
return Object.prototype.hasOwnProperty.call(obj, propName);
}
/**
* Handle the payload / packet recieved in MQTT In and MQTT Sub nodes
*/
function subscriptionHandler(node, datatype, topic, payload, packet) {
// const v5 = node.brokerConn.options && node.brokerConn.options.protocolVersion == 5;
var msg = {
topic: topic,
payload: null,
qos: packet.qos,
retain: packet.retain,
};
// if (v5 && packet.properties) {
// setStrProp(packet.properties, msg, "responseTopic");
// setBufferProp(packet.properties, msg, "correlationData");
// setStrProp(packet.properties, msg, "contentType");
// setIntProp(packet.properties, msg, "messageExpiryInterval", 0);
// setBoolProp(packet.properties, msg, "payloadFormatIndicator");
// setStrProp(packet.properties, msg, "reasonString");
// setUserProperties(packet.properties.userProperties, msg);
// }
var payloadParam
try {
payloadParam = JSON.parse(payload.toString());
} catch (e) {
node.error(RED._("mqtt.errors.invalid-json-parse"), {
payload: payload,
topic: topic,
qos: packet.qos,
retain: packet.retain,
});
return;
}
var parameters = topic.split("/");
var length = parameters.length;
if (length != 7) {
return;
}
var orgId = parameters[2];
var eui = parameters[3];
var channel = parseInt(parameters[4]);
var measurementID = parseInt(parameters[6]);
//过滤掉非当前主题的消息
if ((node.eui !== "+" && node.eui != eui) ||
(node.channel !== "+" && node.channel != channel) ||
(node.measurementID !== "+" && node.measurementID != measurementID)) {
return;
}
//如果是标准原数据格式,则按自定义协议返回json数据
if (node.output == "raw") {
payloadParam.orgId = orgId;
payloadParam.eui = eui;
payloadParam.channel = channel;
payloadParam.measurementID = measurementID;
} else if (node.output == "powerbi") {
//如果是power bi格式,则按powerbi格式返回
payloadParam = [payloadParam];
}
msg.payload = payloadParam;
if (
node.brokerConn.broker === "localhost" ||
node.brokerConn.broker === "127.0.0.1"
) {
msg._topic = topic;
}
node.send(msg);
}
function setStatusDisconnected(node, allNodes) {
if (allNodes) {
for (var id in node.users) {
if (hasProperty(node.users, id)) {
node.users[id].status({
fill: "red",
shape: "ring",
text: "node-red:common.status.disconnected",
});
}
}
} else {
node.status({
fill: "red",
shape: "ring",
text: "node-red:common.status.disconnected",
});
}
}
function setStatusConnecting(node, allNodes) {
if (allNodes) {
for (var id in node.users) {
if (hasProperty(node.users, id)) {
node.users[id].status({
fill: "yellow",
shape: "ring",
text: "node-red:common.status.connecting",
});
}
}
} else {
node.status({
fill: "yellow",
shape: "ring",
text: "node-red:common.status.connecting",
});
}
}
function setStatusConnected(node, allNodes) {
if (allNodes) {
for (var id in node.users) {
if (hasProperty(node.users, id)) {
node.users[id].status({
fill: "green",
shape: "dot",
text: "node-red:common.status.connected",
});
}
}
} else {
node.status({
fill: "green",
shape: "dot",
text: "node-red:common.status.connected",
});
}
}
function handleConnectAction(node, msg, done) {
let actionData = typeof msg.broker === "object" ? msg.broker : null;
if (node.brokerConn.canConnect()) {
// Not currently connected/connecting - trigger the connect
if (actionData) {
node.brokerConn.setOptions(actionData);
}
node.brokerConn.connect(function () {
done();
});
} else {
// Already Connected/Connecting
if (!actionData) {
// All is good - already connected and no broker override provided
done();
} else if (actionData.force) {
// The force flag tells us to cycle the connection.
node.brokerConn.disconnect(function () {
node.brokerConn.setOptions(actionData);
node.brokerConn.connect(function () {
done();
});
});
} else {
// Without force flag, we will refuse to cycle an active connection
done(new Error(RED._("mqtt.errors.invalid-action-alreadyconnected")));
}
}
}
function handleDisconnectAction(node, done) {
node.brokerConn.disconnect(function () {
done();
});
}
//#endregion "Supporting functions"
function SenseCAPConfigNode(n) {
RED.nodes.createNode(this, n);
const node = this;
node.users = {};
// Config node state
node.brokerurl = "";
node.connected = false;
node.connecting = false;
node.closing = false;
node.options = {};
node.queue = [];
node.subscriptions = {};
node.clientListeners = [];
/** @type {mqtt.MqttClient}*/ this.client;
node.setOptions = function (opts, init) {
if (!opts || typeof opts !== "object") {
return; //nothing to change, simply return
}
const originalBrokerURL = node.brokerurl;
//apply property changes (only if the property exists in the opts object)
setIfHasProperty(opts, node, "url", init);
setIfHasProperty(opts, node, "broker", init);
setIfHasProperty(opts, node, "port", init);
setIfHasProperty(opts, node, "clientid", init);
setIfHasProperty(opts, node, "autoConnect", init);
setIfHasProperty(opts, node, "usews", init);
setIfHasProperty(opts, node, "verifyservercert", init);
setIfHasProperty(opts, node, "compatmode", init);
setIfHasProperty(opts, node, "protocolVersion", init);
setIfHasProperty(opts, node, "keepalive", init);
setIfHasProperty(opts, node, "cleansession", init);
setIfHasProperty(opts, node, "sessionExpiry", init);
setIfHasProperty(opts, node, "topicAliasMaximum", init);
setIfHasProperty(opts, node, "maximumPacketSize", init);
setIfHasProperty(opts, node, "receiveMaximum", init);
if (node.credentials) {
node.username = "org-" + node.credentials.orgid;
node.password = node.credentials.apikey;
}
// If the config node is missing certain options (it was probably deployed prior to an update to the node code),
// select/generate sensible options for the new fields
if (typeof node.usews === "undefined") {
node.usews = false;
}
if (typeof node.verifyservercert === "undefined") {
node.verifyservercert = false;
}
if (typeof node.keepalive === "undefined") {
node.keepalive = 60;
} else if (typeof node.keepalive === "string") {
node.keepalive = Number(node.keepalive);
}
if (typeof node.cleansession === "undefined") {
node.cleansession = true;
}
//use url or build a url from usetls://broker:port
if (node.url && node.brokerurl !== node.url) {
node.brokerurl = node.url;
} else {
// if the broker is ws:// or wss:// or tcp://
if (node.broker.indexOf("://") > -1) {
node.brokerurl = node.broker;
// Only for ws or wss, check if proxy env var for additional configuration
if (
node.brokerurl.indexOf("wss://") > -1 ||
node.brokerurl.indexOf("ws://") > -1
) {
// check if proxy is set in env
let prox, noprox, noproxy;
if (process.env.http_proxy) {
prox = process.env.http_proxy;
}
if (process.env.HTTP_PROXY) {
prox = process.env.HTTP_PROXY;
}
if (process.env.no_proxy) {
noprox = process.env.no_proxy.split(",");
}
if (process.env.NO_PROXY) {
noprox = process.env.NO_PROXY.split(",");
}
if (noprox) {
for (var i = 0; i < noprox.length; i += 1) {
if (node.brokerurl.indexOf(noprox[i].trim()) !== -1) {
noproxy = true;
}
}
}
if (prox && !noproxy) {
var parsedUrl = url.parse(node.brokerurl);
var proxyOpts = url.parse(prox);
// true for wss
proxyOpts.secureEndpoint = parsedUrl.protocol
? parsedUrl.protocol === "wss:"
: true;
// Set Agent for wsOption in MQTT
var agent = new HttpsProxyAgent(proxyOpts);
node.options.wsOptions = {
agent: agent,
};
}
}
} else {
// construct the std mqtt:// url
node.brokerurl = "mqtt://";
if (node.broker !== "") {
//Check for an IPv6 address
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
)
) {
node.brokerurl = node.brokerurl + "[" + node.broker + "]:";
} else {
node.brokerurl = node.brokerurl + node.broker + ":";
}
// port now defaults to 1883 if unset.
if (!node.port) {
node.brokerurl = node.brokerurl + "1883";
} else {
node.brokerurl = node.brokerurl + node.port;
}
} else {
node.brokerurl = node.brokerurl + "localhost:1883";
}
}
}
// Ensure cleansession set if clientid not supplied
if (!node.cleansession && !node.clientid) {
node.cleansession = true;
node.warn(RED._("mqtt.errors.nonclean-missingclientid"));
}
// Build options for passing to the MQTT.js API
node.options.username = node.username;
node.options.password = node.password;
node.options.keepalive = node.keepalive;
node.options.clean = node.cleansession;
node.options.clientId =
node.clientid || "nodered_" + RED.util.generateId();
node.options.reconnectPeriod = RED.settings.mqttReconnectTime || 5000;
delete node.options.protocolId; //V4+ default
delete node.options.protocolVersion; //V4 default
delete node.options.properties; //V5 only
if (
node.compatmode == "true" ||
node.compatmode === true ||
node.protocolVersion == 3
) {
node.options.protocolId = "MQIsdp"; //V3 compat only
node.options.protocolVersion = 3;
} else if (node.protocolVersion == 5) {
delete node.options.protocolId;
node.options.protocolVersion = 5;
node.options.properties = {};
node.options.properties.requestResponseInformation = true;
node.options.properties.requestProblemInformation = true;
if (node.userProperties && /^ *{/.test(node.userProperties)) {
try {
setUserProperties(
JSON.parse(node.userProperties),
node.options.properties
);
} catch (err) { }
}
if (node.sessionExpiryInterval && node.sessionExpiryInterval !== "0") {
setIntProp(node, node.options.properties, "sessionExpiryInterval");
}
}
// If there's no rejectUnauthorized already, then this could be an
// old config where this option was provided on the broker node and
// not the tls node
if (typeof node.options.rejectUnauthorized === "undefined") {
node.options.rejectUnauthorized =
node.verifyservercert == "true" || node.verifyservercert === true;
}
};
n.autoConnect =
n.autoConnect === "false" || n.autoConnect === false ? false : true;
node.setOptions(n, true);
// Define functions called by MQTT in and out nodes
node.register = function (mqttNode) {
node.users[mqttNode.id] = mqttNode;
if (Object.keys(node.users).length === 1) {
if (node.autoConnect) {
node.connect();
}
}
};
node.deregister = function (mqttNode, done) {
delete node.users[mqttNode.id];
if (
!node.closing &&
node.connected &&
Object.keys(node.users).length === 0
) {
node.disconnect();
}
done();
};
node.canConnect = function () {
return !node.connected && !node.connecting;
};
node.connect = function (callback) {
if (node.canConnect()) {
node.closing = false;
node.connecting = true;
setStatusConnecting(node, true);
try {
node.serverProperties = {};
if (node.client) {
//belt and braces to avoid left over clients
node.client.end(true);
node._clientRemoveListeners();
}
node.client = mqtt.connect(node.brokerurl, node.options);
node.client.setMaxListeners(0);
let callbackDone = false; //prevent re-connects causing node._clientOn('connect' firing callback multiple times
// Register successful connect or reconnect handler
node._clientOn("connect", function (connack) {
node.closing = false;
node.connecting = false;
node.connected = true;
if (!callbackDone && typeof callback == "function") {
callback();
}
callbackDone = true;
node.topicAliases = {};
node.log(
RED._("mqtt.state.connected", {
broker:
(node.clientid ? node.clientid + "@" : "") + node.brokerurl,
})
);
if (
node.options.protocolVersion == 5 &&
connack &&
hasProperty(connack, "properties")
) {
if (typeof connack.properties == "object") {
//clean & assign all props sent from server.
setIntProp(
connack.properties,
node.serverProperties,
"topicAliasMaximum",
0
);
setIntProp(
connack.properties,
node.serverProperties,
"receiveMaximum",
0
);
setIntProp(
connack.properties,
node.serverProperties,
"sessionExpiryInterval",
0,
0xffffffff
);
setIntProp(
connack.properties,
node.serverProperties,
"maximumQoS",
0,
2
);
setBoolProp(
connack.properties,
node.serverProperties,
"retainAvailable",
true
);
setBoolProp(
connack.properties,
node.serverProperties,
"wildcardSubscriptionAvailable",
true
);
setBoolProp(
connack.properties,
node.serverProperties,
"subscriptionIdentifiersAvailable",
true
);
setBoolProp(
connack.properties,
node.serverProperties,
"sharedSubscriptionAvailable"
);
setIntProp(
connack.properties,
node.serverProperties,
"maximumPacketSize",
0
);
setIntProp(
connack.properties,
node.serverProperties,
"serverKeepAlive"
);
setStrProp(
connack.properties,
node.serverProperties,
"responseInformation"
);
setStrProp(
connack.properties,
node.serverProperties,
"serverReference"
);
setStrProp(
connack.properties,
node.serverProperties,
"assignedClientIdentifier"
);
setStrProp(
connack.properties,
node.serverProperties,
"reasonString"
);
setUserProperties(connack.properties, node.serverProperties);
}
}
setStatusConnected(node, true);
// Remove any existing listeners before resubscribing to avoid duplicates in the event of a re-connection
node._clientRemoveListeners("message");
// Re-subscribe to stored topics
for (var s in node.subscriptions) {
if (node.subscriptions.hasOwnProperty(s)) {
let topic = s;
let qos = 0;
let _options = {};
for (var r in node.subscriptions[s]) {
if (node.subscriptions[s].hasOwnProperty(r)) {
qos = Math.max(qos, node.subscriptions[s][r].qos);
_options = node.subscriptions[s][r].options;
node._clientOn("message", node.subscriptions[s][r].handler);
}
}
_options.qos = _options.qos || qos;
node.client.subscribe(topic, _options);
}
}
});
node._clientOn("reconnect", function () {
setStatusConnecting(node, true);
});
//Broker Disconnect - V5 event
node._clientOn("disconnect", function (packet) {
//Emitted after receiving disconnect packet from broker. MQTT 5.0 feature.
const rc =
(packet && packet.properties && packet.reasonCode) ||
packet.reasonCode;
const rs =
(packet && packet.properties && packet.properties.reasonString) ||
"";
const details = {
broker:
(node.clientid ? node.clientid + "@" : "") + node.brokerurl,
reasonCode: rc,
reasonString: rs,
};
node.connected = false;
node.log(RED._("mqtt.state.broker-disconnected", details));
setStatusDisconnected(node, true);
});
// Register disconnect handlers
node._clientOn("close", function () {
if (node.connected) {
node.connected = false;
node.log(
RED._("mqtt.state.disconnected", {
broker:
(node.clientid ? node.clientid + "@" : "") + node.brokerurl,
})
);
setStatusDisconnected(node, true);
} else if (node.connecting) {
node.log(
RED._("mqtt.state.connect-failed", {
broker:
(node.clientid ? node.clientid + "@" : "") + node.brokerurl,
})
);
}
});
// Register connect error handler
// The client's own reconnect logic will take care of errors
node._clientOn("error", function (error) { });
} catch (err) {
console.log(err);
}
}
};
node.disconnect = function (callback) {
const _callback = function () {
if (node.connected || node.connecting) {
setStatusDisconnected(node, true);
}
if (node.client) {
node._clientRemoveListeners();
}
node.connecting = false;
node.connected = false;
callback && typeof callback == "function" && callback();
};
if (!node.client) {
return _callback();
}
if (node.closing) {
return _callback();
}
let waitEnd = (client, ms) => {
return new Promise((resolve, reject) => {
node.closing = true;
if (!client) {
resolve();
} else {
const t = setTimeout(() => {
//clean end() has exceeded WAIT_END, lets force end!
client && client.end(true);
reject();
}, ms);
client.end(() => {
clearTimeout(t);
resolve();
});
}
});
};
if (node.connected && node.closeMessage) {
node.publish(node.closeMessage, function (err) {
waitEnd(node.client, 2000)
.then(() => {
_callback();
})
.catch((e) => {
_callback();
});
});
} else {
waitEnd(node.client, 2000)
.then(() => {
_callback();
})
.catch((e) => {
_callback();
});
}
};
node.subscriptionIds = {};
node.subid = 1;
node.subscribe = function (topic, options, callback, ref) {
ref = ref || 0;
var qos;
if (typeof options == "object") {
qos = options.qos;
} else {
qos = options;
options = {};
}
options.qos = qos;
if (!node.subscriptionIds[topic]) {
node.subscriptionIds[topic] = node.subid++;
}
options.properties = options.properties || {};
options.properties.subscriptionIdentifier = node.subscriptionIds[topic];
node.subscriptions[topic] = node.subscriptions[topic] || {};
var sub = {
topic: topic,
qos: qos,
options: options,
handler: function (mtopic, mpayload, mpacket) {
if (
mpacket.properties &&
options.properties &&
mpacket.properties.subscriptionIdentifier &&
options.properties.subscriptionIdentifier &&
mpacket.properties.subscriptionIdentifier !==
options.properties.subscriptionIdentifier
) {
//do nothing as subscriptionIdentifier does not match
} else if (matchTopic(topic, mtopic)) {
callback(mtopic, mpayload, mpacket);
}
},
ref: ref,
};
node.subscriptions[topic][ref] = sub;
if (node.connected) {
node._clientOn("message", sub.handler);
node.client.subscribe(topic, options);
}
};
node.unsubscribe = function (topic, ref, removed) {
ref = ref || 0;
var sub = node.subscriptions[topic];
if (sub) {
if (sub[ref]) {
if (node.client) {
node._clientRemoveListeners("message", sub[ref].handler);
}
delete sub[ref];
}
//TODO: Review. The `if(removed)` was commented out to always delete and remove subscriptions.
// if we dont then property changes dont get applied and old subs still trigger
//if (removed) {
if (Object.keys(sub).length === 0) {
delete node.subscriptions[topic];
delete node.subscriptionIds[topic];
if (node.connected) {
node.client.unsubscribe(topic);
}
}
//}
}
};
node.topicAliases = {};
node.publish = function (msg, done) {
if (node.connected) {
if (msg.payload === null || msg.payload === undefined) {
msg.payload = "";
} else if (!Buffer.isBuffer(msg.payload)) {
if (typeof msg.payload === "object") {
msg.payload = JSON.stringify(msg.payload);
} else if (typeof msg.payload !== "string") {
msg.payload = "" + msg.payload;
}
}
var options = {
qos: msg.qos || 0,
retain: msg.retain || false,
};
let topicOK =
hasProperty(msg, "topic") &&
typeof msg.topic === "string" &&
isValidPublishTopic(msg.topic);
//https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
if (node.options.protocolVersion == 5) {
const bsp = node.serverProperties || {};
if (msg.userProperties && typeof msg.userProperties !== "object") {
delete msg.userProperties;
}
if (
hasProperty(msg, "topicAlias") &&
!isNaN(Number(msg.topicAlias))
) {
msg.topicAlias = parseInt(msg.topicAlias);
} else {
delete msg.topicAlias;
}
options.properties = options.properties || {};
setStrProp(msg, options.properties, "responseTopic");
setBufferProp(msg, options.properties, "correlationData");
setStrProp(msg, options.properties, "contentType");
setIntProp(msg, options.properties, "messageExpiryInterval", 0);
setUserProperties(msg.userProperties, options.properties);
setIntProp(
msg,
options.properties,
"topicAlias",
1,
node.serverProperties.topicAliasMaximum || 0
);
setBoolProp(msg, options.properties, "payloadFormatIndicator");
//FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
//check & sanitise topic
if (topicOK && options.properties.topicAlias) {
let aliasValid =
bsp.topicAliasMaximum &&
bsp.topicAliasMaximum >= options.properties.topicAlias;
if (!aliasValid) {
done("Invalid topicAlias");
return;
}
if (
node.topicAliases[options.properties.topicAlias] === msg.topic
) {
msg.topic = "";
} else {
node.topicAliases[options.properties.topicAlias] = msg.topic;
}
} else if (!msg.topic && options.properties.responseTopic) {
msg.topic = msg.responseTopic;
topicOK = isValidPublishTopic(msg.topic);
delete msg.responseTopic; //prevent responseTopic being resent?
}
}
if (topicOK) {
node.client.publish(msg.topic, msg.payload, options, function (err) {
done && done(err);
return;
});
} else {
const error = new Error(RED._("mqtt.errors.invalid-topic"));
error.warn = true;
done(error);
}
}
};
node.on("close", function (done) {
node.disconnect(function () {
done();
});
});
/**
* Add event handlers to the MQTT.js client and track them so that
* we do not remove any handlers that the MQTT client uses internally.
* Use {@link node._clientRemoveListeners `node._clientRemoveListeners`} to remove handlers
* @param {string} event The name of the event
* @param {function} handler The handler for this event
*/
node._clientOn = function (event, handler) {
node.clientListeners.push({ event, handler });
node.client.on(event, handler);
};
/**
* Remove event handlers from the MQTT.js client & only the events
* that we attached in {@link node._clientOn `node._clientOn`}.
* * If `event` is omitted, then all events matching `handler` are removed
* * If `handler` is omitted, then all events named `event` are removed
* * If both parameters are omitted, then all events are removed
* @param {string} [event] The name of the event (optional)
* @param {function} [handler] The handler for this event (optional)
*/
node._clientRemoveListeners = function (event, handler) {
node.clientListeners = node.clientListeners.filter((l) => {
if (event && event !== l.event) {
return true;
}
if (handler && handler !== l.handler) {
return true;
}
node.client.removeListener(l.event, l.handler);
return false; //found and removed, filter out this one
});
};
}
RED.nodes.registerType("sensecap-config", SenseCAPConfigNode, {
credentials: {
orgid: { type: "text" },
apikey: { type: "password" },
},
});
function OpenStreamNode(n) {
RED.nodes.createNode(this, n);
const node = this;
/**@type {string}*/
node.broker = n.broker;
/**@type {BrokerNode}*/
node.brokerConn = RED.nodes.getNode(node.broker);
node.dynamicSubs = {};
node.isDynamic = n.hasOwnProperty("inputs") && n.inputs == 1;
node.inputs = n.inputs;
node.qos = parseInt(n.qos);
node.subscriptionIdentifier = n.subscriptionIdentifier; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901117
node.nl = n.nl;
node.rap = n.rap;
node.rh = n.rh;
node.output = n.output;
var eui = n.eui.trim();
node.eui = eui && eui.length == 16 ? eui : "+";
var reg = /^[0-9]*$/;
var channel = n.channel;
//如果输入的是数字,包括第一位是0
if (channel && reg.test(channel)) {
channel = parseInt(channel);
//超出范围
if (channel < 1 || channel > 99) {
channel = "+";
}
} else {
channel = "+";
}
node.channel = channel;
var measurementID = n.measurementID;
//如果输入的是数字,包括第一位是0
if (measurementID && reg.test(measurementID)) {
measurementID = parseInt(measurementID);
//超出范围
if (measurementID < 4097 || measurementID > 9999) {
measurementID = "+";
}
} else {
measurementID = "+";
}
node.measurementID = measurementID;
if (node.brokerConn.credentials && node.brokerConn.credentials.orgid) {
node.topic = `/device_sensor_data/${node.brokerConn.credentials.orgid}/${node.eui}/${node.channel}/+/${node.measurementID}`;
} else {
node.topic = "";
}
const Actions = {
CONNECT: "connect",
DISCONNECT: "disconnect",
SUBSCRIBE: "subscribe",
UNSUBSCRIBE: "unsubscribe",
GETSUBS: "getSubscriptions",
};
const allowableActions = Object.values(Actions);
if (isNaN(node.qos) || node.qos < 0 || node.qos > 2) {
node.qos = 2;
}
if (!node.isDynamic && !isValidSubscriptionTopic(node.topic)) {
return node.warn(RED._("mqtt.errors.invalid-topic"));
}
node.datatype = n.datatype || "utf8";
if (node.brokerConn) {
const v5 =
node.brokerConn.options && node.brokerConn.options.protocolVersion == 5;
setStatusDisconnected(node);
if (node.topic || node.isDynamic) {
node.brokerConn.register(node);
if (!node.isDynamic) {
let options = { qos: node.qos };
if (v5) {
setIntProp(node, options, "rh", 0, 2, 0);
if (node.nl === "true" || node.nl === true) options.nl = true;
else if (node.nl === "false" || node.nl === false)
options.nl = false;
if (node.rap === "true" || node.rap === true) options.rap = true;
else if (node.rap === "false" || node.rap === false)
options.rap = false;
}
node.brokerConn.subscribe(
node.topic,
options,
function (topic, payload, packet) {
subscriptionHandler(node, node.datatype, topic, payload, packet);
},
node.id
);
}
if (node.brokerConn.connected) {
node.status({
fill: "green",
shape: "dot",
text: "node-red:common.status.connected",
});
}
} else {
node.error(RED._("mqtt.errors.not-defined"));
}
node.on("input", function (msg, send, done) {
const v5 =
node.brokerConn.options &&
node.brokerConn.options.protocolVersion == 5;
const action = msg.action;
if (!allowableActions.includes(action)) {
done(new Error(RED._("mqtt.errors.invalid-action-action")));
return;
}
if (action === Actions.CONNECT) {
handleConnectAction(node, msg, done);
} else if (action === Actions.DISCONNECT) {
handleDisconnectAction(node, done);