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,399 @@
|
|
|
1
|
+
module.exports = function (RED)
|
|
2
|
+
{
|
|
3
|
+
function ShutterComplexControlNode(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
|
+
let max_time = parseInt(config.max_time || 60);
|
|
19
|
+
let revert_time_ms = parseInt(config.revert_time_ms || 100);
|
|
20
|
+
let alarm_action = config.alarm_action || "NOTHING";
|
|
21
|
+
|
|
22
|
+
// runtime values
|
|
23
|
+
let max_time_timeout = null;
|
|
24
|
+
let on_time = null;
|
|
25
|
+
let off_time = null;
|
|
26
|
+
let wait_for_up = false;
|
|
27
|
+
let wait_for_down = false;
|
|
28
|
+
let wait_timeout = null;
|
|
29
|
+
let alarm_active = false;
|
|
30
|
+
|
|
31
|
+
// central handling
|
|
32
|
+
var event = "node:" + config.id;
|
|
33
|
+
var handler = function (msg)
|
|
34
|
+
{
|
|
35
|
+
node.receive(msg);
|
|
36
|
+
}
|
|
37
|
+
RED.events.on(event, handler);
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
node.on("input", function (msg)
|
|
41
|
+
{
|
|
42
|
+
handleTopic(msg);
|
|
43
|
+
|
|
44
|
+
smartContext.set(node.id, nodeSettings);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
node.on("close", function ()
|
|
48
|
+
{
|
|
49
|
+
stopAutoOff();
|
|
50
|
+
RED.events.off(event, handler);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let handleTopic = msg =>
|
|
54
|
+
{
|
|
55
|
+
var resultUp = false;
|
|
56
|
+
var resultDown = false;
|
|
57
|
+
var resultStop = false;
|
|
58
|
+
|
|
59
|
+
let realTopic = helper.getTopicName(msg.topic);
|
|
60
|
+
|
|
61
|
+
// set default topic if invalid value is send
|
|
62
|
+
if (!["up", "up_stop", "down", "down_stop", "stop", "toggle", "up_down", "position", "alarm"].includes(realTopic))
|
|
63
|
+
realTopic = "toggle";
|
|
64
|
+
|
|
65
|
+
// skip if button is released
|
|
66
|
+
if (msg.payload === false && ["up", "up_stop", "down", "down_stop", "stop", "toggle"].includes(realTopic))
|
|
67
|
+
return;
|
|
68
|
+
|
|
69
|
+
// Convert up_down from HA UI to next command
|
|
70
|
+
if (realTopic == "up_down")
|
|
71
|
+
{
|
|
72
|
+
if (msg.payload)
|
|
73
|
+
realTopic = "down";
|
|
74
|
+
else
|
|
75
|
+
realTopic = "up";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Correct next topic to avoid handling up_stop, down_stop or toggle separately.
|
|
79
|
+
if (max_time_timeout != null && (realTopic == "up_stop" || realTopic == "down_stop" || realTopic == "toggle"))
|
|
80
|
+
{
|
|
81
|
+
realTopic = "stop";
|
|
82
|
+
}
|
|
83
|
+
else if (max_time_timeout == null)
|
|
84
|
+
{
|
|
85
|
+
// shutter is not running, set next command depending on topic
|
|
86
|
+
if (realTopic == "up_stop")
|
|
87
|
+
realTopic = "up";
|
|
88
|
+
else if (realTopic == "down_stop")
|
|
89
|
+
realTopic = "down";
|
|
90
|
+
else if (nodeSettings.last_direction_up && realTopic == "toggle")
|
|
91
|
+
realTopic = "down";
|
|
92
|
+
else if (!nodeSettings.last_direction_up && realTopic == "toggle")
|
|
93
|
+
realTopic = "up";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// console.log("Convert topic to: " + realTopic);
|
|
97
|
+
|
|
98
|
+
switch (realTopic)
|
|
99
|
+
{
|
|
100
|
+
case "up":
|
|
101
|
+
if (max_time_timeout != null)
|
|
102
|
+
{
|
|
103
|
+
// Nothing todo
|
|
104
|
+
if (nodeSettings.last_direction_up)
|
|
105
|
+
return;
|
|
106
|
+
|
|
107
|
+
// Runs down at the moment, so stop first
|
|
108
|
+
if (nodeSettings.last_direction_up == false)
|
|
109
|
+
{
|
|
110
|
+
// console.log("--- Sub topic ---");
|
|
111
|
+
handleTopic({ topic: "stop" });
|
|
112
|
+
// console.log("-----------------");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
on_time = Date.now();
|
|
117
|
+
resultUp = true;
|
|
118
|
+
startAutoOff(false, helper.getTimeInMsFromString(msg.time_on) || null);
|
|
119
|
+
if (!alarm_active)
|
|
120
|
+
node.status({ fill: "green", shape: "dot", text: "Up" });
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case "stop":
|
|
124
|
+
if (alarm_active)
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
resultStop = true;
|
|
128
|
+
if (max_time_timeout != null)
|
|
129
|
+
{
|
|
130
|
+
const change_percentage = (Date.now() - on_time) / 1000 / max_time * 100;
|
|
131
|
+
if (nodeSettings.last_direction_up)
|
|
132
|
+
nodeSettings.last_position = Math.max(0, nodeSettings.last_position - change_percentage);
|
|
133
|
+
else
|
|
134
|
+
nodeSettings.last_position = Math.min(100, nodeSettings.last_position + change_percentage);
|
|
135
|
+
}
|
|
136
|
+
off_time = Date.now();
|
|
137
|
+
stopAutoOff();
|
|
138
|
+
node.status({ fill: "red", shape: "dot", text: "Stopped at " + Math.round(nodeSettings.last_position) + "%" });
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case "down":
|
|
142
|
+
if (max_time_timeout != null)
|
|
143
|
+
{
|
|
144
|
+
// Nothing todo
|
|
145
|
+
if (nodeSettings.last_direction_up == false)
|
|
146
|
+
return;
|
|
147
|
+
|
|
148
|
+
// Runs up at the moment, so stop first
|
|
149
|
+
if (nodeSettings.last_direction_up)
|
|
150
|
+
{
|
|
151
|
+
// console.log("--- Sub topic ---");
|
|
152
|
+
handleTopic({ topic: "stop" });
|
|
153
|
+
// console.log("-----------------");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
on_time = Date.now();
|
|
158
|
+
resultDown = true;
|
|
159
|
+
startAutoOff(true, helper.getTimeInMsFromString(msg.time_on) || null);
|
|
160
|
+
if (!alarm_active)
|
|
161
|
+
node.status({ fill: "green", shape: "dot", text: "Down" });
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case "position":
|
|
165
|
+
if (alarm_active)
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
let value = parseFloat(msg.payload);
|
|
169
|
+
|
|
170
|
+
if (value < 0) value = 0;
|
|
171
|
+
if (value > 100) value = 100;
|
|
172
|
+
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
|
|
175
|
+
if (max_time_timeout != null)
|
|
176
|
+
{
|
|
177
|
+
// Change position while running,
|
|
178
|
+
// Calculate current position first
|
|
179
|
+
const change_percentage = (now - on_time) / 1000 / max_time * 100;
|
|
180
|
+
if (nodeSettings.last_direction_up)
|
|
181
|
+
nodeSettings.last_position = Math.max(0, nodeSettings.last_position - change_percentage);
|
|
182
|
+
else
|
|
183
|
+
nodeSettings.last_position = Math.min(100, nodeSettings.last_position + change_percentage);
|
|
184
|
+
|
|
185
|
+
node.status({ fill: "gray", shape: "dot", text: "Update current position to " + nodeSettings.last_position + "%" });
|
|
186
|
+
|
|
187
|
+
// Runs in the wrong direction at the moment, so stop first
|
|
188
|
+
if (nodeSettings.last_direction_up && value > nodeSettings.last_position)
|
|
189
|
+
{
|
|
190
|
+
// console.log("--- Sub topic ---");
|
|
191
|
+
handleTopic({ topic: "stop" });
|
|
192
|
+
// console.log("-----------------");
|
|
193
|
+
}
|
|
194
|
+
else if (nodeSettings.last_direction_up == false && value < nodeSettings.last_position)
|
|
195
|
+
{
|
|
196
|
+
// console.log("--- Sub topic ---");
|
|
197
|
+
handleTopic({ topic: "stop" });
|
|
198
|
+
// console.log("-----------------");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
on_time = now;
|
|
203
|
+
|
|
204
|
+
if (value < nodeSettings.last_position)
|
|
205
|
+
{
|
|
206
|
+
// Go up
|
|
207
|
+
resultUp = true;
|
|
208
|
+
startAutoOff(false, (nodeSettings.last_position - value) / 100 * max_time * 1000, value);
|
|
209
|
+
}
|
|
210
|
+
else if (value > nodeSettings.last_position)
|
|
211
|
+
{
|
|
212
|
+
// Go down
|
|
213
|
+
resultDown = true;
|
|
214
|
+
startAutoOff(true, (value - nodeSettings.last_position) / 100 * max_time * 1000, value);
|
|
215
|
+
}
|
|
216
|
+
else
|
|
217
|
+
{
|
|
218
|
+
// Really an exact match
|
|
219
|
+
handleTopic({ topic: "stop" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
node.status({ fill: "green", shape: "dot", text: "Set position from " + nodeSettings.last_position + "% to " + value + "%" });
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case "alarm":
|
|
227
|
+
alarm_active = msg.payload;
|
|
228
|
+
|
|
229
|
+
if (alarm_active)
|
|
230
|
+
{
|
|
231
|
+
node.status({ fill: "red", shape: "dot", text: "ALARM is active" });
|
|
232
|
+
|
|
233
|
+
switch (alarm_action)
|
|
234
|
+
{
|
|
235
|
+
case "UP":
|
|
236
|
+
handleTopic({ topic: "up" });
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case "DOWN":
|
|
240
|
+
handleTopic({ topic: "down" });
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
sendResult(resultUp, resultDown, resultStop);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let startAutoOff = (down, wait_time_ms = null, new_position = null) =>
|
|
251
|
+
{
|
|
252
|
+
// console.log("startAutoOff (" + down + ", " + wait_time_ms + ", " + new_position + ");");
|
|
253
|
+
|
|
254
|
+
// Stop if any timeout is set
|
|
255
|
+
stopAutoOff();
|
|
256
|
+
|
|
257
|
+
if (new_position != null)
|
|
258
|
+
{
|
|
259
|
+
// use normal times for top or bottom position
|
|
260
|
+
if (new_position == 0)
|
|
261
|
+
{
|
|
262
|
+
new_position = null;
|
|
263
|
+
down = false;
|
|
264
|
+
}
|
|
265
|
+
else if (new_position == 100)
|
|
266
|
+
{
|
|
267
|
+
new_position = null;
|
|
268
|
+
down = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (wait_time_ms == null)
|
|
272
|
+
{
|
|
273
|
+
if (down)
|
|
274
|
+
wait_time_ms = (100 - nodeSettings.last_position) * max_time / 100 * 1000;
|
|
275
|
+
else
|
|
276
|
+
wait_time_ms = nodeSettings.last_position * max_time / 100 * 1000;
|
|
277
|
+
|
|
278
|
+
// Run at least for 5 seconds
|
|
279
|
+
if (wait_time_ms < 5000)
|
|
280
|
+
wait_time_ms = 5000;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
max_time_on = parseInt(wait_time_ms / 1000);
|
|
284
|
+
|
|
285
|
+
if (wait_time_ms < 0)
|
|
286
|
+
{
|
|
287
|
+
node.status({ fill: "red", shape: "dot", text: "time_on value has to be greater than 0" });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!alarm_active)
|
|
292
|
+
node.status({ fill: "yellow", shape: "ring", text: (down ? "Down" : "Up") + ", wait " + helper.formatMsToStatus(max_time_on, "until") + " for auto off" });
|
|
293
|
+
|
|
294
|
+
max_time_timeout = setTimeout(() =>
|
|
295
|
+
{
|
|
296
|
+
handleTopic({ topic: "stop" });
|
|
297
|
+
if (new_position != null)
|
|
298
|
+
nodeSettings.last_position = new_position;
|
|
299
|
+
smartContext.set(node.id, nodeSettings);
|
|
300
|
+
}, wait_time_ms);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
let stopAutoOff = () =>
|
|
304
|
+
{
|
|
305
|
+
if (max_time_timeout != null)
|
|
306
|
+
{
|
|
307
|
+
// console.log("stopAutoOff");
|
|
308
|
+
if (!alarm_active)
|
|
309
|
+
node.status({ fill: "green", shape: "dot", text: "Stopped at " + Math.round(nodeSettings.last_position) + "%" });
|
|
310
|
+
clearTimeout(max_time_timeout);
|
|
311
|
+
off_time = Date.now();
|
|
312
|
+
max_time_timeout = null;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
let sendResult = (up, down, stop) =>
|
|
317
|
+
{
|
|
318
|
+
// console.log("sendResult(" + up + ", " + down + ", " + stop + ");");
|
|
319
|
+
if ((up && wait_for_down) || (down && wait_for_up))
|
|
320
|
+
{
|
|
321
|
+
// console.log("Stop wait timeout");
|
|
322
|
+
clearTimeout(wait_timeout);
|
|
323
|
+
wait_timeout = null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let now = Date.now();
|
|
327
|
+
if (off_time + revert_time_ms > now)
|
|
328
|
+
{
|
|
329
|
+
// Output has to possibly wait until the revert time has passed
|
|
330
|
+
if ((nodeSettings.last_direction_up && down) || (nodeSettings.last_direction_up == false && up))
|
|
331
|
+
{
|
|
332
|
+
// console.log("Start wait timeout for " + (off_time + revert_time_ms - now) + "ms");
|
|
333
|
+
|
|
334
|
+
if (off_time + revert_time_ms - now > revert_time_ms)
|
|
335
|
+
return;
|
|
336
|
+
|
|
337
|
+
wait_timeout = setTimeout(() =>
|
|
338
|
+
{
|
|
339
|
+
wait_timeout = null;
|
|
340
|
+
sendResult(up, down, stop);
|
|
341
|
+
}, off_time + revert_time_ms - now);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (stop && wait_timeout != null)
|
|
347
|
+
{
|
|
348
|
+
clearTimeout(wait_timeout);
|
|
349
|
+
wait_timeout = null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// console.log("really sendResult(" + up + ", " + down + ", " + stop + ");");
|
|
353
|
+
if (up)
|
|
354
|
+
{
|
|
355
|
+
if (!alarm_active || alarm_action === "UP")
|
|
356
|
+
{
|
|
357
|
+
nodeSettings.last_direction_up = true;
|
|
358
|
+
node.send([{ payload: true }, { payload: false }, { payload: nodeSettings.last_position }]);
|
|
359
|
+
notifyCentral(true);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else if (down)
|
|
363
|
+
{
|
|
364
|
+
if (!alarm_active || alarm_action === "DOWN")
|
|
365
|
+
{
|
|
366
|
+
nodeSettings.last_direction_up = false;
|
|
367
|
+
node.send([{ payload: false }, { payload: true }, { payload: nodeSettings.last_position }]);
|
|
368
|
+
notifyCentral(true);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else if (stop && !alarm_active)
|
|
372
|
+
{
|
|
373
|
+
node.send([{ payload: false }, { payload: false }, { payload: nodeSettings.last_position }]);
|
|
374
|
+
notifyCentral(false);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
let notifyCentral = state =>
|
|
379
|
+
{
|
|
380
|
+
if (!config.links)
|
|
381
|
+
return;
|
|
382
|
+
|
|
383
|
+
config.links.forEach(link =>
|
|
384
|
+
{
|
|
385
|
+
RED.events.emit("node:" + link, { source: node.id, state: state });
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// For security reason, stop shutter at node start
|
|
390
|
+
setTimeout(() =>
|
|
391
|
+
{
|
|
392
|
+
node.send([{ payload: false }, { payload: false }, { payload: nodeSettings.last_position }]);
|
|
393
|
+
node.status({ fill: "red", shape: "dot", text: "Stopped at " + Math.round(nodeSettings.last_position) + "%" });
|
|
394
|
+
notifyCentral(false);
|
|
395
|
+
}, 10000);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
RED.nodes.registerType("smart_shutter-complex-control", ShutterComplexControlNode);
|
|
399
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function ()
|
|
3
|
+
{
|
|
4
|
+
let treeList;
|
|
5
|
+
let candidateNodesCount = 0;
|
|
6
|
+
let flows = [];
|
|
7
|
+
let flowMap = {};
|
|
8
|
+
|
|
9
|
+
function onEditPrepare(node, targetTypes)
|
|
10
|
+
{
|
|
11
|
+
if (!node.links)
|
|
12
|
+
node.links = [];
|
|
13
|
+
|
|
14
|
+
const activeSubflow = RED.nodes.subflow(node.z);
|
|
15
|
+
|
|
16
|
+
treeList = $("<div>")
|
|
17
|
+
.css({ width: "100%", height: "100%" })
|
|
18
|
+
.appendTo(".node-input-link-row")
|
|
19
|
+
.treeList({ autoSelect: false })
|
|
20
|
+
.on("treelistitemmouseover", function (e, item)
|
|
21
|
+
{
|
|
22
|
+
if (item.node)
|
|
23
|
+
{
|
|
24
|
+
item.node.highlighted = true;
|
|
25
|
+
item.node.dirty = true;
|
|
26
|
+
RED.view.redraw();
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.on("treelistitemmouseout", function (e, item)
|
|
30
|
+
{
|
|
31
|
+
if (item.node)
|
|
32
|
+
{
|
|
33
|
+
item.node.highlighted = false;
|
|
34
|
+
item.node.dirty = true;
|
|
35
|
+
RED.view.redraw();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
flows = [];
|
|
40
|
+
flowMap = {};
|
|
41
|
+
|
|
42
|
+
if (activeSubflow)
|
|
43
|
+
{
|
|
44
|
+
flowMap[activeSubflow.id] = {
|
|
45
|
+
id: activeSubflow.id,
|
|
46
|
+
class: "red-ui-palette-header",
|
|
47
|
+
label: "Subflow : " + (activeSubflow.name || activeSubflow.id),
|
|
48
|
+
expanded: true,
|
|
49
|
+
children: []
|
|
50
|
+
};
|
|
51
|
+
flows.push(flowMap[activeSubflow.id]);
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
{
|
|
55
|
+
RED.nodes.eachWorkspace(function (ws)
|
|
56
|
+
{
|
|
57
|
+
if (!ws.disabled)
|
|
58
|
+
{
|
|
59
|
+
flowMap[ws.id] = {
|
|
60
|
+
id: ws.id,
|
|
61
|
+
class: "red-ui-palette-header",
|
|
62
|
+
label: (ws.label || ws.id) + (node.z === ws.id ? " *" : ""),
|
|
63
|
+
expanded: true,
|
|
64
|
+
children: []
|
|
65
|
+
};
|
|
66
|
+
flows.push(flowMap[ws.id]);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setTimeout(function ()
|
|
72
|
+
{
|
|
73
|
+
treeList.treeList("show", node.z);
|
|
74
|
+
}, 100);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function initTreeList(node, targetTypes)
|
|
78
|
+
{
|
|
79
|
+
candidateNodesCount = 0;
|
|
80
|
+
for (const key in flowMap)
|
|
81
|
+
{
|
|
82
|
+
flowMap[key].children = [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let candidateNodes = [];
|
|
86
|
+
|
|
87
|
+
targetTypes.forEach(function (targetType)
|
|
88
|
+
{
|
|
89
|
+
candidateNodes = candidateNodes.concat(RED.nodes.filterNodes({ type: targetType }));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
candidateNodes.forEach(function (n)
|
|
93
|
+
{
|
|
94
|
+
if (flowMap[n.z])
|
|
95
|
+
{
|
|
96
|
+
const isChecked = (node.links.indexOf(n.id) !== -1) || (n.links || []).indexOf(node.id) !== -1;
|
|
97
|
+
if (isChecked)
|
|
98
|
+
{
|
|
99
|
+
flowMap[n.z].children.push({
|
|
100
|
+
id: n.id,
|
|
101
|
+
node: n,
|
|
102
|
+
label: n.name || n.id,
|
|
103
|
+
selected: false,
|
|
104
|
+
checkbox: false,
|
|
105
|
+
radio: false
|
|
106
|
+
});
|
|
107
|
+
candidateNodesCount++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
for (const key in flowMap)
|
|
113
|
+
{
|
|
114
|
+
flowMap[key].children.sort((a, b) => a.label.localeCompare(b.label));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const flowsFiltered = flows.filter(function (f) { return f.children.length > 0 });
|
|
118
|
+
treeList.treeList("empty");
|
|
119
|
+
treeList.treeList("data", flowsFiltered);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resizeNodeList()
|
|
123
|
+
{
|
|
124
|
+
var rows = $("#dialog-form>div:not(.node-input-link-row)");
|
|
125
|
+
var height = $("#dialog-form").height();
|
|
126
|
+
for (var i = 0; i < rows.length; i++)
|
|
127
|
+
{
|
|
128
|
+
height -= $(rows[i]).outerHeight(true);
|
|
129
|
+
}
|
|
130
|
+
var editorRow = $("#dialog-form>div.node-input-link-row");
|
|
131
|
+
height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
|
|
132
|
+
$(".node-input-link-row").css("height", height + "px");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
RED.nodes.registerType("smart_shutter-control", {
|
|
136
|
+
category: "Smart Nodes",
|
|
137
|
+
paletteLabel: "Shutter control",
|
|
138
|
+
color: "#C882FF",
|
|
139
|
+
defaults: {
|
|
140
|
+
name: { value: "" },
|
|
141
|
+
links: { value: [], type: "smart_central-control[]" }
|
|
142
|
+
},
|
|
143
|
+
inputs: 1,
|
|
144
|
+
outputs: 3,
|
|
145
|
+
outputLabels: ["Up/Down", "Stop", "Position"],
|
|
146
|
+
icon: "font-awesome/fa-align-justify",
|
|
147
|
+
label: function ()
|
|
148
|
+
{
|
|
149
|
+
return this.name || "Shutter control";
|
|
150
|
+
},
|
|
151
|
+
oneditprepare: function ()
|
|
152
|
+
{
|
|
153
|
+
let node = this;
|
|
154
|
+
onEditPrepare(this, ["smart_central-control"]);
|
|
155
|
+
initTreeList(node, ["smart_central-control"]);
|
|
156
|
+
},
|
|
157
|
+
onadd: function ()
|
|
158
|
+
{
|
|
159
|
+
this.links = [];
|
|
160
|
+
},
|
|
161
|
+
oneditresize: resizeNodeList
|
|
162
|
+
});
|
|
163
|
+
})();
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<script type="text/html" data-template-name="smart_shutter-control">
|
|
167
|
+
<div class="form-row">
|
|
168
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
169
|
+
<input type="text" id="node-input-name" placeholder="Name" />
|
|
170
|
+
</div>
|
|
171
|
+
<span><i class="fa fa-link"></i>Dieser Baustein wird von folgenden Zentralbausteinen gesteuert:</span>
|
|
172
|
+
<div class="form-row node-input-link-row node-input-link-rows"></div>
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<script type="text/html" data-help-name="smart_shutter-control">
|
|
176
|
+
<p>
|
|
177
|
+
<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/>
|
|
178
|
+
Diese Node verwendet nur den Teil <code>name</code>. <code>#</code> und <code>nummer</code> sind dabei optional.
|
|
179
|
+
</p>
|
|
180
|
+
<p>
|
|
181
|
+
Diese Node steuert Rollladen oder Jalousien.
|
|
182
|
+
Es gibt 3 Ausgänge die angesteuert werden können:
|
|
183
|
+
<ol>
|
|
184
|
+
<li><b>Auf/Ab:</b> <code>msg.payload = false</code> lässt den Rollladen nach oben fahren und <code>msg.payload = true</code> lässt den Rollladen nach unten fahren.</li>
|
|
185
|
+
<li><b>Stop:</b> <code>msg.payload = true</code> lässt den Rollladen stoppen.</li>
|
|
186
|
+
<li><b>Position:</b> <code>msg.payload = 42</code> Lässt den Rollladen auf 42% fahren.</li>
|
|
187
|
+
</ol>
|
|
188
|
+
Die Ausgänge sind den jeweiligen KNX Gruppenadressen zuzuordnen.
|
|
189
|
+
</p>
|
|
190
|
+
<p>
|
|
191
|
+
Diese Node erwartet folgende Topics als Eingang:<br/>
|
|
192
|
+
<table>
|
|
193
|
+
<thead>
|
|
194
|
+
<tr>
|
|
195
|
+
<th>Topic</th>
|
|
196
|
+
<th>Beschreibung</th>
|
|
197
|
+
</tr>
|
|
198
|
+
</thead>
|
|
199
|
+
<tbody>
|
|
200
|
+
<tr>
|
|
201
|
+
<td><code>status</code> oder <code>status_position</code></td>
|
|
202
|
+
<td>
|
|
203
|
+
Meldet der Node die aktuelle Position.
|
|
204
|
+
Dadurch kann die Node feststellen in welche Richtung ein Rollladen als letztes gefahren ist.
|
|
205
|
+
Dies wird für das topic <b>toggle</b> benötigt um die nächste Laufrichtung bei einer 1-Tasten Bedienung zu ermitteln.
|
|
206
|
+
</td>
|
|
207
|
+
</tr>
|
|
208
|
+
<tr>
|
|
209
|
+
<td><code>up_down</code></td>
|
|
210
|
+
<td>
|
|
211
|
+
Nimmt einen Befehl von Home Assistant entgegen um informiert zu werden ob der Rollladen läuft und in welche Richtung.<br/>
|
|
212
|
+
<strong>Wichtig:</strong> Dieses Topic startet den Rollladen <u>nicht</u>, da die Gruppenadresse direkt mit dem Jalousiemodul verbunden sein muss.
|
|
213
|
+
</td>
|
|
214
|
+
</tr>
|
|
215
|
+
<tr>
|
|
216
|
+
<td><code>up</code></td>
|
|
217
|
+
<td>Sendet einen Hochfahrbefehl, falls der Rollladen nicht bereits nach oben fährt. Ggf. wird vorher noch ein Stop-Befehl gesendet.</td>
|
|
218
|
+
</tr>
|
|
219
|
+
<tr>
|
|
220
|
+
<td><code>up_stop</code></td>
|
|
221
|
+
<td>Sendet abwechselnd einen Stop- und einen Hochfahrbefehl.</td>
|
|
222
|
+
</tr>
|
|
223
|
+
<tr>
|
|
224
|
+
<td><code>down</code></td>
|
|
225
|
+
<td>Sendet einen Runterfahrbefehl, falls der Rollladen nicht bereits nach unten fährt. Ggf. wird vorher noch ein Stop-Befehl gesendet.</td>
|
|
226
|
+
</tr>
|
|
227
|
+
<tr>
|
|
228
|
+
<td><code>down_stop</code></td>
|
|
229
|
+
<td>Sendet abwechselnd einen Stop- und einen Runterfahrbefehl.</td>
|
|
230
|
+
</tr>
|
|
231
|
+
<tr>
|
|
232
|
+
<td><code>stop</code></td>
|
|
233
|
+
<td>Sendet einen Stopbefehl.</td>
|
|
234
|
+
</tr>
|
|
235
|
+
<tr>
|
|
236
|
+
<td><code>position</code></td>
|
|
237
|
+
<td>Sendet einen Positionsbefehl. <code>msg.payload</code> sollte ein Wert zwischen 0 (offen) und 100 (geschlossen) haben.</td>
|
|
238
|
+
</tr>
|
|
239
|
+
<tr>
|
|
240
|
+
<td><code>toggle</code> (default)</td>
|
|
241
|
+
<td>Schaltet den Rollladen abwechselnd auf hoch, stop, runter, stop.</td>
|
|
242
|
+
</tr>
|
|
243
|
+
</tbody>
|
|
244
|
+
</table>
|
|
245
|
+
</p>
|
|
246
|
+
<p>
|
|
247
|
+
Diese Node verwaltet keine Laufzeit für den Rollladen selbst. Diese muss über ETS für den Ausgang konfiguriert werden.
|
|
248
|
+
Es ist jedoch möglich, den Rollladen nach einer definierten Zeit automatisch abzuschalten.
|
|
249
|
+
Es ist jedoch möglich, den Rollladen nach einer definierten Zeit automatisch abzuschalten.
|
|
250
|
+
Beispiel: <code>msg = { "topic": "up", "time_on": 5000 }</code> oder <code>msg = { "topic": "up", "time_on": "5s" }</code><br/>
|
|
251
|
+
Diese Nachricht lässt den Rollladen für 5000 Millisekunden / 5 Sekunden nach oben fahren. Sollte es sich um eine Jalousie halten, werden die Lamellen entsprechend gedreht.
|
|
252
|
+
Als Einheit für die Zeit können folgende Werte verwendet werden:
|
|
253
|
+
<table>
|
|
254
|
+
<thead>
|
|
255
|
+
<tr>
|
|
256
|
+
<th>Einheit</th>
|
|
257
|
+
<th>Beschreibung</th>
|
|
258
|
+
</tr>
|
|
259
|
+
</thead>
|
|
260
|
+
<tbody>
|
|
261
|
+
<tr>
|
|
262
|
+
<td><code>ms</code> (default)</td>
|
|
263
|
+
<td>Millisekunden</td>
|
|
264
|
+
</tr>
|
|
265
|
+
<tr>
|
|
266
|
+
<td><code>s</code> oder <code>sec</code></td>
|
|
267
|
+
<td>Sekunden</td>
|
|
268
|
+
</tr>
|
|
269
|
+
<tr>
|
|
270
|
+
<td><code>m</code> oder <code>min</code></td>
|
|
271
|
+
<td>Mintun.</td>
|
|
272
|
+
</tr>
|
|
273
|
+
<tr>
|
|
274
|
+
<td><code>h</code></td>
|
|
275
|
+
<td>Stunden</td>
|
|
276
|
+
</tr>
|
|
277
|
+
</tbody>
|
|
278
|
+
</table>
|
|
279
|
+
</p>
|
|
280
|
+
<p>
|
|
281
|
+
Die Angabe einer Zeit funktioniert nicht mit dem topic <b>position</b>.
|
|
282
|
+
</p>
|
|
283
|
+
</script>
|