smart-nodes 0.3.36 → 0.4.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/CHANGELOG.md +62 -0
- package/README.md +35 -17
- package/central/central.html +27 -24
- package/central/central.js +84 -26
- package/central/locales/de-DE/central.html +10 -0
- package/central/locales/de-DE/central.json +14 -0
- package/central/locales/en-US/central.html +10 -0
- package/central/locales/en-US/central.json +14 -0
- package/compare/compare.html +64 -87
- package/compare/compare.js +69 -29
- package/compare/locales/de-DE/compare.html +36 -0
- package/compare/locales/de-DE/compare.json +35 -0
- package/compare/locales/en-US/compare.html +36 -0
- package/compare/locales/en-US/compare.json +35 -0
- package/counter/counter.html +70 -72
- package/counter/counter.js +43 -20
- package/counter/locales/de-DE/counter.html +48 -0
- package/counter/locales/de-DE/counter.json +21 -0
- package/counter/locales/en-US/counter.html +48 -0
- package/counter/locales/en-US/counter.json +21 -0
- package/delay/delay.html +30 -102
- package/delay/delay.js +63 -20
- package/delay/locales/de-DE/delay.html +71 -0
- package/delay/locales/de-DE/delay.json +19 -0
- package/delay/locales/en-US/delay.html +76 -0
- package/delay/locales/en-US/delay.json +19 -0
- package/forwarder/forwarder.html +11 -42
- package/forwarder/forwarder.js +59 -18
- package/forwarder/locales/de-DE/forwarder.html +32 -0
- package/forwarder/locales/de-DE/forwarder.json +15 -0
- package/forwarder/locales/en-US/forwarder.html +32 -0
- package/forwarder/locales/en-US/forwarder.json +15 -0
- package/heating-curve/heating-curve.html +10 -51
- package/heating-curve/heating-curve.js +38 -13
- package/heating-curve/locales/de-DE/heating-curve.html +38 -0
- package/heating-curve/locales/de-DE/heating-curve.json +12 -0
- package/heating-curve/locales/en-US/heating-curve.html +38 -0
- package/heating-curve/locales/en-US/heating-curve.json +12 -0
- package/hysteresis/hysteresis.html +49 -69
- package/hysteresis/hysteresis.js +94 -68
- package/hysteresis/locales/de-DE/hysteresis.html +36 -0
- package/hysteresis/locales/de-DE/hysteresis.json +27 -0
- package/hysteresis/locales/en-US/hysteresis.html +36 -0
- package/hysteresis/locales/en-US/hysteresis.json +27 -0
- package/light/light.html +250 -0
- package/{light-control/light-control.js → light/light.js} +151 -32
- package/light/locales/de-DE/light.html +149 -0
- package/light/locales/de-DE/light.json +24 -0
- package/light/locales/en-US/light.html +148 -0
- package/light/locales/en-US/light.json +24 -0
- package/logic/locales/de-DE/logic.html +12 -0
- package/logic/locales/de-DE/logic.json +26 -0
- package/logic/locales/en-US/logic.html +12 -0
- package/logic/locales/en-US/logic.json +26 -0
- package/logic/logic.html +48 -64
- package/logic/logic.js +63 -29
- package/long-press/locales/de-DE/long-press.html +5 -0
- package/long-press/locales/de-DE/long-press.json +13 -0
- package/long-press/locales/en-US/long-press.html +5 -0
- package/long-press/locales/en-US/long-press.json +13 -0
- package/{long-press-control/long-press-control.html → long-press/long-press.html} +10 -14
- package/long-press/long-press.js +163 -0
- package/mixing-valve/locales/de-DE/mixing-valve.html +65 -0
- package/mixing-valve/locales/de-DE/mixing-valve.json +19 -0
- package/mixing-valve/locales/en-US/mixing-valve.html +66 -0
- package/mixing-valve/locales/en-US/mixing-valve.json +19 -0
- package/mixing-valve/mixing-valve.html +27 -93
- package/mixing-valve/mixing-valve.js +87 -61
- package/multi-press/locales/de-DE/multi-press.html +5 -0
- package/multi-press/locales/de-DE/multi-press.json +12 -0
- package/multi-press/locales/en-US/multi-press.html +5 -0
- package/multi-press/locales/en-US/multi-press.json +12 -0
- package/{multi-press-control/multi-press-control.html → multi-press/multi-press.html} +9 -13
- package/{multi-press-control/multi-press-control.js → multi-press/multi-press.js} +53 -5
- package/package.json +7 -7
- package/persistence.js +1 -0
- package/scene/locales/de-DE/scene.html +105 -0
- package/scene/locales/de-DE/scene.json +21 -0
- package/scene/locales/en-US/scene.html +107 -0
- package/scene/locales/en-US/scene.json +20 -0
- package/{scene-control/scene-control.html → scene/scene.html} +30 -132
- package/{scene-control/scene-control.js → scene/scene.js} +76 -26
- package/scheduler/locales/de-DE/scheduler.html +30 -0
- package/scheduler/locales/de-DE/scheduler.json +21 -0
- package/scheduler/locales/en-US/scheduler.html +30 -0
- package/scheduler/locales/en-US/scheduler.json +21 -0
- package/scheduler/scheduler.html +34 -64
- package/scheduler/scheduler.js +85 -53
- package/shutter/locales/de-DE/shutter.html +127 -0
- package/shutter/locales/de-DE/shutter.json +11 -0
- package/shutter/locales/en-US/shutter.html +133 -0
- package/shutter/locales/en-US/shutter.json +11 -0
- package/{shutter-control/shutter-control.html → shutter/shutter.html} +7 -133
- package/{shutter-control/shutter-control.js → shutter/shutter.js} +116 -56
- package/shutter-complex/locales/de-DE/shutter-complex.html +120 -0
- package/shutter-complex/locales/de-DE/shutter-complex.json +20 -0
- package/shutter-complex/locales/en-US/shutter-complex.html +120 -0
- package/shutter-complex/locales/en-US/shutter-complex.json +20 -0
- package/{shutter-complex-control/shutter-complex-control.html → shutter-complex/shutter-complex.html} +36 -140
- package/shutter-complex/shutter-complex.js +578 -0
- package/smart_helper.js +52 -9
- package/statistic/locales/de-DE/statistic.html +10 -0
- package/statistic/locales/de-DE/statistic.json +29 -0
- package/statistic/locales/en-US/statistic.html +10 -0
- package/statistic/locales/en-US/statistic.json +29 -0
- package/statistic/statistic.html +38 -43
- package/statistic/statistic.js +57 -28
- package/text-exec/locales/de-DE/text-exec.html +18 -0
- package/text-exec/locales/de-DE/text-exec.json +7 -0
- package/text-exec/locales/en-US/text-exec.html +18 -0
- package/text-exec/locales/en-US/text-exec.json +7 -0
- package/text-exec/text-exec.html +9 -25
- package/text-exec/text-exec.js +43 -2
- package/light-control/light-control.html +0 -363
- package/long-press-control/long-press-control.js +0 -76
- package/shutter-complex-control/shutter-complex-control.js +0 -442
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
module.exports = function (RED)
|
|
2
|
+
{
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
function ShutterComplexControlNode(config)
|
|
6
|
+
{
|
|
7
|
+
const node = this;
|
|
8
|
+
RED.nodes.createNode(node, config);
|
|
9
|
+
|
|
10
|
+
// ###################
|
|
11
|
+
// # Class constants #
|
|
12
|
+
// ###################
|
|
13
|
+
const ACTION_UP = 0;
|
|
14
|
+
const ACTION_DOWN = 1;
|
|
15
|
+
const ACTION_STOP = 2;
|
|
16
|
+
const ACTION_POSITION = 3;
|
|
17
|
+
|
|
18
|
+
// #######################
|
|
19
|
+
// # Global help objects #
|
|
20
|
+
// #######################
|
|
21
|
+
const smart_context = require("../persistence.js")(RED);
|
|
22
|
+
const helper = require("../smart_helper.js");
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// ############################
|
|
26
|
+
// # Used from text-exec node #
|
|
27
|
+
// ############################
|
|
28
|
+
if (typeof config.exec_text_names == "string")
|
|
29
|
+
node.exec_text_names = config.exec_text_names.split(",").map(n => n.trim().toLowerCase());
|
|
30
|
+
else
|
|
31
|
+
node.exec_text_names = [];
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// #####################
|
|
35
|
+
// # persistent values #
|
|
36
|
+
// #####################
|
|
37
|
+
var node_settings = helper.cloneObject({
|
|
38
|
+
last_position: 0, // 0 = opened, 100 = closed
|
|
39
|
+
last_direction_up: true, // remember last direction for toggle action
|
|
40
|
+
last_position_before_alarm: 0, // remember position to restore on alarm off event
|
|
41
|
+
alarm_active: false, // remember if alarm is on or off
|
|
42
|
+
}, smart_context.get(node.id));
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// ##########################
|
|
46
|
+
// # Backward compatibility #
|
|
47
|
+
// ##########################
|
|
48
|
+
if (typeof config.max_time != "undefined")
|
|
49
|
+
{
|
|
50
|
+
config.max_time_up = config.max_time;
|
|
51
|
+
config.max_time_down = config.max_time;
|
|
52
|
+
delete config.max_time;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// ##################
|
|
57
|
+
// # Dynamic config #
|
|
58
|
+
// ##################
|
|
59
|
+
let max_time_up = parseInt(config.max_time_up || 60, 10);
|
|
60
|
+
let max_time_down = parseInt(config.max_time_down || 60, 10);
|
|
61
|
+
let revert_time_ms = parseInt(config.revert_time_ms || 100, 10);
|
|
62
|
+
let alarm_action = config.alarm_action || "NOTHING";
|
|
63
|
+
let alarm_off_action = config.alarm_off_action || "NOTHING";
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// ##################
|
|
67
|
+
// # Runtime values #
|
|
68
|
+
// ##################
|
|
69
|
+
|
|
70
|
+
// Here the setTimeout return value is stored to stop the shutter.
|
|
71
|
+
// That means if it is null, the shutter is stopped.
|
|
72
|
+
let timeout = null;
|
|
73
|
+
|
|
74
|
+
// The local time when the shutter starts moving.
|
|
75
|
+
// This is needed to calc the new position of the shutter.
|
|
76
|
+
let on_time = null;
|
|
77
|
+
|
|
78
|
+
// The local time when the shutter was stopped last time.
|
|
79
|
+
// This is used to measure the revert time.
|
|
80
|
+
let off_time = null;
|
|
81
|
+
|
|
82
|
+
// This is the return value of setTimeout when the shutter has to wait until the reverse time is finished.
|
|
83
|
+
let wait_timeout = null;
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// #########################
|
|
87
|
+
// # Central node handling #
|
|
88
|
+
// #########################
|
|
89
|
+
var event = "node:" + config.id;
|
|
90
|
+
var handler = function (msg)
|
|
91
|
+
{
|
|
92
|
+
node.receive(msg);
|
|
93
|
+
}
|
|
94
|
+
RED.events.on(event, handler);
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
// ###############
|
|
98
|
+
// # Node events #
|
|
99
|
+
// ###############
|
|
100
|
+
node.on("input", function (msg)
|
|
101
|
+
{
|
|
102
|
+
handleTopic(msg);
|
|
103
|
+
|
|
104
|
+
setStatus();
|
|
105
|
+
smart_context.set(node.id, node_settings);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
node.on("close", function ()
|
|
109
|
+
{
|
|
110
|
+
startAction(ACTION_STOP);
|
|
111
|
+
RED.events.off(event, handler);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// #####################
|
|
116
|
+
// # Private functions #
|
|
117
|
+
// #####################
|
|
118
|
+
|
|
119
|
+
// This is the main function which handles all topics that was received.
|
|
120
|
+
let handleTopic = msg =>
|
|
121
|
+
{
|
|
122
|
+
helper.log("handle topic:");
|
|
123
|
+
helper.log(msg);
|
|
124
|
+
|
|
125
|
+
let real_topic = helper.getRealTopic(msg.topic, "toggle", ["up", "up_stop", "down", "down_stop", "stop", "toggle", "up_down", "position", "alarm"]);
|
|
126
|
+
|
|
127
|
+
// skip if button is released
|
|
128
|
+
if (msg.payload === false && ["up", "up_stop", "down", "down_stop", "stop", "toggle"].includes(real_topic))
|
|
129
|
+
return;
|
|
130
|
+
|
|
131
|
+
// Convert up_down from HA UI to next command
|
|
132
|
+
if (real_topic == "up_down")
|
|
133
|
+
{
|
|
134
|
+
if (msg.payload)
|
|
135
|
+
real_topic = "down";
|
|
136
|
+
else
|
|
137
|
+
real_topic = "up";
|
|
138
|
+
|
|
139
|
+
delete msg.payload;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Correct next topic to avoid handling up_stop, down_stop or toggle separately.
|
|
143
|
+
if (timeout != null && ["up_stop", "down_stop", "toggle"].includes(real_topic))
|
|
144
|
+
{
|
|
145
|
+
real_topic = "stop";
|
|
146
|
+
}
|
|
147
|
+
else if (timeout == null)
|
|
148
|
+
{
|
|
149
|
+
// shutter is not running, set next command depending on topic
|
|
150
|
+
switch (real_topic)
|
|
151
|
+
{
|
|
152
|
+
case "up_stop":
|
|
153
|
+
real_topic = "up";
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "down_stop":
|
|
157
|
+
real_topic = "down";
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "toggle":
|
|
161
|
+
real_topic = node_settings.last_direction_up ? "down" : "up";
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
helper.log("handle real topic: " + real_topic);
|
|
167
|
+
switch (real_topic)
|
|
168
|
+
{
|
|
169
|
+
case "up":
|
|
170
|
+
startAction(ACTION_UP, msg.time_on ?? null, msg.exact ?? null);
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case "stop":
|
|
174
|
+
startAction(ACTION_STOP);
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case "down":
|
|
178
|
+
startAction(ACTION_DOWN, msg.time_on ?? null, msg.exact ?? null);
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case "position":
|
|
182
|
+
startAction(ACTION_POSITION, msg.payload ?? null);
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case "alarm":
|
|
186
|
+
// Make sure it is bool
|
|
187
|
+
msg.payload = !!msg.payload;
|
|
188
|
+
|
|
189
|
+
// No alarm change, do nothing
|
|
190
|
+
if (node_settings.alarm_active == msg.payload)
|
|
191
|
+
return;
|
|
192
|
+
|
|
193
|
+
node_settings.alarm_active = msg.payload;
|
|
194
|
+
|
|
195
|
+
if (node_settings.alarm_active)
|
|
196
|
+
handleAlarmOn();
|
|
197
|
+
else
|
|
198
|
+
handleAlarmOff();
|
|
199
|
+
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* This function is called when the alarm changes from off to on
|
|
206
|
+
*/
|
|
207
|
+
let handleAlarmOn = () =>
|
|
208
|
+
{
|
|
209
|
+
switch (alarm_action)
|
|
210
|
+
{
|
|
211
|
+
case "UP":
|
|
212
|
+
startAction(ACTION_UP, null, null, true);
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case "DOWN":
|
|
216
|
+
startAction(ACTION_DOWN, null, null, true);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* This function is called when the alarm changes from on to off
|
|
223
|
+
*/
|
|
224
|
+
let handleAlarmOff = () =>
|
|
225
|
+
{
|
|
226
|
+
switch (alarm_off_action)
|
|
227
|
+
{
|
|
228
|
+
case "NOTHING":
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case "UP":
|
|
232
|
+
startAction(ACTION_UP);
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case "DOWN":
|
|
236
|
+
startAction(ACTION_DOWN);
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case "LAST":
|
|
240
|
+
startAction(ACTION_POSITION, node_settings.last_position_before_alarm);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* This functions stops the shutter if needed and starts the requested action.
|
|
247
|
+
*
|
|
248
|
+
* @param {int} action One of ACTION_UP, ACTION_STOP, ACTION_DOWN or ACTION_POSITION.
|
|
249
|
+
* @param {string|float|null} data For ACTION_POSITION this is the position, for ACTION_UP and ACTION_DOWN it is the max run time.
|
|
250
|
+
* @param {boolean?} exact If true, the exact given time is used, otherwise some offset at 0% and 100% is used to ensure the shutter reach the end.
|
|
251
|
+
* @param {boolean?} ignoreAlarm If true, the action is performed even the alarm is activated. The default is false.
|
|
252
|
+
*/
|
|
253
|
+
let startAction = (action, data = null, exact = false, ignoreAlarm = false) =>
|
|
254
|
+
{
|
|
255
|
+
helper.log("startAction");
|
|
256
|
+
helper.log({ action, data, exact, ignoreAlarm });
|
|
257
|
+
|
|
258
|
+
// Nothing allowed if alarm is on
|
|
259
|
+
if (ignoreAlarm === false && node_settings.alarm_active)
|
|
260
|
+
return;
|
|
261
|
+
|
|
262
|
+
// Variable declarations
|
|
263
|
+
let run_time_ms;
|
|
264
|
+
let needStop = false;
|
|
265
|
+
let now = Date.now();
|
|
266
|
+
let currentPosition = calcNewPosition();
|
|
267
|
+
|
|
268
|
+
// handle 0% and 100% as UP and DOWN command
|
|
269
|
+
if (action == ACTION_POSITION && data === 0)
|
|
270
|
+
{
|
|
271
|
+
action = ACTION_UP;
|
|
272
|
+
data = null;
|
|
273
|
+
}
|
|
274
|
+
else if (action == ACTION_POSITION && data === 100)
|
|
275
|
+
{
|
|
276
|
+
action = ACTION_DOWN;
|
|
277
|
+
data = null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Stop waiting for revert time, it will be restarted later with the correct following action
|
|
281
|
+
if (wait_timeout != null)
|
|
282
|
+
{
|
|
283
|
+
clearTimeout(wait_timeout);
|
|
284
|
+
wait_timeout = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Shutter is running, check if it has to be stopped first
|
|
288
|
+
if (timeout != null)
|
|
289
|
+
{
|
|
290
|
+
switch (action)
|
|
291
|
+
{
|
|
292
|
+
case ACTION_UP:
|
|
293
|
+
if (node_settings.last_direction_up === false)
|
|
294
|
+
needStop = true;
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case ACTION_DOWN:
|
|
298
|
+
if (node_settings.last_direction_up === true)
|
|
299
|
+
needStop = true;
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case ACTION_STOP:
|
|
303
|
+
needStop = true;
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case ACTION_POSITION:
|
|
307
|
+
// Wrong direction, do stop
|
|
308
|
+
if (node_settings.last_direction_up === true && currentPosition < data)
|
|
309
|
+
needStop = true;
|
|
310
|
+
if (node_settings.last_direction_up === false && currentPosition > data)
|
|
311
|
+
needStop = true;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Stop shutter if needed and save new positions
|
|
317
|
+
if (needStop)
|
|
318
|
+
{
|
|
319
|
+
clearTimeout(timeout);
|
|
320
|
+
off_time = Date.now();
|
|
321
|
+
timeout = null;
|
|
322
|
+
|
|
323
|
+
if (!node_settings.alarm_active)
|
|
324
|
+
node_settings.last_position_before_alarm = currentPosition;
|
|
325
|
+
|
|
326
|
+
node_settings.last_position = currentPosition;
|
|
327
|
+
sendToOutput(false, false);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Determine needed runtime
|
|
331
|
+
switch (action)
|
|
332
|
+
{
|
|
333
|
+
case ACTION_STOP:
|
|
334
|
+
// Already stopped, nothing more to do
|
|
335
|
+
return;
|
|
336
|
+
|
|
337
|
+
case ACTION_UP:
|
|
338
|
+
// data is the run time
|
|
339
|
+
if (data == null)
|
|
340
|
+
run_time_ms = node_settings.last_position * max_time_up / 100 * 1000;
|
|
341
|
+
else
|
|
342
|
+
run_time_ms = helper.getTimeInMsFromString(data);
|
|
343
|
+
break;
|
|
344
|
+
|
|
345
|
+
case ACTION_DOWN:
|
|
346
|
+
// data is the run time
|
|
347
|
+
if (data == null)
|
|
348
|
+
run_time_ms = (100 - node_settings.last_position) * max_time_down / 100 * 1000;
|
|
349
|
+
else
|
|
350
|
+
run_time_ms = helper.getTimeInMsFromString(data);
|
|
351
|
+
break;
|
|
352
|
+
|
|
353
|
+
case ACTION_POSITION:
|
|
354
|
+
// data is the position in percent
|
|
355
|
+
if (data == null)
|
|
356
|
+
{
|
|
357
|
+
console.warn("WARN: Try to set position without giving a new position");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Make sure it is in range 0-100
|
|
362
|
+
data = Math.min(100, Math.max(0, data));
|
|
363
|
+
|
|
364
|
+
// Convert to UP/DOWN with a specific time
|
|
365
|
+
if (data < currentPosition)
|
|
366
|
+
{
|
|
367
|
+
run_time_ms = (currentPosition - data) / 100 * max_time_up * 1000;
|
|
368
|
+
action = ACTION_UP;
|
|
369
|
+
}
|
|
370
|
+
else // if (data > currentPosition)
|
|
371
|
+
{
|
|
372
|
+
action = ACTION_DOWN;
|
|
373
|
+
run_time_ms = (data - currentPosition) / 100 * max_time_down * 1000;
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (exact !== true && data == null)
|
|
379
|
+
{
|
|
380
|
+
// Run at least for 5 seconds
|
|
381
|
+
if (run_time_ms < 5000)
|
|
382
|
+
run_time_ms = 5000;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let dirChange = ((action == ACTION_UP && !node_settings.last_direction_up) || (action == ACTION_DOWN && node_settings.last_direction_up));
|
|
386
|
+
|
|
387
|
+
// Just to make sure there is no mistake
|
|
388
|
+
if (run_time_ms < 0)
|
|
389
|
+
run_time_ms = 0;
|
|
390
|
+
|
|
391
|
+
if (timeout != null)
|
|
392
|
+
{
|
|
393
|
+
// This happens if the time needs to be changed, but the direction is the same
|
|
394
|
+
clearTimeout(timeout);
|
|
395
|
+
helper.log("stop after " + run_time_ms + "ms");
|
|
396
|
+
|
|
397
|
+
off_time = Date.now();
|
|
398
|
+
on_time = off_time;
|
|
399
|
+
node_settings.last_position = currentPosition;
|
|
400
|
+
|
|
401
|
+
timeout = setTimeout(() =>
|
|
402
|
+
{
|
|
403
|
+
// Just stop after the new time
|
|
404
|
+
startAction(ACTION_STOP, null, null, ignoreAlarm);
|
|
405
|
+
timeout = null;
|
|
406
|
+
|
|
407
|
+
smart_context.set(node.id, node_settings);
|
|
408
|
+
|
|
409
|
+
setStatus();
|
|
410
|
+
}, run_time_ms);
|
|
411
|
+
}
|
|
412
|
+
else if (off_time + revert_time_ms - now > 0 && dirChange)
|
|
413
|
+
{
|
|
414
|
+
// revert time is not fully passed
|
|
415
|
+
helper.log("revert time is not fully passed, wait for " + (off_time + revert_time_ms - now) + "ms");
|
|
416
|
+
wait_timeout = setTimeout(() =>
|
|
417
|
+
{
|
|
418
|
+
wait_timeout = null;
|
|
419
|
+
startAction(action, data, exact, ignoreAlarm);
|
|
420
|
+
|
|
421
|
+
smart_context.set(node.id, node_settings);
|
|
422
|
+
|
|
423
|
+
setStatus();
|
|
424
|
+
}, off_time + revert_time_ms - now);
|
|
425
|
+
}
|
|
426
|
+
else if (run_time_ms == 0)
|
|
427
|
+
{
|
|
428
|
+
// Do nothing.
|
|
429
|
+
// This can happen if exact = true but the shutter is already at the target position
|
|
430
|
+
}
|
|
431
|
+
else
|
|
432
|
+
{
|
|
433
|
+
// The shutter can finally be started.
|
|
434
|
+
switch (action)
|
|
435
|
+
{
|
|
436
|
+
case ACTION_UP:
|
|
437
|
+
helper.log("start ACTION_UP");
|
|
438
|
+
sendToOutput(true, false);
|
|
439
|
+
break;
|
|
440
|
+
|
|
441
|
+
case ACTION_DOWN:
|
|
442
|
+
helper.log("start ACTION_DOWN");
|
|
443
|
+
sendToOutput(false, true);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!node_settings.alarm_active)
|
|
448
|
+
node_settings.last_position_before_alarm = currentPosition;
|
|
449
|
+
|
|
450
|
+
on_time = Date.now();
|
|
451
|
+
|
|
452
|
+
helper.log("stop after " + run_time_ms + "ms");
|
|
453
|
+
timeout = setTimeout(() =>
|
|
454
|
+
{
|
|
455
|
+
startAction(ACTION_STOP, null, null, ignoreAlarm);
|
|
456
|
+
timeout = null;
|
|
457
|
+
|
|
458
|
+
smart_context.set(node.id, node_settings);
|
|
459
|
+
|
|
460
|
+
setStatus();
|
|
461
|
+
}, run_time_ms);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
*
|
|
467
|
+
* @param {*} up
|
|
468
|
+
* @param {*} down
|
|
469
|
+
* @returns
|
|
470
|
+
*/
|
|
471
|
+
let sendToOutput = (up, down) =>
|
|
472
|
+
{
|
|
473
|
+
helper.log("sendToOutput");
|
|
474
|
+
helper.log({ up, down });
|
|
475
|
+
|
|
476
|
+
if (up && down)
|
|
477
|
+
{
|
|
478
|
+
console.error("Fatal exception, Cannot send up and down at the same time.");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (up)
|
|
483
|
+
node_settings.last_direction_up = true;
|
|
484
|
+
else if (down)
|
|
485
|
+
node_settings.last_direction_up = false;
|
|
486
|
+
|
|
487
|
+
node.send([{ payload: up }, { payload: down }, { payload: node_settings.last_position }]);
|
|
488
|
+
|
|
489
|
+
// Inform central nodes that shutter is running/stopped
|
|
490
|
+
notifyCentral(up || down);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
*
|
|
495
|
+
* @returns
|
|
496
|
+
*/
|
|
497
|
+
let calcNewPosition = () =>
|
|
498
|
+
{
|
|
499
|
+
let now = Date.now();
|
|
500
|
+
|
|
501
|
+
if (timeout == null)
|
|
502
|
+
return node_settings.last_position;
|
|
503
|
+
|
|
504
|
+
// Change position while running,
|
|
505
|
+
// Calculate current position first
|
|
506
|
+
if (node_settings.last_direction_up)
|
|
507
|
+
{
|
|
508
|
+
let change_percentage = (now - on_time) / 1000 / max_time_up * 100;
|
|
509
|
+
return Math.max(0, node_settings.last_position - change_percentage);
|
|
510
|
+
}
|
|
511
|
+
else
|
|
512
|
+
{
|
|
513
|
+
let change_percentage = (now - on_time) / 1000 / max_time_down * 100;
|
|
514
|
+
return Math.min(100, node_settings.last_position + change_percentage);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Set the current node status
|
|
520
|
+
*/
|
|
521
|
+
let setStatus = () =>
|
|
522
|
+
{
|
|
523
|
+
let fill = node_settings.alarm_active ? "red" : "green";
|
|
524
|
+
let shape = timeout != null ? "ring" : "dot";
|
|
525
|
+
|
|
526
|
+
// collect all texts and join later with a comma
|
|
527
|
+
let texts = [];
|
|
528
|
+
|
|
529
|
+
if (node_settings.alarm_active)
|
|
530
|
+
texts.push("ALARM");
|
|
531
|
+
|
|
532
|
+
if (timeout == null)
|
|
533
|
+
texts.push("Stopped");
|
|
534
|
+
else if (node_settings.last_direction_up)
|
|
535
|
+
texts.push("Up");
|
|
536
|
+
else
|
|
537
|
+
texts.push("Down");
|
|
538
|
+
|
|
539
|
+
texts.push("Position: " + node_settings.last_position?.toFixed(0) + "%");
|
|
540
|
+
|
|
541
|
+
node.status({ fill, shape, text: helper.getCurrentTimeForStatus() + ": " + texts.join(", ") });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Notify all connected central nodes
|
|
546
|
+
* @param {boolean} state The state if the shutter is running
|
|
547
|
+
* @returns
|
|
548
|
+
*/
|
|
549
|
+
let notifyCentral = state =>
|
|
550
|
+
{
|
|
551
|
+
if (!config.links)
|
|
552
|
+
return;
|
|
553
|
+
|
|
554
|
+
config.links.forEach(link =>
|
|
555
|
+
{
|
|
556
|
+
helper.log(node.id + " -> " + link);
|
|
557
|
+
helper.log({ source: node.id, state: state });
|
|
558
|
+
RED.events.emit("node:" + link, { source: node.id, state: state });
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// For security reason, stop shutter at node start
|
|
563
|
+
wait_timeout = setTimeout(() =>
|
|
564
|
+
{
|
|
565
|
+
wait_timeout = null;
|
|
566
|
+
|
|
567
|
+
sendToOutput(false, false);
|
|
568
|
+
notifyCentral(false);
|
|
569
|
+
|
|
570
|
+
if (node_settings.alarm_active)
|
|
571
|
+
handleAlarmOn();
|
|
572
|
+
|
|
573
|
+
setStatus();
|
|
574
|
+
}, 10000);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
RED.nodes.registerType("smart_shutter-complex-control", ShutterComplexControlNode);
|
|
578
|
+
};
|
package/smart_helper.js
CHANGED
|
@@ -5,16 +5,16 @@ module.exports = {
|
|
|
5
5
|
* If a type is unknown the NodeRed function is used for conversation.
|
|
6
6
|
*
|
|
7
7
|
* Known types are:
|
|
8
|
-
* - "
|
|
8
|
+
* - "NOTHING", which will return always null
|
|
9
9
|
*/
|
|
10
10
|
evaluateNodeProperty(RED, value, type)
|
|
11
11
|
{
|
|
12
12
|
try
|
|
13
13
|
{
|
|
14
|
-
switch (type)
|
|
14
|
+
switch (type?.toUpperCase())
|
|
15
15
|
{
|
|
16
16
|
case null:
|
|
17
|
-
case "
|
|
17
|
+
case "NOTHING":
|
|
18
18
|
return null;
|
|
19
19
|
|
|
20
20
|
default:
|
|
@@ -23,10 +23,7 @@ module.exports = {
|
|
|
23
23
|
}
|
|
24
24
|
catch (error)
|
|
25
25
|
{
|
|
26
|
-
console.error(value);
|
|
27
|
-
console.error(type);
|
|
28
|
-
console.error(error);
|
|
29
|
-
console.error(msg);
|
|
26
|
+
console.error({ value, type, error });
|
|
30
27
|
return null;
|
|
31
28
|
}
|
|
32
29
|
},
|
|
@@ -72,6 +69,23 @@ module.exports = {
|
|
|
72
69
|
return result;
|
|
73
70
|
},
|
|
74
71
|
|
|
72
|
+
/**
|
|
73
|
+
* This functions extracts the topic name out of the given topic and checks if it is a valid topic.
|
|
74
|
+
* If not, the default topic is returned.
|
|
75
|
+
* @param {string} topic
|
|
76
|
+
* @param {string} defaultTopic
|
|
77
|
+
* @param {string[]} possibleTopics
|
|
78
|
+
* @returns The real topic that needs to be processes
|
|
79
|
+
*/
|
|
80
|
+
getRealTopic(topic, defaultTopic, possibleTopics)
|
|
81
|
+
{
|
|
82
|
+
let realTopic = this.getTopicName(topic);
|
|
83
|
+
if (possibleTopics.includes(realTopic))
|
|
84
|
+
return realTopic;
|
|
85
|
+
|
|
86
|
+
return defaultTopic;
|
|
87
|
+
},
|
|
88
|
+
|
|
75
89
|
/**
|
|
76
90
|
* This function converts the given value with the given unit into milli seconds.
|
|
77
91
|
*
|
|
@@ -166,6 +180,26 @@ module.exports = {
|
|
|
166
180
|
return this.getTimeInMs(values[1].replace(",", "."), values[2] || "ms");
|
|
167
181
|
},
|
|
168
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Formats a string by replacing {n} with the n-th argument.
|
|
185
|
+
* Example: format("Hello {0}. {1} {0}.", "world", "Bye") => "Hello world. Bye world."
|
|
186
|
+
* @param {string} str The string to format
|
|
187
|
+
* @param {...string} args The arguments used for the replacement
|
|
188
|
+
* @returns The formatted string
|
|
189
|
+
*/
|
|
190
|
+
format(str, ...args)
|
|
191
|
+
{
|
|
192
|
+
if (!args.length)
|
|
193
|
+
return str;
|
|
194
|
+
|
|
195
|
+
for (var a in args)
|
|
196
|
+
{
|
|
197
|
+
str = str.replace(new RegExp("\\{" + a + "\\}", "gi"), args[a]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return str
|
|
201
|
+
},
|
|
202
|
+
|
|
169
203
|
/**
|
|
170
204
|
* This function returns a string that represents the remaining time given in time_ms.
|
|
171
205
|
* If a timeConcatWord it set, it returns also this word including the target time.
|
|
@@ -302,8 +336,17 @@ module.exports = {
|
|
|
302
336
|
* @param {*} obj The Object to copy
|
|
303
337
|
* @returns A new Object
|
|
304
338
|
*/
|
|
305
|
-
cloneObject(obj)
|
|
339
|
+
cloneObject(...obj)
|
|
340
|
+
{
|
|
341
|
+
return Object.assign({}, ...obj);
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Forward all arguments to the console if it is enabled
|
|
346
|
+
*/
|
|
347
|
+
log()
|
|
306
348
|
{
|
|
307
|
-
|
|
349
|
+
// uncomment to see all log values
|
|
350
|
+
// console.log(...arguments);
|
|
308
351
|
}
|
|
309
352
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="smart_statistic">
|
|
2
|
+
<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>
|
|
3
|
+
<p>Jeder Wert muss mit einem eigenen Topic gesendet werden. Kommt ein zweiter Wert mit dem gleichen Topic, wird der entsprechende Wert überschrieben.</p>
|
|
4
|
+
<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>
|
|
5
|
+
<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>
|
|
6
|
+
<p>
|
|
7
|
+
<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/>
|
|
8
|
+
Diese Node verwendet nur den Teil <code>nummer</code>. <code>name</code> und <code>#</code> sind dabei optional.
|
|
9
|
+
</p>
|
|
10
|
+
</script>
|