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 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
- # Create node package
127
- run `node pack` to create a new *.tgz node module.
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
@@ -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") initTreeList(node, ["smart_shutter-complex-control", "smart_shutter-control"]);
283
- else initTreeList(node, ["smart_light-control", "smart_scene-control"]);
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
- icon: { value: "light" }, // light | outlet
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 || (this.icon == "outlet" ? "Outlet control" : "Light control");
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-icon"><i class="fa fa-picture-o"></i> Icon</label>
237
- <input id="node-input-icon" />
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.1.1",
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>down</code></td>
265
- <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>
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
+ };