# node-red-contrib-boolean-logic-ultimate | # node-red-contrib-boolean-logic-ultimate | ||||
<p> | <p> | ||||
<b>Version 1.0.4</b><br/> | |||||
- Added the option to initialize the undefined inputs with true or false. Thanks to this, the node is immediately operative (will not wait until all topis arrives).<br/> | |||||
</p> | |||||
<p> | |||||
<b>Version 1.0.3</b><br/> | <b>Version 1.0.3</b><br/> | ||||
- Node status: cosmetic adjustments<br/> | - Node status: cosmetic adjustments<br/> | ||||
</p> | </p> |
<li>Single topic + eval other inputs: <u>only whenever the node receives a msg input with the <b>specified topic</b> (having payload = true)</u>, it starts the evaluation of all other inputs as well and outputs the evaluated payload. If the node receives a msg input other than the <b>specified topic</b>), it only retains it's value for the boolean evaluation.</li> | <li>Single topic + eval other inputs: <u>only whenever the node receives a msg input with the <b>specified topic</b> (having payload = true)</u>, it starts the evaluation of all other inputs as well and outputs the evaluated payload. If the node receives a msg input other than the <b>specified topic</b>), it only retains it's value for the boolean evaluation.</li> | ||||
</ol> | </ol> | ||||
Example of trigger mode = Single topic + eval other inputs | |||||
```js | |||||
[{"id":"4d2e4d1.4a02034","type":"BooleanLogicUltimate","z":"5635003e.2d70f","name":"","filtertrue":"both","persist":true,"triggertopic":"MotionSensor","outputtriggeredby":"onlyonetopic","inputCount":"3","topic":"result","x":460,"y":580,"wires":[["a9e93fa0.99508"],[],[]]},{"id":"b3a4633e.ff06c","type":"inject","z":"5635003e.2d70f","name":"","topic":"MotionSensor","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":560,"wires":[["4d2e4d1.4a02034"]]},{"id":"150ff8fd.8110e7","type":"inject","z":"5635003e.2d70f","name":"","topic":"Dusk","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":760,"wires":[["4d2e4d1.4a02034"]]},{"id":"6ecd73e1.9ac75c","type":"inject","z":"5635003e.2d70f","name":"","topic":"Rain","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":660,"wires":[["4d2e4d1.4a02034"]]},{"id":"350fb477.b0d484","type":"inject","z":"5635003e.2d70f","name":"","topic":"Dusk","payload":"false","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":800,"wires":[["4d2e4d1.4a02034"]]},{"id":"1118f46a.b967ac","type":"inject","z":"5635003e.2d70f","name":"","topic":"MotionSensor","payload":"false","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":600,"wires":[["4d2e4d1.4a02034"]]},{"id":"4e793dec.646b4c","type":"inject","z":"5635003e.2d70f","name":"","topic":"Rain","payload":"false","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":240,"y":700,"wires":[["4d2e4d1.4a02034"]]},{"id":"a9e93fa0.99508","type":"debug","z":"5635003e.2d70f","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":650,"y":580,"wires":[]},{"id":"8d44deef.e81d","type":"comment","z":"5635003e.2d70f","name":"Switch on the light only when it's raining and it's dusk. The trigger is someone entering in the Motion Sensor's area.","info":"Triggers only if someone enters \nin the motion sensor's area","x":410,"y":520,"wires":[]}] | |||||
``` | |||||
<b>If input states are undefined</b><br /> | |||||
Every time you create a node or modify the node, all inputs are set to undefined. This means that the node will wait the arrive of all topics (for example 3 topics, if you've selected 3 topics in the option), before it can output a payload. This can be a problem if your logic must be operative as soon as you deploy the flow. To overcome this problem, you can "initialize" all the undefined inputs with True or False. | |||||
<ol> | |||||
<li>Leave undefined: Standard behaviour, the node will wait all the "undefined" topics to arrive, then starts a flow with the result.</li> | |||||
<li>True or False: The node is immediately operative, by force the initialization of the "undefined" inputs with "true" or "false".</li> | |||||
</ol> | |||||
<br/> | |||||
<br/><br/> | <br/><br/> | ||||
}, | }, | ||||
filtertrue: {value:"both"}, | filtertrue: {value:"both"}, | ||||
persist: {value:true}, | persist: {value:true}, | ||||
sInitializeWith: {value:"WaitForPayload"}, | |||||
triggertopic: { | triggertopic: { | ||||
value: "trigger", | value: "trigger", | ||||
required: false | required: false | ||||
$("#node-input-outputtriggeredby").val("all"); | $("#node-input-outputtriggeredby").val("all"); | ||||
this.outputtriggeredby="all"; | this.outputtriggeredby="all"; | ||||
} | } | ||||
// default | |||||
if(typeof this.sInitializeWith === "undefined") | |||||
{ | |||||
$("#node-input-sInitializeWith").val("WaitForPayload"); | |||||
this.sInitializeWith="WaitForPayload"; | |||||
} | |||||
}, | }, | ||||
oneditsave: function () { | oneditsave: function () { | ||||
// Delete persistent state file | // Delete persistent state file | ||||
<label for="node-input-outputtriggeredby"><i class="fa fa-filter"></i> Trigger mode</label> | <label for="node-input-outputtriggeredby"><i class="fa fa-filter"></i> Trigger mode</label> | ||||
<select type="text" id="node-input-outputtriggeredby" placeholder="Event"> | <select type="text" id="node-input-outputtriggeredby" placeholder="Event"> | ||||
<option value="all">All topics</option> | <option value="all">All topics</option> | ||||
<option value="onlyonetopic">Single topic + only eval others</option> | |||||
<option value="onlyonetopic">Single topic</option> | |||||
</select> | </select> | ||||
</div> | </div> | ||||
<div class="form-row" id="triggertopic"> | <div class="form-row" id="triggertopic"> | ||||
<label style="width:auto" for="node-input-persist"> Remember latest input values after reboot</label> <input type="checkbox" id="node-input-persist" style="display:inline-block; width:auto; vertical-align:top;"> | <label style="width:auto" for="node-input-persist"> Remember latest input values after reboot</label> <input type="checkbox" id="node-input-persist" style="display:inline-block; width:auto; vertical-align:top;"> | ||||
<div id="helpallga"><i> If checked, the input values are retained <br /> after a node-red reboot.<br/>If you reboot your node-red, <br />you don't need to wait all inputs <br />to arrive and initialize the node, <br />before the node can output a payload.</div> | <div id="helpallga"><i> If checked, the input values are retained <br /> after a node-red reboot.<br/>If you reboot your node-red, <br />you don't need to wait all inputs <br />to arrive and initialize the node, <br />before the node can output a payload.</div> | ||||
</div> | </div> | ||||
<div class="form-row"> | |||||
<label for="node-input-sInitializeWith"><i class="fa fa-home"></i> If input states are undefined</label> | |||||
<select type="text" id="node-input-sInitializeWith" placeholder=""> | |||||
<option value="WaitForPayload">Leave undefined</option> | |||||
<option value="false">Initialize all with False</option> | |||||
<option value="true">Initialize all with True</option> | |||||
</select> | |||||
</div> | |||||
</script> | </script> | ||||
Every time you modify the node's config, <b>the retained values are cleared</b>.<br/> | Every time you modify the node's config, <b>the retained values are cleared</b>.<br/> | ||||
<br/> | <br/> | ||||
<b> If input states are undefined</b><br /> | |||||
Every time you create a node or modify the node, all inputs are set to undefined. This means that the node will wait the arrive of all topics (for example 3 topics, if you've selected 3 topics in the option), before it can output a payload. This can be a problem if your logic must be operative as soon as you deploy the flow. To overcome this problem, you can "initialize" all the undefined inputs with True or False. | |||||
<ol> | |||||
<li>Leave undefined: Standard behaviour, the node will wait all the "undefined" topics to arrive, then starts a flow with the result.</li> | |||||
<li>True or False: The node is immediately operative, by force the initialization of the "undefined" inputs with "true" or "false".</li> | |||||
</ol> | |||||
<br/> | |||||
<bi How inputs are handled:</i><br /> | |||||
All incoming msg.payloads are converted into a boolean value according to the following rules (this applies to all boolean logic nodes): | All incoming msg.payloads are converted into a boolean value according to the following rules (this applies to all boolean logic nodes): | ||||
<ol> | <ol> | ||||
<li>Boolean values are taken as-is.</li> | <li>Boolean values are taken as-is.</li> | ||||
</ol> | </ol> | ||||
<br> | <br> | ||||
The XOR operation operates in a one, and only one mode, i.e. (A ^ B) ^ C ... ^ n | The XOR operation operates in a one, and only one mode, i.e. (A ^ B) ^ C ... ^ n | ||||
</p> | </p> | ||||
</script> | </script> |
module.exports = function(RED) { | module.exports = function(RED) { | ||||
function BooleanLogicUltimate(config) { | function BooleanLogicUltimate(config) { | ||||
RED.nodes.createNode(this,config); | |||||
this.config = config; | |||||
this.state = {}; | |||||
RED.nodes.createNode(this, config); | |||||
var node = this; | var node = this; | ||||
node.config = config; | node.config = config; | ||||
node.jSonStates = {}; // JSON object with elements. It's not an array! Format: {"Rain":true,"Dusk":true,"MotionSensor":true} | |||||
node.sInitializeWith = typeof node.config.sInitializeWith === "undefined" ? "WaitForPayload" : node.config.sInitializeWith; | |||||
var fs = require('fs'); | var fs = require('fs'); | ||||
var decimal = /^\s*[+-]{0,1}\s*([\d]+(\.[\d]*)*)\s*$/ | var decimal = /^\s*[+-]{0,1}\s*([\d]+(\.[\d]*)*)\s*$/ | ||||
// Helper for the config html, to be able to delete the peristent states file | // 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) { | RED.httpAdmin.get("/stateoperation_delete", RED.auth.needsPermission('BooleanLogicUltimate.read'), function (req, res) { | ||||
//node.send({ req: req }); | //node.send({ req: req }); | ||||
try { | try { | ||||
var contents = fs.readFileSync("states/" + node.id.toString()).toString(); | var contents = fs.readFileSync("states/" + node.id.toString()).toString(); | ||||
if (typeof contents !== 'undefined') { | if (typeof contents !== 'undefined') { | ||||
node.state = JSON.parse(contents); | |||||
node.status({fill: "blue",shape: "ring",text: "Loaded persistent states (" + Object.keys(node.state).length + " total)."}); | |||||
node.jSonStates = JSON.parse(contents); | |||||
node.status({fill: "blue",shape: "ring",text: "Loaded persistent states (" + Object.keys(node.jSonStates).length + " total)."}); | |||||
} | } | ||||
} catch (error) { | } catch (error) { | ||||
node.status({fill: "grey",shape: "ring",text: "No persistent states"}); | node.status({fill: "grey",shape: "ring",text: "No persistent states"}); | ||||
node.status({fill: "yellow",shape: "dot",text: "Waiting for input states"}); | node.status({fill: "yellow",shape: "dot",text: "Waiting for input states"}); | ||||
} | } | ||||
// 14/08/2019 If the inputs are to be initialized, create a dummy items in the array | |||||
initUndefinedInputs(); | |||||
this.on('input', function (msg) { | this.on('input', function (msg) { | ||||
var topic = msg.topic; | var topic = msg.topic; | ||||
var payload = msg.payload; | var payload = msg.payload; | ||||
if (topic !== undefined && payload !== undefined) { | if (topic !== undefined && payload !== undefined) { | ||||
var value = ToBoolean( payload ); | var value = ToBoolean( payload ); | ||||
var state = node.state; | |||||
// 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; | |||||
state[topic] = value; | |||||
// Save the state array to a perisistent file | // Save the state array to a perisistent file | ||||
if (this.config.persist == true) { | |||||
if (node.config.persist == true) { | |||||
try { | try { | ||||
if (!fs.existsSync("states")) fs.mkdirSync("states"); | if (!fs.existsSync("states")) fs.mkdirSync("states"); | ||||
fs.writeFileSync("states/" + node.id.toString(),JSON.stringify(state)); | |||||
fs.writeFileSync("states/" + node.id.toString(),JSON.stringify(node.jSonStates)); | |||||
} catch (error) { | } catch (error) { | ||||
node.status({fill: "red",shape: "dot",text: "Node cannot write to filesystem: " + error}); | node.status({fill: "red",shape: "dot",text: "Node cannot write to filesystem: " + error}); | ||||
} | } | ||||
// Do we have as many inputs as we expect? | // Do we have as many inputs as we expect? | ||||
var keyCount = Object.keys(state).length; | |||||
var keyCount = Object.keys(node.jSonStates).length; | |||||
if( keyCount == node.config.inputCount ) { | if( keyCount == node.config.inputCount ) { | ||||
? node.config.name : "BooleanLogicUltimate") | ? node.config.name : "BooleanLogicUltimate") | ||||
+ " [Logic]: More than the specified " | + " [Logic]: More than the specified " | ||||
+ node.config.inputCount + " topics received, resetting. Will not output new value until " + node.config.inputCount + " new topics have been received."); | + node.config.inputCount + " topics received, resetting. Will not output new value until " + node.config.inputCount + " new topics have been received."); | ||||
node.state = {}; | |||||
node.jSonStates = {}; | |||||
DeletePersistFile(node.id); | DeletePersistFile(node.id); | ||||
DisplayUnkownStatus(); | DisplayUnkownStatus(); | ||||
} else { | } else { | ||||
} catch (error) { | } catch (error) { | ||||
_node.status({fill: "red",shape: "ring",text: "Error deleting persistent file: " + error.toString()}); | _node.status({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; | |||||
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; | |||||
} | |||||
if (nTotalDummyToCreate > 0) { | |||||
setTimeout(() => { node.status({fill: "green",shape: "ring",text: "Initialized " + nTotalDummyToCreate + " undefined inputs with " + node.sInitializeWith});}, 4000) | |||||
} | |||||
} | |||||
} | } | ||||
function CalculateResult(_operation) { | function CalculateResult(_operation) { | ||||
} | } | ||||
else { | else { | ||||
// We need a starting value to perform AND and OR operations. | // We need a starting value to perform AND and OR operations. | ||||
var keys = Object.keys(node.state); | |||||
res = node.state[keys[0]]; | |||||
var keys = Object.keys(node.jSonStates); | |||||
res = node.jSonStates[keys[0]]; | |||||
for( var i = 1; i < keys.length; ++i ) { | for( var i = 1; i < keys.length; ++i ) { | ||||
var key = keys[i]; | var key = keys[i]; | ||||
res = PerformSimpleOperation( _operation, res, node.state[key] ); | |||||
res = PerformSimpleOperation( _operation, res, node.jSonStates[key] ); | |||||
} | } | ||||
} | } | ||||
// XOR = exclusively one input is true. As such, we just count the number of true values and compare to 1. | // XOR = exclusively one input is true. As such, we just count the number of true values and compare to 1. | ||||
var trueCount = 0; | var trueCount = 0; | ||||
for( var key in node.state ) { | |||||
if( node.state[key] ) { | |||||
for( var key in node.jSonStates ) { | |||||
if( node.jSonStates[key] ) { | |||||
trueCount++; | trueCount++; | ||||
} | } | ||||
} | } | ||||
return res; | return res; | ||||
}; | }; | ||||
function DisplayStatus ( value ) { | |||||
node.status( | |||||
{ | |||||
fill: value ? "green" : "red", | |||||
shape: value ? "dot" : "ring", | |||||
text: value ? "true" : "false" | |||||
} | |||||
); | |||||
}; | |||||
function DisplayUnkownStatus () { | function DisplayUnkownStatus () { | ||||
node.status( | node.status( | ||||
{ | { |
{ | { | ||||
"name": "node-red-contrib-boolean-logic-ultimate", | "name": "node-red-contrib-boolean-logic-ultimate", | ||||
"version": "1.0.3", | |||||
"version": "1.0.4", | |||||
"description": "A set of Node-RED enhanced boolean logic, with persisten values after reboot and more", | "description": "A set of Node-RED enhanced boolean logic, with persisten values after reboot and more", | ||||
"author": "Supergiovane (https://github.com/Supergiovane)", | "author": "Supergiovane (https://github.com/Supergiovane)", | ||||
"dependencies": { | "dependencies": { |