@janart19/node-red-fusebox
Version:
A collection of Fusebox-specific custom nodes for Node-RED
203 lines (163 loc) • 8.28 kB
JavaScript
const http = require("http");
// Custom node to query the external connections of a controller: SDP, MQTT, VPN
module.exports = function (RED) {
function QueryExternalConnectionsNode(config) {
RED.nodes.createNode(this, config);
const node = this;
// Retrieve configuration settings
node.name = config.name;
node.interval = config.interval;
var previousValues = {};
var requestInProgress = false;
// Retrieve the config node's settings
node.controller = RED.nodes.getNode(config.controller);
// Validate the controller configuration
if (!node.controller || !node.controller.host || (!node.controller.httpPort && !node.controller.udpPort)) {
node.error("Controller configuration invalid");
node.status({ fill: "red", shape: "dot", text: "Controller configuration invalid" });
return;
}
// Validation constants
const invalidValues = ["", null, undefined];
const interval = node.interval;
// Basic validation
if (invalidValues.includes(interval) || isNaN(interval) || interval < 5 || interval > 7200) {
node.error(`Interval must be between 5 - 7200`);
node.status({ fill: "red", shape: "dot", text: `Invalid interval: ${interval}` });
return;
}
// Due to Promises, set default status before processing the data
node.status({
fill: "grey",
shape: "dot",
text: `Querying status every ${interval} seconds`,
});
// Query immediately and then periodically
setTimeout(queryHTTP, 1000);
const query = setInterval(queryHTTP, interval * 1000);
node.on("close", function (done) {
clearInterval(query);
done();
});
// Send the POST request to the controller
function queryHTTP() {
// Skip if a request is already in progress for this row
if (requestInProgress) {
node.status({ fill: "yellow", shape: "dot", text: `Request in progress (${formatDate()})` });
return;
}
requestInProgress = true;
queryStatus(node)
.then((obj) => {
let links = {};
if (obj.success) {
links = obj.links;
previousValues = { links, timestamp: Date.now() };
const connected = Object.values(links).filter((v) => v === true).length;
const total = Object.values(links).length;
node.status({
fill: "green",
shape: "dot",
text: `Connected ${connected} of ${total} (${formatDate()})`,
});
}
requestInProgress = false;
const controller = { id: node.controller.id, uniqueId: node.controller.uniqueId, host: node.controller.host };
// 1. SDP, 2. MQTT, 3. VPN
const sdpMsg = { topic: "sdp", payload: links?.sdp, controller };
const mqttMsg = { topic: "mqtt", payload: links?.mqtt, controller };
const vpnMsg = { topic: "vpn", payload: links?.vpn, controller };
node.send([sdpMsg, mqttMsg, vpnMsg]);
})
.catch((error) => {
node.error(`Error querying connection status: ${error}`, { error });
requestInProgress = false;
node.send({ payload: false, controller: { id: node.controller.id, uniqueId: node.controller.uniqueId, host: node.controller.host } });
});
}
// Method to query additional data via HTTP with retry mechanism
function queryStatus(node, retries = 3) {
const options = {
hostname: node.controller.host,
port: node.controller.httpPort,
path: "/links",
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
node.debug(`Querying HTTP: ${JSON.stringify(options)}`);
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
node.debug(`Received HTTP message: ${data}`);
const parsedData = JSON.parse(data);
const links = parsedData?.links;
if (links && Object.keys(links).length > 0) {
resolve({ success: true, links });
} else {
if (retries > 0) {
node.warn(`Retrying... (${retries} attempts left)`);
node.status({ fill: "yellow", shape: "dot", text: `Retrying connectivity query (${formatDate()})` });
setTimeout(() => {
resolve(queryStatus(node, retries - 1));
}, 500);
} else {
node.error(`Failed to query data`, { parsedData });
node.status({ fill: "red", shape: "dot", text: `Failed to query connection status (${formatDate()})` });
resolve({ success: false });
}
}
} catch (error) {
node.status({ fill: "red", shape: "dot", text: `Failed to parse HTTP response (${formatDate()})` });
// Retry if necessary
if (retries > 0) {
node.warn(`Retrying... (${retries} attempts left)`);
setTimeout(() => {
resolve(queryStatus(node, retries - 1));
}, 500);
} else {
node.error(`Failed to parse HTTP response: ${error}`, { error });
reject(error);
}
}
});
});
req.on("error", (error) => {
node.status({ fill: "red", shape: "dot", text: `HTTP request error (${formatDate()})` });
// Retry if necessary
if (retries > 0) {
node.warn(`Retrying... (${retries} attempts left)`);
setTimeout(() => {
resolve(queryStatus(node, retries - 1));
}, 500);
} else {
node.error(`HTTP request error: ${error}`, { error });
reject(error);
}
});
req.end();
});
}
// Format the current date and time as DD/MM/YYYY HH:MM:SS
function formatDate() {
const now = new Date();
const options = {
day: "2-digit",
month: "2-digit",
year: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false, // Use 24-hour format
};
return now.toLocaleString("en-GB", options); // 'en-GB' locale for DD/MM/YYYY format
}
}
RED.nodes.registerType("fusebox-query-external-connections", QueryExternalConnectionsNode);
};