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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +127 -0
  4. package/central/central.html +328 -0
  5. package/central/central.js +95 -0
  6. package/compare/compare.html +137 -0
  7. package/compare/compare.js +151 -0
  8. package/delay/delay.html +192 -0
  9. package/delay/delay.js +175 -0
  10. package/examples/central.json +804 -0
  11. package/examples/central.png +0 -0
  12. package/examples/compare.json +916 -0
  13. package/examples/compare.png +0 -0
  14. package/examples/delay.json +198 -0
  15. package/examples/delay.png +0 -0
  16. package/examples/forwarder.json +152 -0
  17. package/examples/forwarder.png +0 -0
  18. package/examples/hysteresis.json +358 -0
  19. package/examples/hysteresis.png +0 -0
  20. package/examples/light-control.json +499 -0
  21. package/examples/light-control.png +0 -0
  22. package/examples/logic.json +562 -0
  23. package/examples/logic.png +0 -0
  24. package/examples/long-press-control.json +113 -0
  25. package/examples/long-press-control.png +0 -0
  26. package/examples/multi-press-control.json +136 -0
  27. package/examples/multi-press-control.png +0 -0
  28. package/examples/scene-control.json +535 -0
  29. package/examples/scene-control.png +0 -0
  30. package/examples/scheduler.json +164 -0
  31. package/examples/scheduler.png +0 -0
  32. package/examples/shutter-complex-control.json +489 -0
  33. package/examples/shutter-complex-control.png +0 -0
  34. package/examples/shutter-control.json +457 -0
  35. package/examples/shutter-control.png +0 -0
  36. package/examples/statistic.json +1112 -0
  37. package/examples/statistic.png +0 -0
  38. package/forwarder/forwarder.html +100 -0
  39. package/forwarder/forwarder.js +95 -0
  40. package/hysteresis/hysteresis.html +152 -0
  41. package/hysteresis/hysteresis.js +146 -0
  42. package/light-control/light-control.html +358 -0
  43. package/light-control/light-control.js +231 -0
  44. package/logic/logic.html +168 -0
  45. package/logic/logic.js +171 -0
  46. package/long-press-control/long-press-control.html +74 -0
  47. package/long-press-control/long-press-control.js +75 -0
  48. package/multi-press-control/multi-press-control.html +135 -0
  49. package/multi-press-control/multi-press-control.js +68 -0
  50. package/package.json +59 -0
  51. package/persistence.js +74 -0
  52. package/scene-control/scene-control.html +575 -0
  53. package/scene-control/scene-control.js +265 -0
  54. package/scheduler/scheduler.html +338 -0
  55. package/scheduler/scheduler.js +209 -0
  56. package/shutter-complex-control/shutter-complex-control.html +330 -0
  57. package/shutter-complex-control/shutter-complex-control.js +399 -0
  58. package/shutter-control/shutter-control.html +283 -0
  59. package/shutter-control/shutter-control.js +208 -0
  60. package/smart_helper.js +156 -0
  61. package/statistic/statistic.html +107 -0
  62. package/statistic/statistic.js +196 -0
@@ -0,0 +1,358 @@
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_light-control", {
136
+ category: "Smart Nodes",
137
+ paletteLabel: "Light control",
138
+ color: "#C882FF",
139
+ defaults: {
140
+ name: { value: "" },
141
+ icon: { value: "light" }, // light | outlet
142
+ max_time_on: { value: "0" },
143
+ max_time_on_unit: { value: "s" },
144
+ alarm_action: { value: 'NOTHING' }, // NOTHING | ON | OFF
145
+ links: { value: [], type: "smart_central-control[]" }
146
+ },
147
+ inputs: 1,
148
+ outputs: 1,
149
+ icon: function ()
150
+ {
151
+ if (this.icon == "outlet")
152
+ return "font-awesome/fa-plug";
153
+ return "font-awesome/fa-lightbulb-o";
154
+ },
155
+ label: function ()
156
+ {
157
+ return this.name || (this.icon == "outlet" ? "Outlet control" : "Light control");
158
+ },
159
+ oneditprepare: function ()
160
+ {
161
+ let node = this;
162
+ onEditPrepare(this, ["smart_central-control"]);
163
+ initTreeList(node, ["smart_central-control"]);
164
+
165
+ $("#node-input-icon")
166
+ .css("max-width", "10rem")
167
+ .typedInput({
168
+ types: [
169
+ {
170
+ default: "light",
171
+ options: [
172
+ { value: "light", label: "Licht" },
173
+ { value: "outlet", label: "Steckdose" }
174
+ ],
175
+ },
176
+ ],
177
+ });
178
+
179
+ $("#node-input-max_time_on")
180
+ .spinner({
181
+ min: 0,
182
+ change: function (event, ui)
183
+ {
184
+ var value = parseInt(this.value);
185
+ value = isNaN(value) ? 0 : value;
186
+ value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
187
+ // value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
188
+ if (value !== this.value) $(this).spinner("value", value);
189
+ },
190
+ }).css("max-width", "4rem");
191
+
192
+ $("#node-input-max_time_on_unit")
193
+ .css("max-width", "10rem")
194
+ .typedInput({
195
+ types: [
196
+ {
197
+ default: "s",
198
+ options: [
199
+ { value: "ms", label: "Millisekunden" },
200
+ { value: "s", label: "Sekunden" },
201
+ { value: "min", label: "Minuten" },
202
+ { value: "h", label: "Stunden" },
203
+ ],
204
+ },
205
+ ],
206
+ });
207
+
208
+ $("#node-input-alarm_action").typedInput({
209
+ types: [
210
+ {
211
+ default: "NOTHING",
212
+ options: [
213
+ { value: "NOTHING", label: "Keine Aktion" },
214
+ { value: "ON", label: "Einschalten" },
215
+ { value: "OFF", label: "Ausschalten" }
216
+ ],
217
+ },
218
+ ],
219
+ });
220
+ },
221
+ onadd: function ()
222
+ {
223
+ this.links = [];
224
+ },
225
+ oneditresize: resizeNodeList
226
+ });
227
+ })();
228
+ </script>
229
+
230
+ <script type="text/html" data-template-name="smart_light-control">
231
+ <div class="form-row">
232
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
233
+ <input type="text" id="node-input-name" placeholder="Name" />
234
+ </div>
235
+ <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" />
238
+ </div>
239
+ <div class="form-row">
240
+ <label for="node-input-max_time_on"><i class="fa fa-clock-o"></i> Zeit Ein</label>
241
+ <input id="node-input-max_time_on" value="0" />
242
+ <input id="node-input-max_time_on_unit" />
243
+ </div>
244
+ <div class="form-row">
245
+ <label for="node-input-alarm_action"><i class="fa fa-exclamation-triangle"></i> Alarm Aktion</label>
246
+ <input id="node-input-alarm_action"/>
247
+ </div>
248
+ <span><i class="fa fa-link"></i> Dieser Baustein wird von folgenden Zentralbausteinen gesteuert:</span>
249
+ <div class="form-row node-input-link-row node-input-link-rows"></div>
250
+ </script>
251
+
252
+ <script type="text/html" data-help-name="smart_light-control">
253
+ <p>
254
+ Diese Node steuert einen Ausgang. Dies kann ein Licht, eine Steckdose oder ähnliches sein.
255
+ Als Ausgang wird immer <code>msg.payload = true</code> oder <code>msg.payload = false</code> gesendet um den Ausgang ein-, bzw. auszuschalten.
256
+ </p>
257
+ <p>
258
+ <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/>
259
+ Diese Node verwendet nur den Teil <code>name</code>. <code>#</code> und <code>nummer</code> sind dabei optional.
260
+ </p>
261
+ <p>
262
+ Folgende topics werden akzeptiert:
263
+ <table>
264
+ <thead>
265
+ <tr>
266
+ <th>Topic</th>
267
+ <th>Beschreibung</th>
268
+ </tr>
269
+ </thead>
270
+ <tbody>
271
+ <tr>
272
+ <td><code>status</code></td>
273
+ <td>
274
+ Gibt über <code>msg.payload = true</code> oder <code>msg.payload = false</code> den aktuellen Status des Ausgangs an.<br/>
275
+ Bei einem Wechsel von ausgeschaltet nach eingeschaltet wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.
276
+ </td>
277
+ </tr>
278
+ <tr>
279
+ <td><code>on</code></td>
280
+ <td>Schaltet den Ausgang ein und startet die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.</td>
281
+ </tr>
282
+ <tr>
283
+ <td><code>off</code></td>
284
+ <td>Schaltet den Ausgang aus.</td>
285
+ </tr>
286
+ <tr>
287
+ <td><code>set</code></td>
288
+ <td>
289
+ Schaltet den Ausgang bei <code>msg.payload = true</code> ein und bei <code>msg.payload = false</code> aus.<br/>
290
+ Bei einem Wechsel von ausgeschaltet nach eingeschaltet wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.
291
+ </td>
292
+ </tr>
293
+ <tr>
294
+ <td><code>set_permanent</code></td>
295
+ <td>
296
+ Schaltet den Ausgang bei <code>msg.payload = true</code> dauerhaft ein und bei <code>msg.payload = false</code> aus.<br/>
297
+ Es wird dabei keine Zeitmessung gestartet.
298
+ </td>
299
+ </tr>
300
+ <tr>
301
+ <td><code>motion</code></td>
302
+ <td>
303
+ Schaltet den Ausgang bei <code>msg.payload = true</code> ein ohne eine Zeitmessung.<br/>
304
+ Bei <code>msg.payload = false</code> wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.<br/>
305
+ Ist keine Zeit angegeben oder hinterlegt, schaltet sich der Ausgang sofort aus.
306
+ </td>
307
+ </tr>
308
+ <tr>
309
+ <td><code>alarm</code></td>
310
+ <td>Setzt den aktuellen Alarmzustand auf den Wert von <code>msg.payload</code> und löst die entsprechende Aktion aus.</td>
311
+ </tr>
312
+ <tr>
313
+ <td><code>toggle</code> (default)</td>
314
+ <td>
315
+ Schaltet den Ausgang abwechselnd ein und aus.<br/>
316
+ Bei einem Wechsel von ausgeschaltet nach eingeschaltet wird die Zeitmessung für die hinterlegte, bzw. mitgesendete Zeit gestartet, sofern vorhanden.
317
+ </td>
318
+ </tr>
319
+ </tbody>
320
+ </table>
321
+ </p>
322
+ <p>
323
+ Diese Node hat eine einstellbare Maximallaufzeit, bevor der Ausgang automatisch wieder ausgeschalten wird.
324
+ Diese Zeitmessung wird wie in der Tabelle oben verwendet.
325
+ Die eingestellte Zeit kann gezielt überschrieben werden.
326
+ Beispiel: <code>msg = { "topic": "on", "time_on": 5000 }</code> oder <code>msg = { "topic": "on", "time_on": "5s" }</code><br/>
327
+ Diese Nachricht schaltet das Licht für 5000 Millisekunden / 5 Sekunden an und anschließend wieder aus.
328
+ Die nächste Nachricht ohne <code>time_on</code> Angabe verwendet wieder die voreingestellte Zeit.
329
+ Ist die Zeit auf 0 eingestellt, wird das Licht <b>nicht</b> automatisch ausgeschalten.<br/>
330
+ Als Einheit für die Zeit können folgende Werte verwendet werden:
331
+ <table>
332
+ <thead>
333
+ <tr>
334
+ <th>Einheit</th>
335
+ <th>Beschreibung</th>
336
+ </tr>
337
+ </thead>
338
+ <tbody>
339
+ <tr>
340
+ <td><code>ms</code> (default)</td>
341
+ <td>Millisekunden</td>
342
+ </tr>
343
+ <tr>
344
+ <td><code>s</code> oder <code>sec</code></td>
345
+ <td>Sekunden</td>
346
+ </tr>
347
+ <tr>
348
+ <td><code>m</code> oder <code>min</code></td>
349
+ <td>Mintun.</td>
350
+ </tr>
351
+ <tr>
352
+ <td><code>h</code></td>
353
+ <td>Stunden</td>
354
+ </tr>
355
+ </tbody>
356
+ </table>
357
+ </p>
358
+ </script>
@@ -0,0 +1,231 @@
1
+ module.exports = function (RED)
2
+ {
3
+ function LightControlNode(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_value: false,
14
+ }, smartContext.get(node.id));
15
+
16
+ // dynamic config
17
+ let max_time_on = helper.getTimeInMs(config.max_time_on, config.max_time_on_unit);
18
+ let alarm_action = config.alarm_action || "NOTHING";
19
+
20
+ // runtime values
21
+ let max_time_on_timeout = null;
22
+ let isPermanent = false;
23
+ let isMotion = false;
24
+ let current_timeout_ms = 0;
25
+ let alarm_active = false;
26
+
27
+ // central handling
28
+ var event = "node:" + node.id;
29
+ var handler = function (msg)
30
+ {
31
+ node.receive(msg);
32
+ }
33
+ RED.events.on(event, handler);
34
+
35
+
36
+ node.on("input", function (msg)
37
+ {
38
+ handleTopic(msg);
39
+
40
+ setStatus();
41
+ smartContext.set(node.id, nodeSettings);
42
+ });
43
+
44
+ node.on("close", function ()
45
+ {
46
+ stopAutoOff();
47
+ RED.events.off(event, handler);
48
+ });
49
+
50
+ let handleTopic = msg =>
51
+ {
52
+ let doRestartTimer = true;
53
+
54
+ switch (helper.getTopicName(msg.topic))
55
+ {
56
+ case "status":
57
+ // Make sure it is bool
58
+ msg.payload = !!msg.payload;
59
+ doRestartTimer = nodeSettings.last_value != msg.payload;
60
+
61
+ nodeSettings.last_value = msg.payload;
62
+ break;
63
+
64
+ case "off":
65
+ nodeSettings.last_value = false;
66
+ break;
67
+
68
+ case "on":
69
+ nodeSettings.last_value = true;
70
+ break;
71
+
72
+ case "set":
73
+ // Make sure it is bool
74
+ msg.payload = !!msg.payload;
75
+ nodeSettings.last_value = msg.payload;
76
+ break;
77
+
78
+ case "set_permanent":
79
+ // Make sure it is bool
80
+ msg.payload = !!msg.payload;
81
+ nodeSettings.last_value = msg.payload;
82
+ isPermanent = msg.payload;
83
+ break;
84
+
85
+ case "motion":
86
+ // Make sure it is bool
87
+ msg.payload = !!msg.payload;
88
+ isMotion = msg.payload;
89
+
90
+ if (msg.payload == false)
91
+ {
92
+ // It already was off, so don't turn on
93
+ if (nodeSettings.last_value == false)
94
+ return;
95
+
96
+ // If time is set to 0, then turn off immediately
97
+ if (helper.getTimeInMsFromString(msg.time_on ?? max_time_on) == 0)
98
+ nodeSettings.last_value = false;
99
+ }
100
+ else
101
+ {
102
+ nodeSettings.last_value = true;
103
+ }
104
+ break;
105
+
106
+ case "alarm":
107
+ // Make sure it is bool
108
+ msg.payload = !!msg.payload;
109
+ alarm_active = msg.payload;
110
+ break;
111
+
112
+ case "toggle":
113
+ default:
114
+ // If button is released, don't handle this message
115
+ if (msg.payload === false)
116
+ return;
117
+
118
+ nodeSettings.last_value = !nodeSettings.last_value;
119
+ break;
120
+ }
121
+
122
+ if (doRestartTimer)
123
+ stopAutoOff();
124
+
125
+ // Check alarm values
126
+ if (alarm_active)
127
+ {
128
+ isPermanent = false;
129
+
130
+ switch (alarm_action)
131
+ {
132
+ case "ON":
133
+ nodeSettings.last_value = true;
134
+ break;
135
+
136
+ default:
137
+ case "OFF":
138
+ nodeSettings.last_value = false;
139
+ break;
140
+ }
141
+ }
142
+
143
+ if (alarm_active || helper.getTopicName(msg.topic) != "status")
144
+ node.send({ payload: nodeSettings.last_value });
145
+
146
+ // Output is on, now
147
+ if (nodeSettings.last_value && doRestartTimer)
148
+ startAutoOffIfNeeded(helper.getTimeInMsFromString(msg.time_on ?? max_time_on));
149
+
150
+ notifyCentral(nodeSettings.last_value);
151
+ }
152
+
153
+ let startAutoOffIfNeeded = origTimeMs =>
154
+ {
155
+ // No timer when alarm is active
156
+ if (alarm_active)
157
+ return;
158
+
159
+ let timeMs = parseInt(origTimeMs);
160
+
161
+ if (isNaN(timeMs))
162
+ {
163
+ node.error("Invalid time_on value send: " + origTimeMs);
164
+ timeMs = max_time_on;
165
+ }
166
+
167
+ current_timeout_ms = timeMs;
168
+
169
+ // Stop if any timeout is set
170
+ stopAutoOff();
171
+
172
+ // 0 = Always on
173
+ if (timeMs <= 0 || isPermanent || isMotion || !nodeSettings.last_value)
174
+ return;
175
+
176
+ max_time_on_timeout = setTimeout(() =>
177
+ {
178
+ max_time_on_timeout = null;
179
+ nodeSettings.last_value = false;
180
+ node.send({ payload: false });
181
+ notifyCentral(false);
182
+
183
+ setStatus();
184
+ smartContext.set(node.id, nodeSettings);
185
+ }, timeMs);
186
+ };
187
+
188
+ let stopAutoOff = () =>
189
+ {
190
+ if (max_time_on_timeout != null)
191
+ {
192
+ clearTimeout(max_time_on_timeout);
193
+ max_time_on_timeout = null;
194
+ }
195
+ };
196
+
197
+ let setStatus = () =>
198
+ {
199
+ if (alarm_active)
200
+ {
201
+ node.status({ fill: "red", shape: "dot", text: "ALARM is active" });
202
+ }
203
+ else if (nodeSettings.last_value)
204
+ {
205
+ if (isPermanent || isMotion || current_timeout_ms <= 0)
206
+ node.status({ fill: "green", shape: "dot", text: "On" });
207
+ else if (max_time_on_timeout)
208
+ node.status({ fill: "yellow", shape: "ring", text: "Wait " + helper.formatMsToStatus(current_timeout_ms, "until") + " for auto off" });
209
+ }
210
+ else
211
+ {
212
+ node.status({ fill: "red", shape: "dot", text: "Off" });
213
+ }
214
+ }
215
+
216
+ let notifyCentral = state =>
217
+ {
218
+ if (!config.links)
219
+ return;
220
+
221
+ config.links.forEach(link =>
222
+ {
223
+ RED.events.emit("node:" + link, { source: node.id, state: state });
224
+ });
225
+ }
226
+
227
+ setStatus();
228
+ }
229
+
230
+ RED.nodes.registerType("smart_light-control", LightControlNode);
231
+ };