UNPKG

node-red-contrib-boolean-logic-ultimate

Version:

A set of Node-RED enhanced boolean logic node, flow interruption node, blinker node, invert node, filter node, with persisten values after reboot and more.

314 lines (266 loc) 11.1 kB
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; function setNodeStatus({ fill, shape, text }) { var 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" }); } // 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; } // 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; } // 15/11/2021 inform user about undefined topic or payload if (!msg.hasOwnProperty("payload") || msg.payload === undefined || msg.payload === null) { setNodeStatus({ fill: "red", shape: "dot", text: "Received invalid payload from " + msg.topic || "" }); return; } // 12/08/2021 Restrict only to true/false if (node.restrictinputevaluation) { if (msg.payload !== true && msg.payload !== false) { setNodeStatus({ fill: "red", shape: "dot", text: "Received non boolean value from " + msg.topic }); return; } } var topic = msg.topic; var payload = msg.payload; var value = ToBoolean(payload); // 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); } } // 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) { SetResult(resAND, resOR, resXOR, node.config.topic, msg); } else { setNodeStatus({ fill: "grey", shape: "ring", text: "Saved (" + (msg.hasOwnProperty("topic") ? msg.topic : "empty input topic") + ") " + value }); } } else { SetResult(resAND, resOR, resXOR, node.config.topic, msg); } } 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; } 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 ToBoolean(value) { var res = false; var decimal = /^\s*[+-]{0,1}\s*([\d]+(\.[\d]*)*)\s*$/ if (typeof value === 'boolean') { res = value; } else if (typeof value === 'number' || typeof value === 'string') { if (typeof value === "string" && value.toLowerCase() === "on") return true; if (typeof value === "string" && value.toLowerCase() === "off") return false; // Is it formated as a decimal number? if (decimal.test(value)) { var v = parseFloat(value); res = v != 0; } else { res = value.toLowerCase() === "true"; } } return res; }; function SetResult(_valueAND, _valueOR, _valueXOR, optionalTopic, _msg) { setNodeStatus({ fill: "green", shape: "dot", text: "(AND)" + (_valueAND !== null ? _valueAND : "---") + " (OR)" + (_valueOR !== null ? _valueOR : "---") + " (XOR)" + (_valueXOR !== null ? _valueXOR : "---") }); var msgAND = null; if (_valueAND != null) { msgAND = RED.util.cloneMessage(_msg); msgAND.topic = optionalTopic === undefined ? "result" : optionalTopic; msgAND.operation = "AND"; msgAND.payload = _valueAND; } var msgOR = null; if (_valueOR != null) { msgOR = RED.util.cloneMessage(_msg); msgOR.topic = optionalTopic === undefined ? "result" : optionalTopic; msgOR.operation = "OR"; msgOR.payload = _valueOR; } var msgXOR = null; if (_valueXOR != null) { msgXOR = RED.util.cloneMessage(_msg); msgXOR.topic = optionalTopic === undefined ? "result" : optionalTopic; msgXOR.operation = "XOR"; msgXOR.payload = _valueXOR; } node.send([msgAND, msgOR, msgXOR]); }; } RED.nodes.registerType("BooleanLogicUltimate", BooleanLogicUltimate); }