node-red-contrib-boolean-logic-ultimate
Version:
A set of Node-RED enhanced boolean logic and utility nodes, flow interruption, blinker, invert, filter, toggle etc.., with persistent values after reboot. Compatible also with Homeassistant values.
490 lines (452 loc) • 15.6 kB
JavaScript
module.exports = function (RED) {
function BooleanLogicUltimate(config) {
RED.nodes.createNode(this, config);
var node = this;
var fs = require("fs");
var path = require("path");
node.config = config;
node.jSonStates = {}; // JSON object containing the states.
node.sInitializeWith =
typeof node.config.sInitializeWith === "undefined"
? "WaitForPayload"
: node.config.sInitializeWith;
node.persistPath = path.join(
RED.settings.userDir,
"booleanlogicultimatepersist"
); // 26/10/2020 Contains the path for the states dir.
node.restrictinputevaluation =
config.restrictinputevaluation === undefined
? false
: config.restrictinputevaluation;
node.delayEvaluation =
config.delayEvaluation === undefined ? 0 : config.delayEvaluation; // 26/01/2022 Starts evaluating the inputs only after this amount of time is elapsed, after the last msg input or trigger
if (
isNaN(parseInt(node.delayEvaluation)) ||
parseInt(node.delayEvaluation) < 0
)
node.delayEvaluation = 0;
if (typeof node.delayEvaluation === "string")
node.delayEvaluation = parseInt(node.delayEvaluation);
node.timerDelayEvaluation = null;
node.inputMessage = {}; // 26/01/2022 input message is stored here.
function setNodeStatus({ fill, shape, text }) {
let dDate = new Date();
node.status({
fill: fill,
shape: shape,
text:
text +
" (" +
dDate.getDate() +
", " +
dDate.toLocaleTimeString() +
")",
});
}
// Helper for the config html, to be able to delete the peristent states file
RED.httpAdmin.get(
"/stateoperation_delete",
RED.auth.needsPermission("BooleanLogicUltimate.read"),
function (req, res) {
//node.send({ req: req });
// Detele the persist file
//var _node = RED.nodes.getNode(req.query.nodeid); // Gets node object from nodeit, because when called from the config html, the node object is not defined
var _nodeid = req.query.nodeid;
try {
if (fs.existsSync(path.join(node.persistPath, _nodeid.toString())))
fs.unlinkSync(path.join(node.persistPath, _nodeid.toString()));
} catch (error) {}
res.json({ status: 220 });
}
);
// 26/10/2020 Check for path and create it if doens't exists
if (!fs.existsSync(node.persistPath)) {
// Create the path
try {
fs.mkdirSync(node.persistPath);
// Backward compatibility: Copy old states dir into the new folder
if (fs.existsSync("states")) {
var filenames = fs.readdirSync("states");
filenames.forEach((file) => {
RED.log.info(
"BooleanLogicUltimate: migrating from old states path to the new persist " +
file
);
fs.copyFileSync(
"states/" + file,
path.join(node.persistPath, path.basename(file))
);
});
}
} catch (error) {
RED.log.error(
"BooleanLogicUltimate: error creating persistent folder. Check user permission to write to the filesystem " +
error.message
);
}
}
// Populate the state array with the persisten file
if (node.config.persist == true) {
try {
var contents = fs
.readFileSync(path.join(node.persistPath, node.id.toString()))
.toString();
if (typeof contents !== "undefined") {
node.jSonStates = JSON.parse(contents);
setNodeStatus({
fill: "blue",
shape: "ring",
text:
"Loaded persistent states (" +
Object.keys(node.jSonStates).length +
" total).",
});
}
} catch (error) {
setNodeStatus({
fill: "grey",
shape: "ring",
text: "No persistent states",
});
}
} else {
setNodeStatus({
fill: "yellow",
shape: "dot",
text: "Waiting for input states",
});
}
// Starts the evaluation delay timer, if needed
node.startTimerDelayEvaluation = () => {
if (node.timerDelayEvaluation !== null)
clearTimeout(node.timerDelayEvaluation);
node.timerDelayEvaluation = setTimeout(() => {
outputResult();
}, node.delayEvaluation);
};
// 14/08/2019 If some inputs are to be initialized, create a dummy items in the array
initUndefinedInputs();
this.on("input", function (msg) {
// 21/04/2021 Msg to reset all inputs
if (msg.hasOwnProperty("reset")) {
setNodeStatus({
fill: "blue",
shape: "ring",
text: "All inputs have been reset.",
});
node.jSonStates = [];
return;
}
// 26/01/2023 you can change the input count from msg
if (msg.hasOwnProperty("inputcount")) {
setTimeout(() => {
setNodeStatus({
fill: "grey",
shape: "dot",
text: "Input count changed to " + msg.inputcount,
});
}, 500);
try {
node.config.inputCount = Number(msg.inputcount);
} catch (error) {}
}
// 15/11/2021 inform user about undefined topic or payload
if (
!msg.hasOwnProperty("topic") ||
msg.topic === undefined ||
msg.topic === null
) {
setNodeStatus({
fill: "red",
shape: "dot",
text: "Received invalid topic!",
});
return;
}
var topic = msg.topic;
const utils = require("./utils.js");
let sPayload = utils.fetchFromObject(
msg,
config.payloadPropName || "payload"
);
// 12/08/2021 Restrict only to true/false
if (node.restrictinputevaluation) {
if (sPayload !== true && sPayload !== false) {
setNodeStatus({
fill: "red",
shape: "dot",
text: "Received non boolean value from " + msg.topic,
});
return;
}
}
var value = utils.ToBoolean(
sPayload,
RED.nodes.getNode(config.translatorConfig)// Retrieve the config node. It can be null, but it's handled in utils.js);
);
if (sPayload === undefined || value === undefined) {
// 15/11/2021 inform user about undefined topic or payload
setNodeStatus({
fill: "red",
shape: "dot",
text: "Received invalid payload from " + msg.topic || "",
});
return;
}
// 14/08/2019 if inputs are initialized, remove a "dummy" item from the state's array, as soon as new topic arrives
if (node.sInitializeWith !== "WaitForPayload") {
// Search if the current topic is in the state array
if (typeof node.jSonStates[topic] === "undefined") {
// Delete one dummy
for (let index = 0; index < node.config.inputCount; index++) {
if (node.jSonStates.hasOwnProperty("dummy" + index)) {
//RED.log.info(JSON.stringify(node.jSonStates))
delete node.jSonStates["dummy" + index];
//RED.log.info(JSON.stringify(node.jSonStates))
break;
}
}
}
}
// Add current attribute
node.jSonStates[topic] = value;
// Save the state array to a perisistent file
if (node.config.persist == true) {
try {
fs.writeFileSync(
path.join(node.persistPath, node.id.toString()),
JSON.stringify(node.jSonStates)
);
} catch (error) {
setNodeStatus({
fill: "red",
shape: "dot",
text: "Node cannot write to filesystem: " + error.message,
});
RED.log.error(
"BooleanLogicUltimate: unable to write to the filesystem. Check wether the user running node-red, has write permission to the filesysten. " +
error.message
);
}
}
node.inputMessage = msg; // 26/01/2022 Store MSG to be used in the outputResult function.
// Do we have as many inputs as we expect?
var keyCount = Object.keys(node.jSonStates).length;
if (keyCount == node.config.inputCount) {
// var resAND = CalculateResult("AND");
// var resOR = CalculateResult("OR");
// var resXOR = CalculateResult("XOR");
// if (node.config.filtertrue == "onlytrue") {
// if (!resAND) { resAND = null };
// if (!resOR) { resOR = null };
// if (!resXOR) { resXOR = null };
// }
// Operation mode evaluation
if (node.config.outputtriggeredby == "onlyonetopic") {
if (
typeof node.config.triggertopic !== "undefined" &&
node.config.triggertopic !== "" &&
msg.hasOwnProperty("topic") &&
msg.topic !== "" &&
node.config.triggertopic === msg.topic
) {
if (node.delayEvaluation > 0) {
node.startTimerDelayEvaluation();
setNodeStatus({
fill: "blue",
shape: "ring",
text: "Delay Eval " + node.delayEvaluation + "ms",
});
} else {
outputResult();
}
} else {
setNodeStatus({
fill: "grey",
shape: "ring",
text:
"Saved (" +
(msg.hasOwnProperty("topic")
? msg.topic
: "empty input topic") +
") " +
value,
});
}
} else {
if (node.delayEvaluation > 0) {
node.startTimerDelayEvaluation();
setNodeStatus({
fill: "blue",
shape: "ring",
text: "Delay Eval " + node.delayEvaluation + "ms",
});
} else {
outputResult();
}
}
} else if (keyCount > node.config.inputCount) {
setNodeStatus({
fill: "gray",
shape: "ring",
text: "Reset due to unexpected new topic",
});
DeletePersistFile();
} else {
setNodeStatus({
fill: "green",
shape: "ring",
text: "Arrived topic " + keyCount + " of " + node.config.inputCount,
});
}
});
this.on("close", function (removed, done) {
if (removed) {
// This node has been deleted
// Delete persistent states on change/deploy
DeletePersistFile();
} else {
// This node is being restarted
}
done();
});
function DeletePersistFile() {
// Detele the persist file
try {
if (fs.existsSync(path.join(node.persistPath, node.id.toString())))
fs.unlinkSync(path.join(node.persistPath, node.id.toString()));
setNodeStatus({
fill: "red",
shape: "ring",
text: "Persistent states deleted (" + node.id.toString() + ").",
});
} catch (error) {
setNodeStatus({
fill: "red",
shape: "ring",
text: "Error deleting persistent file: " + error.toString(),
});
}
node.jSonStates = {}; // Resets inputs
// 14/08/2019 If the inputs are to be initialized, create a dummy items in the array
initUndefinedInputs();
}
function initUndefinedInputs() {
if (node.sInitializeWith !== "WaitForPayload") {
var nTotalDummyToCreate =
Number(node.config.inputCount) - Object.keys(node.jSonStates).length;
if (nTotalDummyToCreate > 0) {
RED.log.info(
"BooleanLogicUltimate: Will create " +
nTotalDummyToCreate +
" dummy (" +
node.sInitializeWith +
") values"
);
for (let index = 0; index < nTotalDummyToCreate; index++) {
node.jSonStates["dummy" + index] =
node.sInitializeWith === "false" ? false : true;
}
let t = setTimeout(() => {
setNodeStatus({
fill: "green",
shape: "ring",
text:
"Initialized " +
nTotalDummyToCreate +
" undefined inputs with " +
node.sInitializeWith,
});
}, 4000);
}
}
}
function CalculateResult(_operation) {
var res;
if (_operation == "XOR") {
res = PerformXOR();
} else {
// We need a starting value to perform AND and OR operations.
var keys = Object.keys(node.jSonStates);
res = node.jSonStates[keys[0]];
for (var i = 1; i < keys.length; ++i) {
var key = keys[i];
res = PerformSimpleOperation(_operation, res, node.jSonStates[key]);
}
}
return res;
}
function PerformXOR() {
// XOR = exclusively one input is true. As such, we just count the number of true values and compare to 1.
var trueCount = 0;
for (var key in node.jSonStates) {
if (node.jSonStates[key]) {
trueCount++;
}
}
return trueCount == 1;
}
function PerformSimpleOperation(operation, val1, val2) {
var res;
if (operation === "AND") {
res = val1 && val2;
} else if (operation === "OR") {
res = val1 || val2;
} else {
node.error("Unknown operation: " + operation);
}
return res;
}
function outputResult() {
let optionalTopic = node.config.topic;
let calculatedValueAND = CalculateResult("AND");
let calculatedValueOR = CalculateResult("OR");
let calculatedValueXOR = CalculateResult("XOR");
if (node.config.filtertrue == "onlytrue") {
if (!calculatedValueAND) {
calculatedValueAND = null;
}
if (!calculatedValueOR) {
calculatedValueOR = null;
}
if (!calculatedValueXOR) {
calculatedValueXOR = null;
}
}
setNodeStatus({
fill: "green",
shape: "dot",
text:
"(AND)" +
(calculatedValueAND !== null ? calculatedValueAND : "---") +
" (OR)" +
(calculatedValueOR !== null ? calculatedValueOR : "---") +
" (XOR)" +
(calculatedValueXOR !== null ? calculatedValueXOR : "---"),
});
var msgAND = null;
if (calculatedValueAND != null) {
msgAND = RED.util.cloneMessage(node.inputMessage);
msgAND.topic = optionalTopic === undefined ? "result" : optionalTopic;
msgAND.operation = "AND";
msgAND.payload = calculatedValueAND;
}
var msgOR = null;
if (calculatedValueOR != null) {
msgOR = RED.util.cloneMessage(node.inputMessage);
msgOR.topic = optionalTopic === undefined ? "result" : optionalTopic;
msgOR.operation = "OR";
msgOR.payload = calculatedValueOR;
}
var msgXOR = null;
if (calculatedValueXOR != null) {
msgXOR = RED.util.cloneMessage(node.inputMessage);
msgXOR.topic = optionalTopic === undefined ? "result" : optionalTopic;
msgXOR.operation = "XOR";
msgXOR.payload = calculatedValueXOR;
}
node.send([msgAND, msgOR, msgXOR]);
}
}
RED.nodes.registerType("BooleanLogicUltimate", BooleanLogicUltimate);
};