smart-nodes 0.1.1 → 0.3.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/README.md +14 -2
- package/central/central.html +4 -2
- package/light-control/light-control.html +14 -6
- package/light-control/light-control.js +11 -0
- package/package.json +5 -3
- package/scene-control/scene-control.html +6 -0
- package/shutter-complex-control/shutter-complex-control.html +22 -2
- package/shutter-control/shutter-control.html +15 -0
- package/text-exec/text-exec.html +311 -0
- package/text-exec/text-exec.js +176 -0
package/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
The smart nodes was created to control smart home devices like lights, power outlets shutter and some more.
|
|
4
4
|
This controls are designed to work with the [node-red-contrib-knx-ultimate](https://github.com/Supergiovane/node-red-contrib-knx-ultimate) but it could also work with other smart technologies.
|
|
5
5
|
|
|
6
|
+
The knx binary input and output/switching modules should be configured very stupid. Closing a binary input should output 1 and releasing 0.
|
|
7
|
+
Same for the output/switching modules, 1 should turn on and 0 turn out. That makes it easy to count the presses or check for short and long presses.
|
|
8
|
+
Some node has to filter out the binary inputs if they goes to 0 or in other words, if `msg.payload == false`. Please see the internal node documentation then this signals are ignored. As an example sending a toggle topic for closing an input and also for releasing would toggle a light twice.
|
|
9
|
+
|
|
6
10
|
Sometimes one source node should be connected to multiple smart nodes which requires different topics.
|
|
7
11
|
To avoid requiring many change nodes, the smart node are using a special `msg.topic` notation. You can always send topics in the format `name#number`, e.g. `toggle#1`.
|
|
8
12
|
Smart nodes that requires a name are using only the name part. the # and the number are optional. Smart nodes that requries a number will only use the number, the name and # is also optional.
|
|
@@ -123,5 +127,13 @@ This control can send a defined message on defined times.
|
|
|
123
127
|
* The state can be saved between NodeRed restarts.
|
|
124
128
|
* The last message can automatically be sent 10 seconds after a deployment.
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
## 14. Text execution
|
|
131
|
+
This control parses a text and performs actions to the selected and matching smart nodes
|
|
132
|
+
|
|
133
|
+
### **Features:**
|
|
134
|
+
* Control light and scenes in german and english
|
|
135
|
+
* Control shutter in german and english
|
|
136
|
+
|
|
137
|
+
### Examples
|
|
138
|
+
* Turn on kitchen and cupboard off
|
|
139
|
+
* Open shutter in kitchen and turn light off
|
package/central/central.html
CHANGED
|
@@ -279,8 +279,10 @@
|
|
|
279
279
|
onEditPrepare(this, ["smart_shutter-complex-control", "smart_shutter-control"]);
|
|
280
280
|
$("#node-input-mode").on("change", function ()
|
|
281
281
|
{
|
|
282
|
-
if (this.value == "shutter")
|
|
283
|
-
|
|
282
|
+
if (this.value == "shutter")
|
|
283
|
+
initTreeList(node, ["smart_shutter-complex-control", "smart_shutter-control"]);
|
|
284
|
+
else
|
|
285
|
+
initTreeList(node, ["smart_light-control", "smart_scene-control"]);
|
|
284
286
|
});
|
|
285
287
|
|
|
286
288
|
if (!this.mode)
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
color: "#C882FF",
|
|
139
139
|
defaults: {
|
|
140
140
|
name: { value: "" },
|
|
141
|
-
|
|
141
|
+
exec_text_names: { value: "" },
|
|
142
142
|
max_time_on: { value: "0" },
|
|
143
143
|
max_time_on_unit: { value: "s" },
|
|
144
144
|
alarm_action: { value: 'NOTHING' }, // NOTHING | ON | OFF
|
|
@@ -148,13 +148,11 @@
|
|
|
148
148
|
outputs: 1,
|
|
149
149
|
icon: function ()
|
|
150
150
|
{
|
|
151
|
-
if (this.icon == "outlet")
|
|
152
|
-
return "font-awesome/fa-plug";
|
|
153
151
|
return "font-awesome/fa-lightbulb-o";
|
|
154
152
|
},
|
|
155
153
|
label: function ()
|
|
156
154
|
{
|
|
157
|
-
return this.name ||
|
|
155
|
+
return this.name || "Light control";
|
|
158
156
|
},
|
|
159
157
|
oneditprepare: function ()
|
|
160
158
|
{
|
|
@@ -233,8 +231,9 @@
|
|
|
233
231
|
<input type="text" id="node-input-name" placeholder="Name" />
|
|
234
232
|
</div>
|
|
235
233
|
<div class="form-row">
|
|
236
|
-
<label for="node-input-
|
|
237
|
-
<input id="node-input-
|
|
234
|
+
<label for="node-input-exec_text_names"><i class="fa fa-comments-o"></i> Text</label>
|
|
235
|
+
<input id="node-input-exec_text_names" type="text" />
|
|
236
|
+
<div style="max-width: 450px;">Diese Node kann über die eingegebenen Wörter gesteuert werden. Mehrere Wörter werden durch ein Komma getrennt.</div>
|
|
238
237
|
</div>
|
|
239
238
|
<div class="form-row">
|
|
240
239
|
<label for="node-input-max_time_on"><i class="fa fa-clock-o"></i> Zeit Ein</label>
|
|
@@ -265,6 +264,7 @@
|
|
|
265
264
|
<tr>
|
|
266
265
|
<th>Topic</th>
|
|
267
266
|
<th>Beschreibung</th>
|
|
267
|
+
<th>msg.payload == false wird ignoriert</th>
|
|
268
268
|
</tr>
|
|
269
269
|
</thead>
|
|
270
270
|
<tbody>
|
|
@@ -274,14 +274,17 @@
|
|
|
274
274
|
Gibt über <code>msg.payload = true</code> oder <code>msg.payload = false</code> den aktuellen Status des Ausgangs an.<br/>
|
|
275
275
|
Bei einem Wechsel von ausgeschaltet nach eingeschaltet wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.
|
|
276
276
|
</td>
|
|
277
|
+
<td>Nein</td>
|
|
277
278
|
</tr>
|
|
278
279
|
<tr>
|
|
279
280
|
<td><code>on</code></td>
|
|
280
281
|
<td>Schaltet den Ausgang ein und startet die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.</td>
|
|
282
|
+
<td>Ja</td>
|
|
281
283
|
</tr>
|
|
282
284
|
<tr>
|
|
283
285
|
<td><code>off</code></td>
|
|
284
286
|
<td>Schaltet den Ausgang aus.</td>
|
|
287
|
+
<td>Ja</td>
|
|
285
288
|
</tr>
|
|
286
289
|
<tr>
|
|
287
290
|
<td><code>set</code></td>
|
|
@@ -289,6 +292,7 @@
|
|
|
289
292
|
Schaltet den Ausgang bei <code>msg.payload = true</code> ein und bei <code>msg.payload = false</code> aus.<br/>
|
|
290
293
|
Bei einem Wechsel von ausgeschaltet nach eingeschaltet wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.
|
|
291
294
|
</td>
|
|
295
|
+
<td>Nein</td>
|
|
292
296
|
</tr>
|
|
293
297
|
<tr>
|
|
294
298
|
<td><code>set_permanent</code></td>
|
|
@@ -296,6 +300,7 @@
|
|
|
296
300
|
Schaltet den Ausgang bei <code>msg.payload = true</code> dauerhaft ein und bei <code>msg.payload = false</code> aus.<br/>
|
|
297
301
|
Es wird dabei keine Zeitmessung gestartet.
|
|
298
302
|
</td>
|
|
303
|
+
<td>Nein</td>
|
|
299
304
|
</tr>
|
|
300
305
|
<tr>
|
|
301
306
|
<td><code>motion</code></td>
|
|
@@ -304,10 +309,12 @@
|
|
|
304
309
|
Bei <code>msg.payload = false</code> wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.<br/>
|
|
305
310
|
Ist keine Zeit angegeben oder hinterlegt, schaltet sich der Ausgang sofort aus.
|
|
306
311
|
</td>
|
|
312
|
+
<td>Nein</td>
|
|
307
313
|
</tr>
|
|
308
314
|
<tr>
|
|
309
315
|
<td><code>alarm</code></td>
|
|
310
316
|
<td>Setzt den aktuellen Alarmzustand auf den Wert von <code>msg.payload</code> und löst die entsprechende Aktion aus.</td>
|
|
317
|
+
<td>Nein</td>
|
|
311
318
|
</tr>
|
|
312
319
|
<tr>
|
|
313
320
|
<td><code>toggle</code> (default)</td>
|
|
@@ -315,6 +322,7 @@
|
|
|
315
322
|
Schaltet den Ausgang abwechselnd ein und aus.<br/>
|
|
316
323
|
Bei einem Wechsel von ausgeschaltet nach eingeschaltet wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.
|
|
317
324
|
</td>
|
|
325
|
+
<td>Ja</td>
|
|
318
326
|
</tr>
|
|
319
327
|
</tbody>
|
|
320
328
|
</table>
|
|
@@ -8,6 +8,9 @@ module.exports = function (RED)
|
|
|
8
8
|
const smartContext = require("../persistence.js")(RED);
|
|
9
9
|
const helper = require("../smart_helper.js");
|
|
10
10
|
|
|
11
|
+
// used from text-exec node
|
|
12
|
+
node.exec_text_names = config.exec_text_names;
|
|
13
|
+
|
|
11
14
|
// persistent values
|
|
12
15
|
var nodeSettings = Object.assign({}, {
|
|
13
16
|
last_value: false,
|
|
@@ -62,10 +65,18 @@ module.exports = function (RED)
|
|
|
62
65
|
break;
|
|
63
66
|
|
|
64
67
|
case "off":
|
|
68
|
+
// If button is released, don't handle this message
|
|
69
|
+
if (msg.payload === false)
|
|
70
|
+
return;
|
|
71
|
+
|
|
65
72
|
nodeSettings.last_value = false;
|
|
66
73
|
break;
|
|
67
74
|
|
|
68
75
|
case "on":
|
|
76
|
+
// If button is released, don't handle this message
|
|
77
|
+
if (msg.payload === false)
|
|
78
|
+
return;
|
|
79
|
+
|
|
69
80
|
nodeSettings.last_value = true;
|
|
70
81
|
break;
|
|
71
82
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-nodes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Smart Nodes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"comparator",
|
|
20
20
|
"statistic",
|
|
21
21
|
"scheduler",
|
|
22
|
-
"central"
|
|
22
|
+
"central",
|
|
23
|
+
"text execution"
|
|
23
24
|
],
|
|
24
25
|
"scripts": {
|
|
25
26
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"statistic": "statistic/statistic.js",
|
|
43
44
|
"scheduler": "scheduler/scheduler.js",
|
|
44
45
|
"delay": "delay/delay.js",
|
|
45
|
-
"central": "central/central.js"
|
|
46
|
+
"central": "central/central.js",
|
|
47
|
+
"text-exec": "text-exec/text-exec.js"
|
|
46
48
|
}
|
|
47
49
|
},
|
|
48
50
|
"repository": {
|
|
@@ -217,6 +217,7 @@
|
|
|
217
217
|
color: "#C882FF",
|
|
218
218
|
defaults: {
|
|
219
219
|
name: { value: "" },
|
|
220
|
+
exec_text_names: { value: "" },
|
|
220
221
|
max_time_on: { value: "0" },
|
|
221
222
|
max_time_on_unit: { value: "s" },
|
|
222
223
|
outputs: { value: 1 },
|
|
@@ -443,6 +444,11 @@
|
|
|
443
444
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
444
445
|
<input type="text" id="node-input-name" placeholder="Name" />
|
|
445
446
|
</div>
|
|
447
|
+
<div class="form-row">
|
|
448
|
+
<label for="node-input-exec_text_names"><i class="fa fa-comments-o"></i> Text</label>
|
|
449
|
+
<input id="node-input-exec_text_names" type="text" />
|
|
450
|
+
<div style="max-width: 450px;">Diese Node kann über die eingegebenen Wörter gesteuert werden. Mehrere Wörter werden durch ein Komma getrennt.</div>
|
|
451
|
+
</div>
|
|
446
452
|
<div class="form-row">
|
|
447
453
|
<label for="node-input-max_time_on"><i class="fa fa-clock-o"></i> Zeit Ein</label>
|
|
448
454
|
<input id="node-input-max_time_on" value="0" />
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
color: "#C882FF",
|
|
139
139
|
defaults: {
|
|
140
140
|
name: { value: "" },
|
|
141
|
+
exec_text_names: { value: "" },
|
|
141
142
|
max_time: { value: 60 },
|
|
142
143
|
revert_time_ms: { value: 100 },
|
|
143
144
|
alarm_action: { value: 'NOTHING' }, // NOTHING | UP | DOWN
|
|
@@ -211,6 +212,11 @@
|
|
|
211
212
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
212
213
|
<input type="text" id="node-input-name" placeholder="Name" />
|
|
213
214
|
</div>
|
|
215
|
+
<div class="form-row">
|
|
216
|
+
<label for="node-input-exec_text_names"><i class="fa fa-comments-o"></i> Text</label>
|
|
217
|
+
<input id="node-input-exec_text_names" type="text" />
|
|
218
|
+
<div style="max-width: 450px;">Diese Node kann über die eingegebenen Wörter gesteuert werden. Mehrere Wörter werden durch ein Komma getrennt.</div>
|
|
219
|
+
</div>
|
|
214
220
|
<div class="form-row">
|
|
215
221
|
<label for="node-input-max_time"><i class="fa fa-clock-o"></i> Zeit [s]</label>
|
|
216
222
|
<input id="node-input-max_time" placeholder="Zeit für eine komplette Fahrt [s]" />
|
|
@@ -253,40 +259,54 @@
|
|
|
253
259
|
<tr>
|
|
254
260
|
<th>Topic</th>
|
|
255
261
|
<th>Beschreibung</th>
|
|
262
|
+
<th>msg.payload == false wird ignoriert</th>
|
|
256
263
|
</tr>
|
|
257
264
|
</thead>
|
|
258
265
|
<tbody>
|
|
259
266
|
<tr>
|
|
260
267
|
<td><code>up_stop</code></td>
|
|
261
268
|
<td>Sendet abwechselnd einen Stop- und einen Hochfahrbefehl.</td>
|
|
269
|
+
<td>Ja</td>
|
|
262
270
|
</tr>
|
|
263
271
|
<tr>
|
|
264
|
-
<td><code>
|
|
265
|
-
<td>Sendet einen
|
|
272
|
+
<td><code>up</code></td>
|
|
273
|
+
<td>Sendet einen Hochfahrbefehl, falls der Rollladen nicht bereits nach oben fährt. Ggf. wird vorher noch ein Stop-Befehl gesendet und die eingestellte Zeit gewartet.</td>
|
|
274
|
+
<td>Ja</td>
|
|
266
275
|
</tr>
|
|
267
276
|
<tr>
|
|
268
277
|
<td><code>down_stop</code></td>
|
|
269
278
|
<td>Sendet abwechselnd einen Stop- und einen Runterfahrbefehl.</td>
|
|
279
|
+
<td>Ja</td>
|
|
280
|
+
</tr>
|
|
281
|
+
<tr>
|
|
282
|
+
<td><code>down</code></td>
|
|
283
|
+
<td>Sendet einen Runterfahrbefehl, falls der Rollladen nicht bereits nach unten fährt. Ggf. wird vorher noch ein Stop-Befehl gesendet und die eingestellte Zeit gewartet.</td>
|
|
284
|
+
<td>Ja</td>
|
|
270
285
|
</tr>
|
|
271
286
|
<tr>
|
|
272
287
|
<td><code>up_down</code></td>
|
|
273
288
|
<td>Nimmt einen Befehl von Home Assistant entgegen und leitet die entsprechende Aktion ein.</td>
|
|
289
|
+
<td>Nein</td>
|
|
274
290
|
</tr>
|
|
275
291
|
<tr>
|
|
276
292
|
<td><code>stop</code></td>
|
|
277
293
|
<td>Sendet einen Stopbefehl.</td>
|
|
294
|
+
<td>Ja</td>
|
|
278
295
|
</tr>
|
|
279
296
|
<tr>
|
|
280
297
|
<td><code>position</code></td>
|
|
281
298
|
<td>Sendet einen Positionsbefehl. <code>msg.payload</code> sollte ein Wert zwischen 0 (offen) und 100 (geschlossen) haben.</td>
|
|
299
|
+
<td>Nein</td>
|
|
282
300
|
</tr>
|
|
283
301
|
<tr>
|
|
284
302
|
<td><code>alarm</code></td>
|
|
285
303
|
<td>Setzt den aktuellen Alarmzustand.</td>
|
|
304
|
+
<td>Nein</td>
|
|
286
305
|
</tr>
|
|
287
306
|
<tr>
|
|
288
307
|
<td><code>toggle</code> (default)</td>
|
|
289
308
|
<td>Schaltet den Rollladen abwechselnd auf hoch, stop, runter, stop.</td>
|
|
309
|
+
<td>Ja</td>
|
|
290
310
|
</tr>
|
|
291
311
|
</tbody>
|
|
292
312
|
</table>
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
color: "#C882FF",
|
|
139
139
|
defaults: {
|
|
140
140
|
name: { value: "" },
|
|
141
|
+
exec_text_names: { value: "" },
|
|
141
142
|
links: { value: [], type: "smart_central-control[]" }
|
|
142
143
|
},
|
|
143
144
|
inputs: 1,
|
|
@@ -168,6 +169,11 @@
|
|
|
168
169
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
169
170
|
<input type="text" id="node-input-name" placeholder="Name" />
|
|
170
171
|
</div>
|
|
172
|
+
<div class="form-row">
|
|
173
|
+
<label for="node-input-exec_text_names"><i class="fa fa-comments-o"></i> Text</label>
|
|
174
|
+
<input id="node-input-exec_text_names" type="text" />
|
|
175
|
+
<div style="max-width: 450px;">Diese Node kann über die eingegebenen Wörter gesteuert werden. Mehrere Wörter werden durch ein Komma getrennt.</div>
|
|
176
|
+
</div>
|
|
171
177
|
<span><i class="fa fa-link"></i>Dieser Baustein wird von folgenden Zentralbausteinen gesteuert:</span>
|
|
172
178
|
<div class="form-row node-input-link-row node-input-link-rows"></div>
|
|
173
179
|
</script>
|
|
@@ -194,6 +200,7 @@
|
|
|
194
200
|
<tr>
|
|
195
201
|
<th>Topic</th>
|
|
196
202
|
<th>Beschreibung</th>
|
|
203
|
+
<th>msg.payload == false wird ignoriert</th>
|
|
197
204
|
</tr>
|
|
198
205
|
</thead>
|
|
199
206
|
<tbody>
|
|
@@ -211,34 +218,42 @@
|
|
|
211
218
|
Nimmt einen Befehl von Home Assistant entgegen um informiert zu werden ob der Rollladen läuft und in welche Richtung.<br/>
|
|
212
219
|
<strong>Wichtig:</strong> Dieses Topic startet den Rollladen <u>nicht</u>, da die Gruppenadresse direkt mit dem Jalousiemodul verbunden sein muss.
|
|
213
220
|
</td>
|
|
221
|
+
<td>Nein</td>
|
|
214
222
|
</tr>
|
|
215
223
|
<tr>
|
|
216
224
|
<td><code>up</code></td>
|
|
217
225
|
<td>Sendet einen Hochfahrbefehl, falls der Rollladen nicht bereits nach oben fährt. Ggf. wird vorher noch ein Stop-Befehl gesendet.</td>
|
|
226
|
+
<td>Ja</td>
|
|
218
227
|
</tr>
|
|
219
228
|
<tr>
|
|
220
229
|
<td><code>up_stop</code></td>
|
|
221
230
|
<td>Sendet abwechselnd einen Stop- und einen Hochfahrbefehl.</td>
|
|
231
|
+
<td>Ja</td>
|
|
222
232
|
</tr>
|
|
223
233
|
<tr>
|
|
224
234
|
<td><code>down</code></td>
|
|
225
235
|
<td>Sendet einen Runterfahrbefehl, falls der Rollladen nicht bereits nach unten fährt. Ggf. wird vorher noch ein Stop-Befehl gesendet.</td>
|
|
236
|
+
<td>Ja</td>
|
|
226
237
|
</tr>
|
|
227
238
|
<tr>
|
|
228
239
|
<td><code>down_stop</code></td>
|
|
229
240
|
<td>Sendet abwechselnd einen Stop- und einen Runterfahrbefehl.</td>
|
|
241
|
+
<td>Ja</td>
|
|
230
242
|
</tr>
|
|
231
243
|
<tr>
|
|
232
244
|
<td><code>stop</code></td>
|
|
233
245
|
<td>Sendet einen Stopbefehl.</td>
|
|
246
|
+
<td>Ja</td>
|
|
234
247
|
</tr>
|
|
235
248
|
<tr>
|
|
236
249
|
<td><code>position</code></td>
|
|
237
250
|
<td>Sendet einen Positionsbefehl. <code>msg.payload</code> sollte ein Wert zwischen 0 (offen) und 100 (geschlossen) haben.</td>
|
|
251
|
+
<td>Nein</td>
|
|
238
252
|
</tr>
|
|
239
253
|
<tr>
|
|
240
254
|
<td><code>toggle</code> (default)</td>
|
|
241
255
|
<td>Schaltet den Rollladen abwechselnd auf hoch, stop, runter, stop.</td>
|
|
256
|
+
<td>Ja</td>
|
|
242
257
|
</tr>
|
|
243
258
|
</tbody>
|
|
244
259
|
</table>
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function ()
|
|
3
|
+
{
|
|
4
|
+
let treeList; // The tree control itself
|
|
5
|
+
let candidateNodesCount = 0; // The total entries shown in the list, used for the search box as info
|
|
6
|
+
let flows = []; // A List with all flow maps
|
|
7
|
+
let flowMap = {}; // A key value list with all flow id => flow details and children
|
|
8
|
+
|
|
9
|
+
function onEditPrepare(node, targetTypes)
|
|
10
|
+
{
|
|
11
|
+
if (!node.links)
|
|
12
|
+
node.links = [];
|
|
13
|
+
|
|
14
|
+
node.oldLinks = [];
|
|
15
|
+
|
|
16
|
+
const activeSubflow = RED.nodes.subflow(node.z);
|
|
17
|
+
|
|
18
|
+
// init tree list control
|
|
19
|
+
treeList = $("<div>")
|
|
20
|
+
.css({ width: "100%", height: "100%" })
|
|
21
|
+
.appendTo(".node-input-link-row")
|
|
22
|
+
.treeList({ autoSelect: false })
|
|
23
|
+
.on("treelistitemmouseover", function (e, item)
|
|
24
|
+
{
|
|
25
|
+
if (item.node)
|
|
26
|
+
{
|
|
27
|
+
item.node.highlighted = true;
|
|
28
|
+
item.node.dirty = true;
|
|
29
|
+
RED.view.redraw();
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.on("treelistitemmouseout", function (e, item)
|
|
33
|
+
{
|
|
34
|
+
if (item.node)
|
|
35
|
+
{
|
|
36
|
+
item.node.highlighted = false;
|
|
37
|
+
item.node.dirty = true;
|
|
38
|
+
RED.view.redraw();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// init search box
|
|
43
|
+
const search = $("#node-input-link-target-filter").searchBox({
|
|
44
|
+
style: "compact",
|
|
45
|
+
delay: 300,
|
|
46
|
+
change: function ()
|
|
47
|
+
{
|
|
48
|
+
var val = $(this).val().trim().toLowerCase();
|
|
49
|
+
if (val === "")
|
|
50
|
+
{
|
|
51
|
+
treeList.treeList("filter", null);
|
|
52
|
+
search.searchBox("count", "");
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
{
|
|
56
|
+
const count = treeList.treeList("filter", function (item)
|
|
57
|
+
{
|
|
58
|
+
return item.label.toLowerCase().indexOf(val) > -1 || (item.parent && item.parent.label.toLowerCase().indexOf(val) > -1) || (item.node && item.node.type.toLowerCase().indexOf(val) > -1);
|
|
59
|
+
});
|
|
60
|
+
search.searchBox("count", count + " / " + candidateNodesCount);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// clear shown tree data
|
|
66
|
+
flows = [];
|
|
67
|
+
flowMap = {};
|
|
68
|
+
|
|
69
|
+
// Add all existing flows
|
|
70
|
+
if (activeSubflow)
|
|
71
|
+
{
|
|
72
|
+
flowMap[activeSubflow.id] = {
|
|
73
|
+
id: activeSubflow.id,
|
|
74
|
+
class: "red-ui-palette-header",
|
|
75
|
+
label: "Subflow : " + (activeSubflow.name || activeSubflow.id),
|
|
76
|
+
expanded: true,
|
|
77
|
+
children: [],
|
|
78
|
+
};
|
|
79
|
+
flows.push(flowMap[activeSubflow.id]);
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
{
|
|
83
|
+
RED.nodes.eachWorkspace(function (ws)
|
|
84
|
+
{
|
|
85
|
+
if (!ws.disabled)
|
|
86
|
+
{
|
|
87
|
+
flowMap[ws.id] = {
|
|
88
|
+
id: ws.id,
|
|
89
|
+
class: "red-ui-palette-header",
|
|
90
|
+
label: (ws.label || ws.id) + (node.z === ws.id ? " *" : ""),
|
|
91
|
+
expanded: true,
|
|
92
|
+
children: [],
|
|
93
|
+
};
|
|
94
|
+
flows.push(flowMap[ws.id]);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setTimeout(function ()
|
|
100
|
+
{
|
|
101
|
+
treeList.treeList("show", node.z);
|
|
102
|
+
}, 100);
|
|
103
|
+
|
|
104
|
+
candidateNodesCount = 0;
|
|
105
|
+
for (const key in flowMap)
|
|
106
|
+
{
|
|
107
|
+
flowMap[key].children = [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let candidateNodes = [];
|
|
111
|
+
|
|
112
|
+
targetTypes.forEach(function (targetType)
|
|
113
|
+
{
|
|
114
|
+
candidateNodes = candidateNodes.concat(RED.nodes.filterNodes({ type: targetType }));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Add all nodes, matching the given types
|
|
118
|
+
candidateNodes.forEach(function (n)
|
|
119
|
+
{
|
|
120
|
+
if (flowMap[n.z])
|
|
121
|
+
{
|
|
122
|
+
const isChecked = node.links.indexOf(n.id) !== -1 || (n.links || []).indexOf(node.id) !== -1;
|
|
123
|
+
if (isChecked) node.oldLinks.push(n.id);
|
|
124
|
+
|
|
125
|
+
flowMap[n.z].children.push({
|
|
126
|
+
id: n.id,
|
|
127
|
+
node: n,
|
|
128
|
+
label: n.type.replace("smart_", "").replace("-control", "").replace("-complex", "") + ": " + (n.name || n.id),
|
|
129
|
+
selected: isChecked,
|
|
130
|
+
checkbox: true,
|
|
131
|
+
radio: false,
|
|
132
|
+
});
|
|
133
|
+
candidateNodesCount++;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Sort nodes by name
|
|
138
|
+
for (const key in flowMap)
|
|
139
|
+
{
|
|
140
|
+
flowMap[key].children.sort((a, b) => a.label.localeCompare(b.label));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// filter empty flows
|
|
144
|
+
const flowsFiltered = flows.filter(function (f)
|
|
145
|
+
{
|
|
146
|
+
return f.children.length > 0;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// clear list and refill it
|
|
150
|
+
treeList.treeList("empty");
|
|
151
|
+
treeList.treeList("data", flowsFiltered);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resizeNodeList()
|
|
155
|
+
{
|
|
156
|
+
// calculate new heigt if browser is beeing resized
|
|
157
|
+
var rows = $("#dialog-form>div:not(.node-input-link-row)");
|
|
158
|
+
var height = $("#dialog-form").height();
|
|
159
|
+
for (var i = 0; i < rows.length; i++)
|
|
160
|
+
{
|
|
161
|
+
height -= $(rows[i]).outerHeight(true);
|
|
162
|
+
}
|
|
163
|
+
var editorRow = $("#dialog-form>div.node-input-link-row");
|
|
164
|
+
height -= parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom"));
|
|
165
|
+
$(".node-input-link-row").css("height", height + "px");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onEditSave(node)
|
|
169
|
+
{
|
|
170
|
+
var flows = treeList.treeList("data");
|
|
171
|
+
node.links = [];
|
|
172
|
+
|
|
173
|
+
// Set own links for selected nodes
|
|
174
|
+
flows.forEach(function (f)
|
|
175
|
+
{
|
|
176
|
+
f.children.forEach(function (n)
|
|
177
|
+
{
|
|
178
|
+
if (n.selected)
|
|
179
|
+
node.links.push(n.id);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
node.oldLinks.sort();
|
|
184
|
+
node.links.sort();
|
|
185
|
+
|
|
186
|
+
// Mark new and old links
|
|
187
|
+
var nodeMap = {};
|
|
188
|
+
var length = Math.max(node.oldLinks.length, node.links.length);
|
|
189
|
+
for (var i = 0; i < length; i++)
|
|
190
|
+
{
|
|
191
|
+
if (i < node.oldLinks.length)
|
|
192
|
+
{
|
|
193
|
+
nodeMap[node.oldLinks[i]] = nodeMap[node.oldLinks[i]] || {};
|
|
194
|
+
nodeMap[node.oldLinks[i]].old = true;
|
|
195
|
+
}
|
|
196
|
+
if (i < node.links.length)
|
|
197
|
+
{
|
|
198
|
+
nodeMap[node.links[i]] = nodeMap[node.links[i]] || {};
|
|
199
|
+
nodeMap[node.links[i]].new = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Remove own node from old linked nodes
|
|
204
|
+
// Add own node to new linked nodes
|
|
205
|
+
var n;
|
|
206
|
+
for (var id in nodeMap)
|
|
207
|
+
{
|
|
208
|
+
if (nodeMap.hasOwnProperty(id))
|
|
209
|
+
{
|
|
210
|
+
n = RED.nodes.node(id);
|
|
211
|
+
if (n)
|
|
212
|
+
{
|
|
213
|
+
// init links and remove duplicate links
|
|
214
|
+
if (!n.links)
|
|
215
|
+
n.links = [];
|
|
216
|
+
else
|
|
217
|
+
n.links = n.links.filter((value, index, array) => array.indexOf(value) === index);
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if (nodeMap[id].old && !nodeMap[id].new)
|
|
221
|
+
{
|
|
222
|
+
// Removed id
|
|
223
|
+
i = n.links.indexOf(node.id);
|
|
224
|
+
if (i > -1) n.links.splice(i, 1);
|
|
225
|
+
}
|
|
226
|
+
else if (nodeMap[id].new)
|
|
227
|
+
{
|
|
228
|
+
// Added id
|
|
229
|
+
i = n.links.indexOf(node.id);
|
|
230
|
+
if (i === -1) n.links.push(node.id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// This function is called, when copying and pasting central node
|
|
238
|
+
function onAdd()
|
|
239
|
+
{
|
|
240
|
+
for (var i = 0; i < this.links.length; i++)
|
|
241
|
+
{
|
|
242
|
+
var n = RED.nodes.node(this.links[i]);
|
|
243
|
+
if (n && n.links.indexOf(this.id) === -1)
|
|
244
|
+
{
|
|
245
|
+
n.links.push(this.id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
RED.nodes.registerType("smart_text-exec", {
|
|
251
|
+
category: "Smart Nodes",
|
|
252
|
+
paletteLabel: "Text exec control",
|
|
253
|
+
color: "#E9967A",
|
|
254
|
+
defaults: {
|
|
255
|
+
name: { value: "" },
|
|
256
|
+
links: { value: [], type: "smart_shutter-complex-control[]" },
|
|
257
|
+
},
|
|
258
|
+
inputs: 1,
|
|
259
|
+
outputs: 0,
|
|
260
|
+
icon: function ()
|
|
261
|
+
{
|
|
262
|
+
return "font-awesome/fa-cog";
|
|
263
|
+
},
|
|
264
|
+
label: function ()
|
|
265
|
+
{
|
|
266
|
+
return this.name || "Text exec control";
|
|
267
|
+
},
|
|
268
|
+
oneditprepare: function ()
|
|
269
|
+
{
|
|
270
|
+
console.log("Here");
|
|
271
|
+
onEditPrepare(this, ["smart_light-control", "smart_scene-control", "smart_shutter-complex-control", "smart_shutter-control"]);
|
|
272
|
+
},
|
|
273
|
+
oneditsave: function ()
|
|
274
|
+
{
|
|
275
|
+
onEditSave(this);
|
|
276
|
+
},
|
|
277
|
+
onadd: onAdd,
|
|
278
|
+
oneditresize: resizeNodeList,
|
|
279
|
+
});
|
|
280
|
+
})();
|
|
281
|
+
</script>
|
|
282
|
+
|
|
283
|
+
<script type="text/html" data-template-name="smart_text-exec">
|
|
284
|
+
<div class="form-row">
|
|
285
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
286
|
+
<input type="text" id="node-input-name" />
|
|
287
|
+
</div>
|
|
288
|
+
<div class="node-input-link-rows" style="position:relative; height: 30px; text-align: right;"
|
|
289
|
+
><div style="display:inline-block"><input type="text" id="node-input-link-target-filter" /></div
|
|
290
|
+
></div>
|
|
291
|
+
<div class="form-row node-input-link-row node-input-link-rows"></div>
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<script type="text/html" data-help-name="smart_text-exec">
|
|
295
|
+
<p>
|
|
296
|
+
Diese Node nimmt einen Text entgegen, zerlegt ihn und führt die Kommandos auf den ausgewählten und passenden Nodes aus.
|
|
297
|
+
</p>
|
|
298
|
+
<p>
|
|
299
|
+
<strong>Beispiele:</strong><br/><br/>
|
|
300
|
+
<strong>Schalte das Licht im Wohnzimmer ein.</strong><br/>
|
|
301
|
+
Alle Light-controls und Scene-controls die das Wort <strong>Wohnzimmer</strong> enthalten bekommen eine Nachricht mit dem topic <code>on</code>.<br/><br/>
|
|
302
|
+
|
|
303
|
+
<strong>Schalte das Büro ein und die Küche aus.</strong><br/>
|
|
304
|
+
Alle Light-controls und Scene-controls die das Wort <strong>Büro</strong> enthalten bekommen eine Nachricht mit dem topic <code>on</code> und
|
|
305
|
+
alle Light-controls und Scene-controls die das Wort <strong>Küche</strong> enthalten bekommen eine Nachricht mit dem topic <code>off</code>.<br/><br/>
|
|
306
|
+
|
|
307
|
+
<strong>Schließe die Rollladen im Wohnzimmer und schalte das Licht in der Küche an.</strong><br/>
|
|
308
|
+
Alle Schutter-controls und Shutter-complex-controls die das Wort <strong>Wohnzimmer</strong> enthalten bekommen eine Nachricht mit dem topic <code>down</code> und
|
|
309
|
+
alle Light-controls und Scene-controls die das Wort <strong>Küche</strong> enthalten bekommen eine Nachricht mit dem topic <code>on</code>.<br/><br/>
|
|
310
|
+
</p>
|
|
311
|
+
</script>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
module.exports = function (RED)
|
|
2
|
+
{
|
|
3
|
+
const ignoreWords = ["bitte", "schalte", "der", "die", "das", "dem", "den", "in", "im", "bei", "beim", "am", "alle",
|
|
4
|
+
"please", "switch", "turn", "the", "at", "by", "in", "all"]
|
|
5
|
+
|
|
6
|
+
function TextExecNode(config)
|
|
7
|
+
{
|
|
8
|
+
const node = this;
|
|
9
|
+
RED.nodes.createNode(node, config);
|
|
10
|
+
|
|
11
|
+
node.status({});
|
|
12
|
+
|
|
13
|
+
let log = [];
|
|
14
|
+
|
|
15
|
+
node.on("input", function (msg)
|
|
16
|
+
{
|
|
17
|
+
let mode = "light"; // light || shutter
|
|
18
|
+
let action = null; // on || off || up || stop || down
|
|
19
|
+
let message = null;
|
|
20
|
+
|
|
21
|
+
if (typeof msg.payload == "string")
|
|
22
|
+
{
|
|
23
|
+
message = msg.payload;
|
|
24
|
+
}
|
|
25
|
+
else if (typeof msg.payload?.event?.command == "string")
|
|
26
|
+
{
|
|
27
|
+
message = msg.payload?.event?.command;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if message was found
|
|
31
|
+
if (typeof message != "string")
|
|
32
|
+
{
|
|
33
|
+
node.error("Couldn't detect sended message");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
node.status({ fill: "green", shape: "dot", text: message });
|
|
38
|
+
log = [];
|
|
39
|
+
|
|
40
|
+
let searchWords = [];
|
|
41
|
+
for (const word of message.toLowerCase().trim().split(/[\s,.]+/).filter(w => !ignoreWords.includes(w)))
|
|
42
|
+
{
|
|
43
|
+
switch (word)
|
|
44
|
+
{
|
|
45
|
+
case "licht":
|
|
46
|
+
case "steckdose":
|
|
47
|
+
case "light":
|
|
48
|
+
case "power":
|
|
49
|
+
case "outlet":
|
|
50
|
+
// node.log("Set mode to light");
|
|
51
|
+
mode = "light";
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case "rollladen":
|
|
55
|
+
case "rolladen":
|
|
56
|
+
case "beschattung":
|
|
57
|
+
case "fenster":
|
|
58
|
+
case "cover":
|
|
59
|
+
case "shutter":
|
|
60
|
+
case "shading":
|
|
61
|
+
case "window":
|
|
62
|
+
case "windows":
|
|
63
|
+
// node.log("Set mode to shutter");
|
|
64
|
+
mode = "shutter";
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case "an":
|
|
68
|
+
case "on":
|
|
69
|
+
case "ein":
|
|
70
|
+
case "einschalten":
|
|
71
|
+
// node.log("Set action to on");
|
|
72
|
+
action = "on";
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case "aus":
|
|
76
|
+
case "off":
|
|
77
|
+
case "ausschalten":
|
|
78
|
+
// node.log("Set action to off");
|
|
79
|
+
action = "off";
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case "hoch":
|
|
83
|
+
case "up":
|
|
84
|
+
case "auf":
|
|
85
|
+
case "öffne":
|
|
86
|
+
case "open":
|
|
87
|
+
// node.log("Set action to up");
|
|
88
|
+
action = "up";
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case "runter":
|
|
92
|
+
case "down":
|
|
93
|
+
case "ab":
|
|
94
|
+
case "schließe":
|
|
95
|
+
case "close":
|
|
96
|
+
// node.log("Set action to down");
|
|
97
|
+
action = "down";
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case "stoppe":
|
|
101
|
+
case "stop":
|
|
102
|
+
// node.log("Set action to stop");
|
|
103
|
+
action = "stop";
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case "und":
|
|
107
|
+
case "and":
|
|
108
|
+
if (performAction(mode, action, searchWords))
|
|
109
|
+
{
|
|
110
|
+
action = null;
|
|
111
|
+
searchWords = [];
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
searchWords.push(word);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
performAction(mode, action, searchWords);
|
|
122
|
+
|
|
123
|
+
// Show what happened in debug bar
|
|
124
|
+
// node.warn(log);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
let findMatchingNodes = (mode, searchWords) =>
|
|
128
|
+
{
|
|
129
|
+
let result = [];
|
|
130
|
+
|
|
131
|
+
config.links.forEach(link =>
|
|
132
|
+
{
|
|
133
|
+
let linkedNode = RED.nodes.getNode(link);
|
|
134
|
+
|
|
135
|
+
if (linkedNode && linkedNode.exec_text_names != "")
|
|
136
|
+
{
|
|
137
|
+
if (mode == "light" && !["smart_light-control", "smart_scene-control"].includes(linkedNode.type))
|
|
138
|
+
return;
|
|
139
|
+
|
|
140
|
+
if (mode == "shutter" && !["smart_shutter-control", "smart_shutter-complex-control"].includes(linkedNode.type))
|
|
141
|
+
return;
|
|
142
|
+
|
|
143
|
+
let names = linkedNode.exec_text_names.trim().split(/[\s,]+/).map(txt => txt.toLowerCase());
|
|
144
|
+
if (searchWords.filter(n => names.indexOf(n) != -1).length > 0)
|
|
145
|
+
result.push(linkedNode);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let performAction = (mode, action, searchWords) =>
|
|
153
|
+
{
|
|
154
|
+
if (action != null && searchWords.length > 0)
|
|
155
|
+
{
|
|
156
|
+
// node.log("Search for " + searchWords.length + " nodes");
|
|
157
|
+
|
|
158
|
+
let nodes = findMatchingNodes(mode, searchWords);
|
|
159
|
+
if (nodes.length > 0)
|
|
160
|
+
{
|
|
161
|
+
nodes.forEach(node =>
|
|
162
|
+
{
|
|
163
|
+
RED.events.emit("node:" + node.id, { "topic": action });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
log.push({ mode, action, searchWords, foundNodes: nodes.length });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
RED.nodes.registerType("smart_text-exec", TextExecNode);
|
|
176
|
+
};
|