smart-nodes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +127 -0
  4. package/central/central.html +328 -0
  5. package/central/central.js +95 -0
  6. package/compare/compare.html +137 -0
  7. package/compare/compare.js +151 -0
  8. package/delay/delay.html +192 -0
  9. package/delay/delay.js +175 -0
  10. package/examples/central.json +804 -0
  11. package/examples/central.png +0 -0
  12. package/examples/compare.json +916 -0
  13. package/examples/compare.png +0 -0
  14. package/examples/delay.json +198 -0
  15. package/examples/delay.png +0 -0
  16. package/examples/forwarder.json +152 -0
  17. package/examples/forwarder.png +0 -0
  18. package/examples/hysteresis.json +358 -0
  19. package/examples/hysteresis.png +0 -0
  20. package/examples/light-control.json +499 -0
  21. package/examples/light-control.png +0 -0
  22. package/examples/logic.json +562 -0
  23. package/examples/logic.png +0 -0
  24. package/examples/long-press-control.json +113 -0
  25. package/examples/long-press-control.png +0 -0
  26. package/examples/multi-press-control.json +136 -0
  27. package/examples/multi-press-control.png +0 -0
  28. package/examples/scene-control.json +535 -0
  29. package/examples/scene-control.png +0 -0
  30. package/examples/scheduler.json +164 -0
  31. package/examples/scheduler.png +0 -0
  32. package/examples/shutter-complex-control.json +489 -0
  33. package/examples/shutter-complex-control.png +0 -0
  34. package/examples/shutter-control.json +457 -0
  35. package/examples/shutter-control.png +0 -0
  36. package/examples/statistic.json +1112 -0
  37. package/examples/statistic.png +0 -0
  38. package/forwarder/forwarder.html +100 -0
  39. package/forwarder/forwarder.js +95 -0
  40. package/hysteresis/hysteresis.html +152 -0
  41. package/hysteresis/hysteresis.js +146 -0
  42. package/light-control/light-control.html +358 -0
  43. package/light-control/light-control.js +231 -0
  44. package/logic/logic.html +168 -0
  45. package/logic/logic.js +171 -0
  46. package/long-press-control/long-press-control.html +74 -0
  47. package/long-press-control/long-press-control.js +75 -0
  48. package/multi-press-control/multi-press-control.html +135 -0
  49. package/multi-press-control/multi-press-control.js +68 -0
  50. package/package.json +59 -0
  51. package/persistence.js +74 -0
  52. package/scene-control/scene-control.html +575 -0
  53. package/scene-control/scene-control.js +265 -0
  54. package/scheduler/scheduler.html +338 -0
  55. package/scheduler/scheduler.js +209 -0
  56. package/shutter-complex-control/shutter-complex-control.html +330 -0
  57. package/shutter-complex-control/shutter-complex-control.js +399 -0
  58. package/shutter-control/shutter-control.html +283 -0
  59. package/shutter-control/shutter-control.js +208 -0
  60. package/smart_helper.js +156 -0
  61. package/statistic/statistic.html +107 -0
  62. package/statistic/statistic.js +196 -0
@@ -0,0 +1,208 @@
1
+ module.exports = function (RED)
2
+ {
3
+ function ShutterControlNode(config)
4
+ {
5
+ const node = this;
6
+ RED.nodes.createNode(node, config);
7
+
8
+ const smartContext = require("../persistence.js")(RED);
9
+ const helper = require("../smart_helper.js");
10
+
11
+ // persistent values
12
+ var nodeSettings = Object.assign({}, {
13
+ last_position: 0, // 0 = opened, 100 = closed
14
+ last_direction_up: true, // remember last direction for toggle action
15
+ }, smartContext.get(node.id));
16
+
17
+ // dynamic config
18
+
19
+ // runtime values
20
+ let is_running = false; // remember if shutter is running, this is only recognized when starting within this control or position 0% or 100% is reached.
21
+ let max_time_on_timeout = null; // save timeout value to cancel auto timeout if needed
22
+
23
+ // central handling
24
+ var event = "node:" + config.id;
25
+ var handler = function (msg)
26
+ {
27
+ node.receive(msg);
28
+ }
29
+ RED.events.on(event, handler);
30
+
31
+ node.status({ fill: "red", shape: "dot", text: "Stopped at " + Math.round(nodeSettings.last_position) + "%" });
32
+
33
+ node.on("input", function (msg)
34
+ {
35
+ handleTopic(msg);
36
+
37
+ smartContext.set(node.id, nodeSettings);
38
+ });
39
+
40
+ node.on("close", function ()
41
+ {
42
+ stopAutoOff();
43
+ RED.events.off(event, handler);
44
+ });
45
+
46
+ let handleTopic = msg =>
47
+ {
48
+ var resultUpDown = null;
49
+ var resultStop = null;
50
+ var resultPosition = null;
51
+
52
+ var realTopic = helper.getTopicName(msg.topic);
53
+
54
+ // set default topic
55
+ if (!["status", "status_position", "up_down", "up", "up_stop", "down", "down_stop", "stop", "toggle", "position"].includes(realTopic))
56
+ realTopic = "toggle";
57
+
58
+ // skip if button is released
59
+ if (msg.payload === false && ["up", "up_stop", "down", "down_stop", "stop", "toggle"].includes(realTopic))
60
+ return;
61
+
62
+ // Correct next topic to avoid handling up_stop, down_stop or toggle separately.
63
+ if ((max_time_on_timeout != null || is_running) && (realTopic == "up_stop" || realTopic == "down_stop" || realTopic == "toggle"))
64
+ {
65
+ realTopic = "stop";
66
+ }
67
+ else if (max_time_on_timeout == null && !is_running)
68
+ {
69
+ // shutter is not running, set next command depending on topic
70
+ if (realTopic == "up_stop")
71
+ realTopic = "up";
72
+ else if (realTopic == "down_stop")
73
+ realTopic = "down";
74
+ else if (nodeSettings.last_direction_up && realTopic == "toggle")
75
+ realTopic = "down";
76
+ else if (!nodeSettings.last_direction_up && realTopic == "toggle")
77
+ realTopic = "up";
78
+ }
79
+
80
+
81
+ switch (realTopic)
82
+ {
83
+ case "status":
84
+ case "status_position":
85
+ nodeSettings.last_direction_up = nodeSettings.last_position > msg.payload;
86
+ nodeSettings.last_position = msg.payload;
87
+
88
+ if (is_running && (msg.payload == 0 || msg.payload == 100))
89
+ is_running = false;
90
+
91
+ node.status({ fill: "yellow", shape: "ring", text: "Position status received: " + msg.payload + "%" });
92
+ return;
93
+
94
+ // This is only used to track starting of the shutter
95
+ case "up_down":
96
+ nodeSettings.last_direction_up = !msg.payload;
97
+ is_running = true;
98
+
99
+ if (nodeSettings.last_direction_up)
100
+ node.status({ fill: "green", shape: "dot", text: "Up" });
101
+ else
102
+ node.status({ fill: "green", shape: "dot", text: "Down" });
103
+ return;
104
+
105
+ case "up":
106
+ nodeSettings.last_direction_up = true;
107
+ is_running = true;
108
+ resultUpDown = false;
109
+ node.status({ fill: "green", shape: "dot", text: "Up" });
110
+ startAutoOffIfNeeded(msg);
111
+ break;
112
+
113
+ case "stop":
114
+ is_running = false;
115
+ resultStop = true;
116
+ stopAutoOff();
117
+ node.status({ fill: "green", shape: "dot", text: "Stopped" });
118
+ break;
119
+
120
+ case "down":
121
+ nodeSettings.last_direction_up = false;
122
+ is_running = true;
123
+ resultUpDown = true;
124
+ node.status({ fill: "green", shape: "dot", text: "Down" });
125
+ startAutoOffIfNeeded(msg);
126
+ break;
127
+
128
+ case "position":
129
+ let value = parseFloat(msg.payload);
130
+
131
+ if (value < 0) value = 0;
132
+ if (value > 100) value = 100;
133
+ // is_running = true; // Not guaranteed that the shutter starts running.
134
+ resultPosition = value;
135
+ node.status({ fill: "green", shape: "dot", text: "Set position to " + value + "%" });
136
+ break;
137
+ }
138
+
139
+ if (resultUpDown != null)
140
+ {
141
+ node.send([{ payload: resultUpDown, topic: "up_down" }, null, null]);
142
+ notifyCentral(true);
143
+ }
144
+ else if (resultStop != null)
145
+ {
146
+ node.send([null, { payload: resultStop, topic: "stop" }, null]);
147
+ notifyCentral(false);
148
+ }
149
+ else if (resultPosition != null)
150
+ {
151
+ node.send([null, null, { payload: resultPosition, topic: "position" }]);
152
+ }
153
+ };
154
+
155
+ let startAutoOffIfNeeded = msg =>
156
+ {
157
+ if (!msg.time_on)
158
+ return;
159
+
160
+ let timeMs = helper.getTimeInMsFromString(msg.time_on);
161
+ if (isNaN(timeMs))
162
+ {
163
+ node.status({ fill: "red", shape: "dot", text: "Invalid time_on value send: " + msg.time_on });
164
+ return;
165
+ }
166
+
167
+ if (timeMs <= 0)
168
+ {
169
+ node.status({ fill: "red", shape: "dot", text: "time_on value has to be greater than 0" });
170
+ return;
171
+ }
172
+
173
+ // Stop if any timeout is set
174
+ stopAutoOff();
175
+
176
+ node.status({ fill: "yellow", shape: "ring", text: "Wait " + (timeMs / 1000).toFixed(1) + " sec for auto off" });
177
+ max_time_on_timeout = setTimeout(() =>
178
+ {
179
+ node.status({ fill: "green", shape: "dot", text: "Stopped" });
180
+ is_running = false;
181
+ node.send([null, { payload: true }, null]);
182
+ notifyCentral(false);
183
+ }, timeMs);
184
+ };
185
+
186
+ let stopAutoOff = () =>
187
+ {
188
+ if (max_time_on_timeout != null)
189
+ {
190
+ node.status({});
191
+ clearTimeout(max_time_on_timeout);
192
+ max_time_on_timeout = null;
193
+ }
194
+ };
195
+
196
+ let notifyCentral = state =>
197
+ {
198
+ if (!config.links)
199
+ return;
200
+
201
+ config.links.forEach(link =>
202
+ {
203
+ RED.events.emit("node:" + link, { source: node.id, state: state });
204
+ });
205
+ };
206
+ }
207
+ RED.nodes.registerType("smart_shutter-control", ShutterControlNode);
208
+ };
@@ -0,0 +1,156 @@
1
+ module.exports = {
2
+ evaluateNodeProperty(RED, value, type)
3
+ {
4
+ try
5
+ {
6
+ switch (type)
7
+ {
8
+ case "null":
9
+ return null;
10
+
11
+ default:
12
+ return RED.util.evaluateNodeProperty(value, type);
13
+ }
14
+ }
15
+ catch (error)
16
+ {
17
+ console.error(error);
18
+ return null;
19
+ }
20
+ },
21
+ getTopicName(topic)
22
+ {
23
+ if (typeof topic == "undefined" || topic == null || topic == "" || typeof topic != "string")
24
+ return null;
25
+
26
+ if (topic.indexOf("#") == -1)
27
+ return topic;
28
+
29
+ return topic.substring(0, topic.indexOf("#"));
30
+ },
31
+ getTopicNumber(topic)
32
+ {
33
+ if (typeof topic == "undefined" || topic == null || topic == "")
34
+ return null;
35
+
36
+ if (typeof topic == "string")
37
+ {
38
+ if (topic.indexOf("#") != -1)
39
+ return parseInt(topic.substring(topic.indexOf("#") + 1), 10);
40
+ }
41
+
42
+ let result = parseInt(topic, 10);
43
+ if (isNaN(result) || !isFinite(result))
44
+ return null;
45
+
46
+ return result;
47
+ },
48
+ getTimeInMs(value, unit)
49
+ {
50
+ value = parseInt(value, 10);
51
+ if (isNaN(value) || value == 0)
52
+ return 0;
53
+
54
+ switch (unit)
55
+ {
56
+ case "ms":
57
+ return value;
58
+
59
+ case "s":
60
+ case "sec":
61
+ return value * 1000;
62
+
63
+ case "min":
64
+ return value * 60 * 1000;
65
+
66
+ case "h":
67
+ return value * 3600 * 1000;
68
+ }
69
+ },
70
+ getTimeInS(value, unit)
71
+ {
72
+ value = parseInt(value, 10);
73
+ if (isNaN(value) || value == 0)
74
+ return 0;
75
+
76
+ switch (unit)
77
+ {
78
+ case "ms":
79
+ return value / 1000;
80
+
81
+ case "s":
82
+ return value;
83
+
84
+ case "min":
85
+ return value * 60;
86
+
87
+ case "h":
88
+ return value * 3600;
89
+ }
90
+ },
91
+ getTimeInMsFromString(value)
92
+ {
93
+ // default in ms
94
+ if (typeof value == "number")
95
+ return value;
96
+
97
+ if (typeof value != "string")
98
+ return 0;
99
+
100
+ // Split 123min into ["123", "min"]
101
+ let values = value.match(/^([0-9]+)(ms|s|sec|m|min|h)?$/)
102
+
103
+ // string doesn't match
104
+ if (values == null)
105
+ return 0;
106
+
107
+ // default is ms
108
+ if (values.length == 2)
109
+ return this.getTimeInMs(values[1], "ms");
110
+
111
+ // default is ms
112
+ if (values.length == 3)
113
+ return this.getTimeInMs(values[1], values[2]);
114
+
115
+ // Something went wrong
116
+ return 0;
117
+ },
118
+ formatMsToStatus(value, timeConcatWord = null)
119
+ {
120
+ let result = "";
121
+
122
+ if (timeConcatWord)
123
+ {
124
+ let date = (new Date(Date.now() + value));
125
+ result = " " + timeConcatWord + " " + date.getHours() + ":" + ("" + date.getMinutes()).padStart(2, "0") + ":" + ("" + date.getSeconds()).padStart(2, "0");
126
+ }
127
+
128
+ if (value == 0)
129
+ return "0:00" + result;
130
+
131
+ // value in sec
132
+ value = parseInt(value / 1000, 10);
133
+ result = (value % 60) + result;
134
+ if (value % 60 < 10)
135
+ result = "0" + result;
136
+
137
+ // value in min
138
+ value = parseInt(value / 60, 10);
139
+ result = (value % 60) + ":" + result;
140
+ if (value % 60 < 10)
141
+ result = "0" + result;
142
+
143
+ // value in hour
144
+ value = parseInt(value / 60, 10);
145
+ result = (value % 24) + ":" + result;
146
+ if (value % 24 < 10)
147
+ result = "0" + result;
148
+
149
+ // value in days
150
+ value = parseInt(value / 24, 10);
151
+ if (value > 0)
152
+ result = value + "." + result;
153
+
154
+ return result;
155
+ }
156
+ };
@@ -0,0 +1,107 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("smart_statistic", {
3
+ category: "Smart Nodes",
4
+ paletteLabel: "Statistic",
5
+ color: "#E2D96E",
6
+ defaults: {
7
+ name: { value: "" },
8
+ operation: { value: "MIN" }, // MIN, MAX, SUM, DIFF, ABS, ABS_DIFF, AVG, MOV_AVG
9
+ count: { value: 10 },
10
+ save_state: { value: true },
11
+ resend_on_start: { value: true }
12
+ },
13
+ inputs: 1,
14
+ outputs: 1,
15
+ icon: "function.svg",
16
+ label: function ()
17
+ {
18
+ return this.name || this.operation || "Statistic";
19
+ },
20
+ oneditprepare: function ()
21
+ {
22
+ $("#node-input-operation")
23
+ .typedInput({
24
+ types: [
25
+ {
26
+ default: "MIN",
27
+ options: [
28
+ { value: "MIN", label: "Minimum" },
29
+ { value: "MAX", label: "Maximum" },
30
+ { value: "SUM", label: "Summe" },
31
+ { value: "DIFF", label: "Differenz" },
32
+ { value: "ABS", label: "Absoluter Wert" },
33
+ { value: "ABS_DIFF", label: "Absolute Differenz" },
34
+ { value: "AVG", label: "Durchschnitt" },
35
+ { value: "MOV_AVG", label: "Gleitender Mittelwert" }
36
+ ],
37
+ },
38
+ ],
39
+ });
40
+
41
+ $("#node-input-count").css("max-width", "4rem").spinner({
42
+ min: 2,
43
+ change: function (event, ui)
44
+ {
45
+ var value = parseInt(this.value);
46
+ value = isNaN(value) ? 0 : value;
47
+ value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
48
+ // value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
49
+ if (value !== this.value) $(this).spinner("value", value);
50
+ },
51
+ });
52
+
53
+ $("#node-input-operation").on("change", function (event, type, value)
54
+ {
55
+ if (value === "MOV_AVG")
56
+ $(".statistic-moving-average-row").show();
57
+ else
58
+ $(".statistic-moving-average-row").hide();
59
+ });
60
+
61
+ $("#node-input-save_state").on("change", ev =>
62
+ {
63
+ if (ev.target.checked)
64
+ $("#resend_on_start_row").show();
65
+ else
66
+ $("#resend_on_start_row").hide();
67
+ });
68
+ $("#node-input-save_state").trigger("change");
69
+ }
70
+ });
71
+ </script>
72
+
73
+ <script type="text/html" data-template-name="smart_statistic">
74
+ <div class="form-row">
75
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
76
+ <input type="text" id="node-input-name" placeholder="Name" />
77
+ </div>
78
+ <div class="form-row">
79
+ <label for="node-input-operation"><i class="fa fa-sort"></i> Operation</label>
80
+ <input id="node-input-operation" />
81
+ </div>
82
+ <div class="form-row statistic-moving-average-row" style="display: none;">
83
+ <label for="node-input-count"><i class="fa fa-hashtag"></i> Anzahl</label>
84
+ <input id="node-input-count" />
85
+ </div>
86
+ <hr/>
87
+ <h4 style="margin: 0.5rem 0;">Systemstart</h4>
88
+ <div class="form-row">
89
+ <input type="checkbox" id="node-input-save_state" style="width: 20px;" />
90
+ <label for="node-input-save_state" style="width: calc(100% - 30px);">Zustand speichern</label>
91
+ </div>
92
+ <div class="form-row" id="resend_on_start_row">
93
+ <input type="checkbox" id="node-input-resend_on_start" style="width: 20px;" />
94
+ <label for="node-input-resend_on_start" style="width: calc(100% - 30px);">Letze Nachricht 10 Sekunden nach dem Start senden</label>
95
+ </div>
96
+ </script>
97
+
98
+ <script type="text/html" data-help-name="smart_statistic">
99
+ <p>Dieser Knoten berechnet das Minimum, Maximum, die Summe, die Differenz, den absoluten Wert, die absolute Differenz, den Durchschnitt sowie den gleitenden Mittelwert verschiedener Werte.</p>
100
+ <p>Jeder Wert muss mit einem eigenen Topic gesendet werden. Kommt ein zweiter Wert mit dem gleichen Topic, wird der entsprechende Wert überschrieben.</p>
101
+ <p>Im Falle von MIN, MAX und ABS wird das entsprechende Topic, welches mit den Werten kam, mit ausgegeben. Bei SUM, DIFF, AVG und MOV_AVG handelt es sich um kombinierte Ergebnisse, weshalb Topic hier immer nicht gesetzt ist.</p>
102
+ <p>Für den absoluten Wert sowie den gleitenden Mittelwert spielt das Topic keine Rolle, jeder Wert der empfangen wird, wird für die Berechnung verwendet.</p>
103
+ <p>
104
+ <b>Hinweis:</b> Smart Nodes verwenden Topics im Format <code>name#nummer</code>, damit können verschiedene Smart Nodes mit dem gleichen Topic angesteuert werden.<br/>
105
+ Diese Node verwendet nur den Teil <code>nummer</code>. <code>name</code> und <code>#</code> sind dabei optional.
106
+ </p>
107
+ </script>
@@ -0,0 +1,196 @@
1
+ module.exports = function (RED)
2
+ {
3
+ function StatisticNode(config)
4
+ {
5
+ const node = this;
6
+ RED.nodes.createNode(node, config);
7
+
8
+ const smartContext = require("../persistence.js")(RED);
9
+ const helper = require("../smart_helper.js");
10
+
11
+ var nodeSettings = {
12
+ values: [],
13
+ lastMessage: null,
14
+ };
15
+
16
+ if (config.save_state)
17
+ {
18
+ // load old saved values
19
+ nodeSettings = Object.assign(nodeSettings, smartContext.get(node.id));
20
+ }
21
+ else
22
+ {
23
+ // delete old saved values
24
+ smartContext.del(node.id);
25
+ }
26
+
27
+ // dynamic config
28
+ let operation = config.operation
29
+ let count = config.count;
30
+
31
+ // runtime values
32
+
33
+
34
+ node.on("input", function (msg)
35
+ {
36
+ if (isNaN(parseFloat(msg.payload)))
37
+ {
38
+ // node.error("Invalid payload: " + msg.payload);
39
+ return;
40
+ }
41
+
42
+ let realTopic = helper.getTopicNumber(msg.topic) || 0;
43
+ // realTopic should be sended with 1-based, but internally 0-based is needed
44
+ realTopic--;
45
+
46
+ if (operation === "MOV_AVG")
47
+ {
48
+ nodeSettings.values.push(parseFloat(msg.payload));
49
+ if (nodeSettings.values.length > count)
50
+ nodeSettings.values.splice(0, 1);
51
+ }
52
+ else if (operation != "ABS")
53
+ {
54
+ if (typeof msg.topic === "undefined")
55
+ {
56
+ node.error("Topic not set");
57
+ return;
58
+ }
59
+ nodeSettings.values[realTopic] = parseFloat(msg.payload);
60
+ }
61
+
62
+ msg = getResult(msg);
63
+ setStatus(msg);
64
+
65
+ if (msg)
66
+ {
67
+ nodeSettings.lastMessage = msg;
68
+ node.send(msg);
69
+ }
70
+
71
+ if (config.save_state)
72
+ smartContext.set(node.id, nodeSettings);
73
+ });
74
+
75
+ node.on("close", function ()
76
+ {
77
+ });
78
+
79
+ let getResult = (msg) =>
80
+ {
81
+ let length;
82
+ if (operation !== "MOV_AVG" && operation !== "ABS")
83
+ {
84
+ length = Object.entries(nodeSettings.values).length;
85
+ if (length == 0)
86
+ return null;
87
+ }
88
+
89
+ let result = null;
90
+ switch (operation)
91
+ {
92
+ case "MIN":
93
+ result = Object.entries(nodeSettings.values).reduce((v1, v2) =>
94
+ {
95
+ if (v1[1] <= v2[1])
96
+ return v1;
97
+ return v2;
98
+ });
99
+ break;
100
+
101
+ case "MAX":
102
+ result = Object.entries(nodeSettings.values).reduce((v1, v2) =>
103
+ {
104
+ if (v1[1] >= v2[1])
105
+ return v1;
106
+ return v2;
107
+ });
108
+ break;
109
+
110
+ case "SUM":
111
+ result = Object.entries(nodeSettings.values).reduce((v1, v2) =>
112
+ {
113
+ return [null, v1[1] + v2[1]];
114
+ });
115
+ break;
116
+
117
+ case "DIFF":
118
+ if (nodeSettings.values.length >= 2)
119
+ {
120
+ result = [
121
+ null,
122
+ nodeSettings.values[0] - nodeSettings.values[1]
123
+ ];
124
+ }
125
+ break;
126
+
127
+ case "ABS":
128
+ msg.payload = Math.abs(msg.payload);
129
+ return msg;
130
+
131
+ case "ABS_DIFF":
132
+ if (nodeSettings.values.length >= 2)
133
+ {
134
+ result = [
135
+ null,
136
+ Math.abs(nodeSettings.values[0] - nodeSettings.values[1])
137
+ ];
138
+ }
139
+ break;
140
+
141
+ case "AVG":
142
+ let value = Object.entries(nodeSettings.values).reduce((v1, v2) =>
143
+ {
144
+ return [null, v1[1] + v2[1]];
145
+ });
146
+
147
+ return {
148
+ payload: value[1] / length
149
+ };
150
+
151
+ case "MOV_AVG":
152
+ msg.payload = nodeSettings.values.reduce((v1, v2) => v1 + v2) / nodeSettings.values.length;
153
+ return msg;
154
+ }
155
+
156
+ if (result != null)
157
+ {
158
+ if (Number.isNaN(result[1]) || !Number.isFinite(result[1]))
159
+ return null;
160
+
161
+ if (result[0] == null)
162
+ return { payload: result[1] };
163
+
164
+ return {
165
+ topic: result[0],
166
+ payload: result[1]
167
+ };
168
+ }
169
+
170
+ return null;
171
+ }
172
+
173
+ let setStatus = (msg) =>
174
+ {
175
+ if (msg == null)
176
+ return;
177
+
178
+ if (operation === "ABS")
179
+ node.status({ fill: "yellow", shape: "ring", text: operation + " => " + msg.payload });
180
+ else
181
+ node.status({ fill: "yellow", shape: "ring", text: operation + "(" + Object.entries(nodeSettings.values).map(v => v[1]).join(",") + ") => " + msg.payload });
182
+ }
183
+
184
+ if (config.save_state && config.resend_on_start && nodeSettings.lastMessage != null)
185
+ {
186
+ setTimeout(() =>
187
+ {
188
+ node.send(nodeSettings.lastMessage);
189
+ }, 10000);
190
+ }
191
+
192
+ setStatus(nodeSettings.lastMessage);
193
+ }
194
+
195
+ RED.nodes.registerType("smart_statistic", StatisticNode);
196
+ }