@ifp-software/node-red-contrib-oee-ai-connector
Version:
Easily connect your production machines to oee.ai – The Industry 4.0 solution for OEE optimization.
204 lines (170 loc) • 6.1 kB
JavaScript
module.exports = function (RED) {
"use strict";
const mqtt = require("mqtt");
const HttpsProxyAgent = require("https-proxy-agent");
// These error codes are considered fatal and will cause the node to stop
// trying to connect if they occur during the first connection attempt.
// See the list of error codes at https://github.com/mqttjs/MQTT.js/blob/master/lib/client.js.
const fatalErrorCodes = [4, 5, 134, 135, 138, 142, 156, 159, 160];
const hostnames = {
preview: "mqtt.preview.oee.ai",
production: "mqtt.oee.ai",
};
function clientIdFromCredentials(credentials) {
const sensorId = credentials.id;
const randomId = Math.random().toString(32).substring(2);
if (sensorId) {
const shortId = sensorId.split("-")[0];
return `oee-node-red_${shortId}_${randomId}`;
}
}
function topicFromCredentials(credentials) {
const sensorId = credentials.id;
if (sensorId) {
return `oee/sensor/${sensorId}/measurement`;
}
}
function brokerUrlFromConfig(config) {
const hostname = hostnames[config.server];
const [schema, port] = config.protocol.split("-");
const url = `${schema}://${hostname}:${port}`;
if (config.protocol.startsWith("ws")) {
return `${url}/mqtt`;
}
return url;
}
function buildMessage(from, to, count) {
return JSON.stringify({
data: [
{
attributes: {
from: from.toISOString(),
to: to.toISOString(),
count,
},
},
],
});
}
function setupNode(node, config) {
node.status({ fill: "red", shape: "ring", text: "disconnected" });
node.connected = false;
node.connectedOnce = false;
if (!node.credentials.id || !node.credentials.token) {
node.error("Incomplete credentials. Please provide sensor ID and token.");
Promise.resolve().then(() => {
node.status({ fill: "grey", shape: "ring", text: "incomplete credentials" });
});
return;
}
node.options = {
clientId: clientIdFromCredentials(node.credentials),
username: node.credentials.id,
password: node.credentials.token,
proxy: node.credentials.proxy,
proxyUsername: node.credentials.proxyUsername,
proxyPassword: node.credentials.proxyPassword,
keepalive: 60,
reschedulePings: false,
clean: true,
reconnectPeriod: 60_000,
rejectUnauthorized: false,
};
if (node.credentials.proxy) {
const proxyUrl = new URL(node.credentials.proxy);
if (node.credentials.proxyUsername) {
proxyUrl.username = node.credentials.proxyUsername;
}
if (node.credentials.proxyPassword) {
proxyUrl.password = node.credentials.proxyPassword;
}
node.options.wsOptions = {
agent: new HttpsProxyAgent(proxyUrl.toString()),
};
}
node.brokerUrl = brokerUrlFromConfig(config);
node.mqttClient = mqtt.connect(node.brokerUrl, node.options);
node.status({ fill: "yellow", shape: "ring", text: "connecting" });
node.connecting = true;
node.mqttClient.on("offline", () => {
node.warn("Lost connection to broker");
});
node.mqttClient.on("reconnect", () => {
node.log("Attempting to reconnect to broker");
});
node.mqttClient.on("packetsend", (packet) => {
node.trace(`Packet sent: ${JSON.stringify(packet)}`);
});
node.mqttClient.on("packetreceive", (packet) => {
node.trace(`Packet received: ${JSON.stringify(packet)}`);
});
node.mqttClient.on("connect", () => {
node.connecting = false;
node.status({ fill: "green", shape: "dot", text: "connected" });
node.connected = true;
node.connectedOnce = true;
node.log(`Connected to ${node.brokerUrl} as ${node.options.clientId}`);
});
node.mqttClient.on("error", (error) => {
if (!node.connectedOnce && fatalErrorCodes.includes(error.code)) {
node.error(`Fatal ${error} (code ${error.code})`);
node.mqttClient.end();
} else {
node.error(`${error} (code ${error.code})`);
}
node.status({ fill: "red", shape: "ring", text: "error" });
});
node.mqttClient.on("close", () => {
if (node.connected) {
node.connected = false;
node.status({ fill: "red", shape: "ring", text: "disconnected" });
node.log(`Disconnected from ${node.brokerUrl} as ${node.options.clientId}`);
}
});
node.on("input", (msg) => {
const to = msg.to ? new Date(msg.to) : new Date();
const from = msg.from ? new Date(msg.from) : new Date(to.valueOf() - 30000);
const count = parseFloat(msg.count) || 0;
const topic = topicFromCredentials(node.credentials);
const options = { qos: 2, retain: false };
const message = buildMessage(from, to, count);
node.mqttClient.publish(topic, message, options);
node.debug(`Message ${message} sent to topic ${topic}`);
});
node.on("close", (done) => {
node.mqttClient.end();
done();
});
}
function OeeAiConnectorNode(config) {
RED.nodes.createNode(this, config);
const node = this;
// Migrations:
//
// `protocol` was previously:
// - `undefined` (until 2.3.0)
// - "wss" or "tcp" (until 2.9.0)
//
// Please make sure to keep this migration code equivalent to the one in the
// `.html` file of the node if you change it.
if (config.protocol === undefined || config.protocol === "tcp") {
if (config.server === "preview") {
config.protocol = "mqtt-1883";
} else if (config.server === "production") {
config.protocol = "mqtts-8883";
}
} else if (config.protocol === "wss") {
config.protocol = "wss-8084";
}
setupNode(node, config);
}
RED.nodes.registerType("oee-ai-connector", OeeAiConnectorNode, {
credentials: {
id: { type: "text" },
token: { type: "password" },
proxy: { type: "text" },
proxyUsername: { type: "text" },
proxyPassword: { type: "password" },
},
});
};