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.
- package/LICENSE +21 -0
- package/LICENSE.md +21 -0
- package/README.md +127 -0
- package/central/central.html +328 -0
- package/central/central.js +95 -0
- package/compare/compare.html +137 -0
- package/compare/compare.js +151 -0
- package/delay/delay.html +192 -0
- package/delay/delay.js +175 -0
- package/examples/central.json +804 -0
- package/examples/central.png +0 -0
- package/examples/compare.json +916 -0
- package/examples/compare.png +0 -0
- package/examples/delay.json +198 -0
- package/examples/delay.png +0 -0
- package/examples/forwarder.json +152 -0
- package/examples/forwarder.png +0 -0
- package/examples/hysteresis.json +358 -0
- package/examples/hysteresis.png +0 -0
- package/examples/light-control.json +499 -0
- package/examples/light-control.png +0 -0
- package/examples/logic.json +562 -0
- package/examples/logic.png +0 -0
- package/examples/long-press-control.json +113 -0
- package/examples/long-press-control.png +0 -0
- package/examples/multi-press-control.json +136 -0
- package/examples/multi-press-control.png +0 -0
- package/examples/scene-control.json +535 -0
- package/examples/scene-control.png +0 -0
- package/examples/scheduler.json +164 -0
- package/examples/scheduler.png +0 -0
- package/examples/shutter-complex-control.json +489 -0
- package/examples/shutter-complex-control.png +0 -0
- package/examples/shutter-control.json +457 -0
- package/examples/shutter-control.png +0 -0
- package/examples/statistic.json +1112 -0
- package/examples/statistic.png +0 -0
- package/forwarder/forwarder.html +100 -0
- package/forwarder/forwarder.js +95 -0
- package/hysteresis/hysteresis.html +152 -0
- package/hysteresis/hysteresis.js +146 -0
- package/light-control/light-control.html +358 -0
- package/light-control/light-control.js +231 -0
- package/logic/logic.html +168 -0
- package/logic/logic.js +171 -0
- package/long-press-control/long-press-control.html +74 -0
- package/long-press-control/long-press-control.js +75 -0
- package/multi-press-control/multi-press-control.html +135 -0
- package/multi-press-control/multi-press-control.js +68 -0
- package/package.json +59 -0
- package/persistence.js +74 -0
- package/scene-control/scene-control.html +575 -0
- package/scene-control/scene-control.js +265 -0
- package/scheduler/scheduler.html +338 -0
- package/scheduler/scheduler.js +209 -0
- package/shutter-complex-control/shutter-complex-control.html +330 -0
- package/shutter-complex-control/shutter-complex-control.js +399 -0
- package/shutter-control/shutter-control.html +283 -0
- package/shutter-control/shutter-control.js +208 -0
- package/smart_helper.js +156 -0
- package/statistic/statistic.html +107 -0
- 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
|
+
};
|
package/smart_helper.js
ADDED
|
@@ -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
|
+
}
|