@@ -1,5 +1,9 @@ | |||
# node-red-contrib-boolean-logic-ultimate | |||
<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/> | |||
- Node status: cosmetic adjustments<br/> | |||
</p> |
@@ -51,10 +51,14 @@ Changing the topic is usually only needed when chaining multiple boolean nodes a | |||
<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> | |||
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/> | |||
@@ -8,6 +8,7 @@ | |||
}, | |||
filtertrue: {value:"both"}, | |||
persist: {value:true}, | |||
sInitializeWith: {value:"WaitForPayload"}, | |||
triggertopic: { | |||
value: "trigger", | |||
required: false | |||
@@ -90,6 +91,14 @@ | |||
$("#node-input-outputtriggeredby").val("all"); | |||
this.outputtriggeredby="all"; | |||
} | |||
// default | |||
if(typeof this.sInitializeWith === "undefined") | |||
{ | |||
$("#node-input-sInitializeWith").val("WaitForPayload"); | |||
this.sInitializeWith="WaitForPayload"; | |||
} | |||
}, | |||
oneditsave: function () { | |||
// Delete persistent state file | |||
@@ -124,7 +133,7 @@ | |||
<label for="node-input-outputtriggeredby"><i class="fa fa-filter"></i> Trigger mode</label> | |||
<select type="text" id="node-input-outputtriggeredby" placeholder="Event"> | |||
<option value="all">All topics</option> | |||
<option value="onlyonetopic">Single topic + only eval others</option> | |||
<option value="onlyonetopic">Single topic</option> | |||
</select> | |||
</div> | |||
<div class="form-row" id="triggertopic"> | |||
@@ -136,6 +145,14 @@ | |||
<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> | |||
<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> | |||
@@ -184,7 +201,15 @@ | |||
Every time you modify the node's config, <b>the retained values are cleared</b>.<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): | |||
<ol> | |||
<li>Boolean values are taken as-is.</li> | |||
@@ -193,5 +218,6 @@ | |||
</ol> | |||
<br> | |||
The XOR operation operates in a one, and only one mode, i.e. (A ^ B) ^ C ... ^ n | |||
</p> | |||
</script> |
@@ -1,14 +1,13 @@ | |||
module.exports = function(RED) { | |||
function BooleanLogicUltimate(config) { | |||
RED.nodes.createNode(this,config); | |||
this.config = config; | |||
this.state = {}; | |||
RED.nodes.createNode(this, config); | |||
var node = this; | |||
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 decimal = /^\s*[+-]{0,1}\s*([\d]+(\.[\d]*)*)\s*$/ | |||
// 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 }); | |||
@@ -21,8 +20,8 @@ module.exports = function(RED) { | |||
try { | |||
var contents = fs.readFileSync("states/" + node.id.toString()).toString(); | |||
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) { | |||
node.status({fill: "grey",shape: "ring",text: "No persistent states"}); | |||
@@ -32,23 +31,45 @@ module.exports = function(RED) { | |||
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) { | |||
var topic = msg.topic; | |||
var payload = msg.payload; | |||
if (topic !== undefined && payload !== undefined) { | |||
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 | |||
if (this.config.persist == true) { | |||
if (node.config.persist == true) { | |||
try { | |||
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) { | |||
node.status({fill: "red",shape: "dot",text: "Node cannot write to filesystem: " + error}); | |||
@@ -56,7 +77,7 @@ module.exports = function(RED) { | |||
} | |||
// 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 ) { | |||
@@ -93,7 +114,7 @@ module.exports = function(RED) { | |||
? node.config.name : "BooleanLogicUltimate") | |||
+ " [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.state = {}; | |||
node.jSonStates = {}; | |||
DeletePersistFile(node.id); | |||
DisplayUnkownStatus(); | |||
} else { | |||
@@ -122,6 +143,23 @@ module.exports = function(RED) { | |||
} catch (error) { | |||
_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) { | |||
@@ -132,12 +170,12 @@ module.exports = function(RED) { | |||
} | |||
else { | |||
// 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 ) { | |||
var key = keys[i]; | |||
res = PerformSimpleOperation( _operation, res, node.state[key] ); | |||
res = PerformSimpleOperation( _operation, res, node.jSonStates[key] ); | |||
} | |||
} | |||
@@ -149,8 +187,8 @@ module.exports = function(RED) { | |||
// 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.state ) { | |||
if( node.state[key] ) { | |||
for( var key in node.jSonStates ) { | |||
if( node.jSonStates[key] ) { | |||
trueCount++; | |||
} | |||
} | |||
@@ -194,16 +232,7 @@ module.exports = function(RED) { | |||
return res; | |||
}; | |||
function DisplayStatus ( value ) { | |||
node.status( | |||
{ | |||
fill: value ? "green" : "red", | |||
shape: value ? "dot" : "ring", | |||
text: value ? "true" : "false" | |||
} | |||
); | |||
}; | |||
function DisplayUnkownStatus () { | |||
node.status( | |||
{ |
@@ -1,6 +1,6 @@ | |||
{ | |||
"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", | |||
"author": "Supergiovane (https://github.com/Supergiovane)", | |||
"dependencies": { |