You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
8.5KB

  1. module.exports = function(RED) {
  2. function BooleanLogicUltimate(config) {
  3. RED.nodes.createNode(this, config);
  4. var node = this;
  5. node.config = config;
  6. node.jSonStates = {}; // JSON object with elements. It's not an array! Format: {"Rain":true,"Dusk":true,"MotionSensor":true}
  7. node.sInitializeWith = typeof node.config.sInitializeWith === "undefined" ? "WaitForPayload" : node.config.sInitializeWith;
  8. var fs = require('fs');
  9. var decimal = /^\s*[+-]{0,1}\s*([\d]+(\.[\d]*)*)\s*$/
  10. // Helper for the config html, to be able to delete the peristent states file
  11. RED.httpAdmin.get("/stateoperation_delete", RED.auth.needsPermission('BooleanLogicUltimate.read'), function (req, res) {
  12. //node.send({ req: req });
  13. DeletePersistFile(req.query.nodeid);
  14. res.json({ status: 220 });
  15. });
  16. // Populate the state array with the persisten file
  17. if (node.config.persist == true) {
  18. try {
  19. var contents = fs.readFileSync("states/" + node.id.toString()).toString();
  20. if (typeof contents !== 'undefined') {
  21. node.jSonStates = JSON.parse(contents);
  22. node.status({fill: "blue",shape: "ring",text: "Loaded persistent states (" + Object.keys(node.jSonStates).length + " total)."});
  23. }
  24. } catch (error) {
  25. node.status({fill: "grey",shape: "ring",text: "No persistent states"});
  26. }
  27. } else {
  28. node.status({fill: "yellow",shape: "dot",text: "Waiting for input states"});
  29. }
  30. // 14/08/2019 If the inputs are to be initialized, create a dummy items in the array
  31. initUndefinedInputs();
  32. this.on('input', function (msg) {
  33. var topic = msg.topic;
  34. var payload = msg.payload;
  35. if (topic !== undefined && payload !== undefined) {
  36. var value = ToBoolean( payload );
  37. // 14/08/2019 if inputs are initialized, remove a "dummy" item from the state's array, as soon as new topic arrives
  38. if(node.sInitializeWith !== "WaitForPayload")
  39. {
  40. // Search if the current topic is in the state array
  41. if (typeof node.jSonStates[topic] === "undefined")
  42. {
  43. // Delete one dummy
  44. for (let index = 0; index < node.config.inputCount; index++) {
  45. if (node.jSonStates.hasOwnProperty("dummy" + index)) {
  46. //RED.log.info(JSON.stringify(node.jSonStates))
  47. delete node.jSonStates["dummy" + index];
  48. //RED.log.info(JSON.stringify(node.jSonStates))
  49. break;
  50. }
  51. }
  52. }
  53. }
  54. // Add current attribute
  55. node.jSonStates[topic] = value;
  56. // Save the state array to a perisistent file
  57. if (node.config.persist == true) {
  58. try {
  59. if (!fs.existsSync("states")) fs.mkdirSync("states");
  60. fs.writeFileSync("states/" + node.id.toString(),JSON.stringify(node.jSonStates));
  61. } catch (error) {
  62. node.status({fill: "red",shape: "dot",text: "Node cannot write to filesystem: " + error});
  63. }
  64. }
  65. // Do we have as many inputs as we expect?
  66. var keyCount = Object.keys(node.jSonStates).length;
  67. if( keyCount == node.config.inputCount ) {
  68. var resAND = CalculateResult("AND");
  69. var resOR = CalculateResult("OR");
  70. var resXOR = CalculateResult("XOR");
  71. if (node.config.filtertrue == "onlytrue") {
  72. if (!resAND) { resAND = null };
  73. if (!resOR) { resOR = null };
  74. if (!resXOR) { resXOR = null };
  75. }
  76. // Operation mode evaluation
  77. if (node.config.outputtriggeredby == "onlyonetopic") {
  78. if (typeof node.config.triggertopic !== "undefined"
  79. && node.config.triggertopic !== ""
  80. && msg.hasOwnProperty("topic") && msg.topic !==""
  81. && node.config.triggertopic === msg.topic)
  82. {
  83. SetResult(resAND, resOR, resXOR, node.config.topic);
  84. } else
  85. {
  86. node.status({ fill: "grey", shape: "ring", text: "Saved (" + (msg.hasOwnProperty("topic") ? msg.topic : "empty input topic") + ") " + value});
  87. }
  88. } else
  89. {
  90. SetResult(resAND, resOR, resXOR, node.config.topic);
  91. }
  92. }
  93. else if(keyCount > node.config.inputCount ) {
  94. node.warn(
  95. (node.config.name !== undefined && node.config.name.length > 0
  96. ? node.config.name : "BooleanLogicUltimate")
  97. + " [Logic]: More than the specified "
  98. + node.config.inputCount + " topics received, resetting. Will not output new value until " + node.config.inputCount + " new topics have been received.");
  99. node.jSonStates = {};
  100. DeletePersistFile(node.id);
  101. DisplayUnkownStatus();
  102. } else {
  103. node.status({ fill: "green", shape: "ring", text: " Arrived topic " + keyCount + " of " + node.config.inputCount});
  104. }
  105. }
  106. });
  107. this.on('close', function(removed, done) {
  108. if (removed) {
  109. // This node has been deleted
  110. // Delete persistent states on change/deploy
  111. DeletePersistFile(node.id);
  112. } else {
  113. // This node is being restarted
  114. }
  115. done();
  116. });
  117. function DeletePersistFile (_nodeid){
  118. // Detele the persist file
  119. var _node = RED.nodes.getNode(_nodeid); // Gets node object from nodeit, because when called from the config html, the node object is not defined
  120. try {
  121. if (fs.existsSync("states/" + _nodeid.toString())) fs.unlinkSync("states/" + _nodeid.toString());
  122. _node.status({fill: "red",shape: "ring",text: "Persistent states deleted ("+_nodeid.toString()+")."});
  123. } catch (error) {
  124. _node.status({fill: "red",shape: "ring",text: "Error deleting persistent file: " + error.toString()});
  125. }
  126. node.jSonStates = {}; // Resets inputs
  127. // 14/08/2019 If the inputs are to be initialized, create a dummy items in the array
  128. initUndefinedInputs();
  129. }
  130. function initUndefinedInputs() {
  131. if (node.sInitializeWith !== "WaitForPayload")
  132. {
  133. var nTotalDummyToCreate = Number(node.config.inputCount) - Object.keys(node.jSonStates).length;
  134. RED.log.info("BooleanLogicUltimate: Will create " + nTotalDummyToCreate + " dummy (" + node.sInitializeWith + ") values")
  135. for (let index = 0; index < nTotalDummyToCreate; index++) {
  136. node.jSonStates["dummy" + index] = node.sInitializeWith === "false" ? false : true;
  137. }
  138. if (nTotalDummyToCreate > 0) {
  139. setTimeout(() => { node.status({fill: "green",shape: "ring",text: "Initialized " + nTotalDummyToCreate + " undefined inputs with " + node.sInitializeWith});}, 4000)
  140. }
  141. }
  142. }
  143. function CalculateResult(_operation) {
  144. var res;
  145. if( _operation == "XOR") {
  146. res = PerformXOR();
  147. }
  148. else {
  149. // We need a starting value to perform AND and OR operations.
  150. var keys = Object.keys(node.jSonStates);
  151. res = node.jSonStates[keys[0]];
  152. for( var i = 1; i < keys.length; ++i ) {
  153. var key = keys[i];
  154. res = PerformSimpleOperation( _operation, res, node.jSonStates[key] );
  155. }
  156. }
  157. return res;
  158. }
  159. function PerformXOR()
  160. {
  161. // XOR = exclusively one input is true. As such, we just count the number of true values and compare to 1.
  162. var trueCount = 0;
  163. for( var key in node.jSonStates ) {
  164. if( node.jSonStates[key] ) {
  165. trueCount++;
  166. }
  167. }
  168. return trueCount == 1;
  169. }
  170. function PerformSimpleOperation( operation, val1, val2 ) {
  171. var res;
  172. if( operation === "AND" ) {
  173. res = val1 && val2;
  174. }
  175. else if( operation === "OR" ) {
  176. res = val1 || val2;
  177. }
  178. else {
  179. node.error( "Unknown operation: " + operation );
  180. }
  181. return res;
  182. }
  183. function ToBoolean( value ) {
  184. var res = false;
  185. if (typeof value === 'boolean') {
  186. res = value;
  187. }
  188. else if( typeof value === 'number' || typeof value === 'string' ) {
  189. // Is it formated as a decimal number?
  190. if( decimal.test( value ) ) {
  191. var v = parseFloat( value );
  192. res = v != 0;
  193. }
  194. else {
  195. res = value.toLowerCase() === "true";
  196. }
  197. }
  198. return res;
  199. };
  200. function DisplayUnkownStatus () {
  201. node.status(
  202. {
  203. fill: "gray",
  204. shape: "ring",
  205. text: "Reset due to unexpected new topic"
  206. });
  207. };
  208. function SetResult(_valueAND, _valueOR, _valueXOR, optionalTopic) {
  209. node.status({fill: "green",shape: "dot",text: "(AND)" + _valueAND + " (OR)" +_valueOR + " (XOR)" +_valueXOR});
  210. if (_valueAND!=null){
  211. var msgAND = {
  212. topic: optionalTopic === undefined ? "result" : optionalTopic,
  213. operation:"AND",
  214. payload: _valueAND
  215. };
  216. }
  217. if (_valueOR!=null){
  218. var msgOR = {
  219. topic: optionalTopic === undefined ? "result" : optionalTopic,
  220. operation:"OR",
  221. payload: _valueOR
  222. };
  223. }
  224. if (_valueXOR!=null){
  225. var msgXOR = {
  226. topic: optionalTopic === undefined ? "result" : optionalTopic,
  227. operation:"XOR",
  228. payload: _valueXOR
  229. };
  230. }
  231. node.send([msgAND,msgOR,msgXOR]);
  232. };
  233. }
  234. RED.nodes.registerType("BooleanLogicUltimate",BooleanLogicUltimate);
  235. }