Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

271 lines
9.0KB

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