@janart19/node-red-fusebox
Version:
A collection of Fusebox-specific custom nodes for Node-RED
356 lines (283 loc) • 15.2 kB
JavaScript
const http = require("http");
// Custom node to perform CRUD operations via the /calendar endpoint.
module.exports = function (RED) {
function SqlCalendarNode(config) {
RED.nodes.createNode(this, config);
const node = this;
var previousValues = {};
// Retrieve configuration settings
node.name = config.name;
node.topic = config.topic;
node.operationMode = config.operationMode;
node.eventId = config.eventId;
node.eventIdType = config.eventIdType;
node.title = config.title;
node.titleType = config.titleType;
node.description = config.description;
node.descriptionType = config.descriptionType;
node.value = config.value;
node.valueType = config.valueType;
node.timestamp = config.timestamp;
node.timestampType = config.timestampType;
node.start = config.start;
node.startType = config.startType;
node.end = config.end;
node.endType = config.endType;
node.source = config.source;
node.sourceType = config.sourceType;
// 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;
}
// Check for unnecessary form values
const invalidValues = ["", null, undefined];
const operationModeValid = ["check", "create", "read", "update", "delete"];
// Listen for input messages
node.on("input", function (msg) {
const operationMode = node.operationMode;
const topic = node.topic;
const id = parseInt(evaluate(node.eventId, node.eventIdType, node, msg));
const title = evaluate(node.title, node.titleType, node, msg);
const description = evaluate(node.description, node.descriptionType, node, msg);
const value = evaluate(node.value, node.valueType, node, msg);
let timestamp = evaluate(node.timestamp, node.timestampType, node, msg);
let ts_min = evaluate(node.start, node.startType, node, msg);
let ts_max = evaluate(node.end, node.endType, node, msg);
const source = evaluate(node.source, node.sourceType, node, msg);
// Convert date objects and ISO strings to unix timestamps (integers)
if (timestamp instanceof Date) timestamp = timestamp.getTime();
if (typeof timestamp === "string") timestamp = new Date(timestamp).getTime();
if (ts_min instanceof Date) ts_min = ts_min.getTime();
if (typeof ts_min === "string") ts_min = new Date(ts_min).getTime();
if (ts_max instanceof Date) ts_max = ts_max.getTime();
if (typeof ts_max === "string") ts_max = new Date(ts_max).getTime();
// Basic validation
if (!operationModeValid.includes(operationMode)) {
node.error(`Operation mode must be one of: ${operationModeValid.join(", ")}`);
node.status({ fill: "red", shape: "dot", text: `Invalid operation mode: ${operationMode}` });
return;
}
// Required: title, timestamp, source
// Optional: description, value
if (operationMode === "create") {
if (invalidValues.includes(title)) {
node.error("Event title required");
node.status({ fill: "red", shape: "dot", text: "Event title required" });
return;
}
if (invalidValues.includes(timestamp)) {
node.error("Timestamp required");
node.status({ fill: "red", shape: "dot", text: "Event timestamp required" });
return;
}
if (invalidValues.includes(source)) {
node.error("Event source required");
node.status({ fill: "red", shape: "dot", text: "Event source required" });
return;
}
}
// Required: eventId, title, timestamp
// Optional: description, value, source
if (operationMode === "update") {
if (invalidValues.includes(id) || isNaN(id)) {
node.error("Valid eventId required");
node.status({ fill: "red", shape: "dot", text: "Valid eventId required" });
return;
}
if (invalidValues.includes(title)) {
node.error("Event title required");
node.status({ fill: "red", shape: "dot", text: "Event title required" });
return;
}
if (invalidValues.includes(timestamp)) {
node.error("Timestamp required");
node.status({ fill: "red", shape: "dot", text: "Event timestamp required" });
return;
}
}
// Required: eventId OR mix of title, description, value, start, end, source
if (operationMode === "delete") {
if (
(invalidValues.includes(id) || isNaN(id)) &&
invalidValues.includes(title) &&
invalidValues.includes(description) &&
invalidValues.includes(value) &&
invalidValues.includes(ts_min) &&
invalidValues.includes(ts_max) &&
invalidValues.includes(source)
) {
node.error("At least one parameter required");
node.status({ fill: "red", shape: "dot", text: "At least one parameter required" });
return;
}
}
const parameters = { id, title, description, value, timestamp, ts_min, ts_max, source, operationMode };
// Initialize the previous values object
const previousRequest = getPreviousValue(parameters, "request");
// Skip if a request is already in progress for this row
if (previousRequest) {
node.status({ fill: "yellow", shape: "dot", text: `Request in progress for ${title || "event"} (${formatDate()})` });
return;
}
// Build the POST request
const postData = {
configuration: { id, title, description, value, timestamp, ts_min, ts_max, source, check: operationMode === "check" },
};
// Remove empty values
for (const key in postData.configuration) {
const val = postData.configuration[key];
if (invalidValues.includes(val) || (["id", "timestamp", "ts_min", "ts_max"].includes(key) && isNaN(val))) {
delete postData.configuration[key];
}
}
setPreviousRequest(parameters, true);
// Send the POST request to the controller
sendCalendarOperation(node, postData, parameters)
.then((result) => {
setPreviousRequest(parameters, false);
const outMsg = {
...msg,
...(!invalidValues.includes(topic) && { topic }),
};
node.send({ ...outMsg, payload: result, parameters, controller: { id: node.controller.id, uniqueId: node.controller.uniqueId, host: node.controller.host } });
})
.catch((error) => {
node.error(`Error sending calendar value: ${error}`, { error });
setPreviousRequest(parameters, false);
const outMsg = {
...msg,
...(!invalidValues.includes(topic) && { topic }),
};
node.send({ ...outMsg, payload: false, parameters, controller: { id: node.controller.id, uniqueId: node.controller.uniqueId, host: node.controller.host } });
});
});
// Method to query additional data via HTTP with retry mechanism
function sendCalendarOperation(node, postData = {}, parameters = {}, retries = 3) {
const { operationMode } = parameters;
let path = "/calendar";
let method = "GET";
if (operationMode === "create") method = "POST";
if (operationMode === "update") method = "PUT";
if (operationMode === "delete") method = "DELETE";
// Build query parameters for GET requests
if (method === "GET" || method === "DELETE") {
let parameters = "";
for (const key in postData.configuration) {
parameters += `&${key}=${encodeURIComponent(postData.configuration[key])}`;
}
parameters = parameters.slice(1);
if (parameters) path = `/calendar?${parameters}`;
}
const options = {
hostname: node.controller.host,
port: node.controller.httpPort,
path: path,
method: method,
headers: {
"Content-Type": "application/json",
},
};
node.debug(`Querying HTTP: ${JSON.stringify(options)} with body ${JSON.stringify(postData)}`);
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);
if (parsedData?.success === true || parsedData) {
node.status({
fill: "green",
shape: "dot",
text: `Calendar entry ${operationMode}${["read", "check"].includes(operationMode) ? "" : "d"} (${formatDate()})`,
});
resolve(parsedData);
} else {
if (retries > 0) {
node.warn(`Retrying... (${retries} attempts left)`);
node.status({ fill: "yellow", shape: "dot", text: `Retrying sending data (${formatDate()})` });
setTimeout(() => {
resolve(sendCalendarOperation(node, postData, parameters, retries - 1));
}, 500);
} else {
node.error(`Failed to send data`, parameters);
node.status({ fill: "red", shape: "dot", text: `Failed to send data (${formatDate()})` });
resolve(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(sendCalendarOperation(node, postData, parameters, 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(sendCalendarOperation(node, postData, parameters, retries - 1));
}, 500);
} else {
node.error(`HTTP request error: ${error}`, { error });
reject(error);
}
});
// Write data to request body
if (method !== "GET" && method !== "DELETE") {
req.write(JSON.stringify(postData));
}
req.end();
});
}
// Evaluate the value of a property, catching any errors (e.g. read properties of undefined : msg.payload.success)
function evaluate(value, type, node, msg) {
try {
return RED.util.evaluateNodeProperty(value, type, node, msg);
} catch (err) {
return undefined;
}
}
function getPreviousValue(parameters = {}, key = "title") {
const { title = "_default" } = parameters;
if (!previousValues[title]) previousValues[title] = { title: null, value: null, start: null, request: null };
return previousValues[title][key] ?? null;
}
function setPreviousRequest(parameters = {}, status = null) {
const { title = "_default" } = parameters;
previousValues[title].request = status;
}
// 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-sql-calendar", SqlCalendarNode);
};