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,209 @@
1
+ module.exports = function (RED)
2
+ {
3
+ function SchedulerNode(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
+ var nodeSettings = Object.assign({}, {
12
+ enabled: config.enabled,
13
+ }, smartContext.get(node.id));
14
+
15
+ let timeouts = [];
16
+ let nextEvents = [];
17
+
18
+ setTimeout(() =>
19
+ {
20
+ for (let i = 0; i < config.schedules.length; i++)
21
+ {
22
+ const schedule = config.schedules[i];
23
+ schedule.position = i + 1;
24
+ schedule.message = helper.evaluateNodeProperty(RED, schedule.message, "json");
25
+ schedule.days = schedule.days.split(",");
26
+ }
27
+
28
+ if (nodeSettings.enabled)
29
+ initTimeouts();
30
+
31
+ setStatus();
32
+ }, 1000);
33
+
34
+
35
+ node.on("input", function (msg)
36
+ {
37
+ switch (helper.getTopicName(msg.topic))
38
+ {
39
+ case "enable":
40
+ if (nodeSettings.enabled)
41
+ return;
42
+
43
+ nodeSettings.enabled = true;
44
+ break;
45
+
46
+ case "disable":
47
+ if (!nodeSettings.enabled)
48
+ return;
49
+
50
+ nodeSettings.enabled = false;
51
+ break;
52
+
53
+ case "set_state":
54
+ if (nodeSettings.enabled == !!msg.payload)
55
+ return;
56
+
57
+ nodeSettings.enabled = !!msg.payload;
58
+ break;
59
+
60
+ default:
61
+ return;
62
+ }
63
+
64
+ if (nodeSettings.enabled)
65
+ initTimeouts();
66
+ else
67
+ clearTimeouts();
68
+
69
+ setStatus();
70
+ smartContext.set(node.id, nodeSettings);
71
+ });
72
+
73
+ node.on("close", function ()
74
+ {
75
+ clearTimeouts();
76
+ });
77
+
78
+ let clearTimeouts = () =>
79
+ {
80
+ for (let i = 0; i < timeouts.length; i++)
81
+ {
82
+ if (timeouts[i] != 0)
83
+ {
84
+ clearTimeout(timeouts[i]);
85
+ timeouts[i] = 0;
86
+ }
87
+ }
88
+ }
89
+
90
+ let initTimeouts = () =>
91
+ {
92
+ for (let i = 0; i < config.schedules.length; i++)
93
+ {
94
+ initTimeout(i, config.schedules[i]);
95
+ }
96
+ }
97
+
98
+ let initTimeout = (i, schedule) =>
99
+ {
100
+ if (!nodeSettings.enabled)
101
+ return;
102
+
103
+ let waitTime = getWaitInMs(i, schedule);
104
+ if (waitTime != null)
105
+ {
106
+ if (timeouts[i])
107
+ clearTimeout(timeouts[i]);
108
+
109
+ timeouts[i] = setTimeout(raiseEvent, waitTime, i, schedule);
110
+ }
111
+ }
112
+
113
+ let raiseEvent = (i, schedule) =>
114
+ {
115
+ if (!nodeSettings.enabled)
116
+ return;
117
+
118
+ timeouts[i] = 0;
119
+ node.send(schedule.message);
120
+ initTimeout(i, schedule);
121
+ setStatus();
122
+ }
123
+
124
+ let getWaitInMs = (i, schedule) =>
125
+ {
126
+ // If no day is checked then we cannot it is never raised
127
+ if (!schedule.days || schedule.days.length == 0)
128
+ return null;
129
+
130
+ let now = new Date();
131
+ let findNextDay = false;
132
+
133
+ // check if the time has already passed today
134
+ if (now.getHours() > schedule.hour)
135
+ {
136
+ findNextDay = true;
137
+ }
138
+ else if (now.getHours() == schedule.hour)
139
+ {
140
+ if (now.getMinutes() > schedule.minute)
141
+ {
142
+ findNextDay = true;
143
+ }
144
+ else if (now.getMinutes() == schedule.minute)
145
+ {
146
+ findNextDay = now.getSeconds() >= schedule.second;
147
+ }
148
+ }
149
+
150
+ // find next day when the event should be raised
151
+ let possibleDays = schedule.days.filter(d => findNextDay ? d > now.getDay() : d >= now.getDay());
152
+ if (possibleDays.length == 0)
153
+ possibleDays = Math.min(...schedule.days);
154
+ else
155
+ possibleDays = Math.min(...possibleDays);
156
+
157
+ let nextEvent = new Date(
158
+ now.getFullYear(),
159
+ now.getMonth(),
160
+ now.getDate() + (
161
+ possibleDays < now.getDay() ? 7 - now.getDay() + possibleDays : possibleDays - now.getDay()
162
+ ),
163
+ schedule.hour,
164
+ schedule.minute,
165
+ schedule.second
166
+ );
167
+
168
+ nextEvents[i] = nextEvent;
169
+
170
+ return nextEvent.getTime() - now.getTime();
171
+ }
172
+
173
+ let setStatus = () =>
174
+ {
175
+ if (!nodeSettings.enabled)
176
+ {
177
+ node.status({
178
+ fill: "red",
179
+ shape: "dot",
180
+ text: "Scheduler disabled"
181
+ });
182
+ }
183
+ else if (nextEvents.filter(d => d).lenght == 0)
184
+ {
185
+ node.status({
186
+ fill: "red",
187
+ shape: "dot",
188
+ text: "No events planned"
189
+ });
190
+ }
191
+ else
192
+ {
193
+ // filter out empty values
194
+ let nextEvent = new Date(Math.min(...nextEvents.filter(d => d)));
195
+ let time = nextEvent.getTime() - (new Date()).getTime();
196
+ time = Math.ceil(time / 1000) * 1000;
197
+
198
+ node.status({
199
+ fill: "yellow",
200
+ shape: "dot",
201
+ text: "Wait " + helper.formatMsToStatus(time, "until") + " to raise next event"
202
+ });
203
+ }
204
+ }
205
+
206
+ }
207
+
208
+ RED.nodes.registerType("smart_scheduler", SchedulerNode);
209
+ }
@@ -0,0 +1,330 @@
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-complex-control", {
136
+ category: "Smart Nodes",
137
+ paletteLabel: "Shutter complex control",
138
+ color: "#C882FF",
139
+ defaults: {
140
+ name: { value: "" },
141
+ max_time: { value: 60 },
142
+ revert_time_ms: { value: 100 },
143
+ alarm_action: { value: 'NOTHING' }, // NOTHING | UP | DOWN
144
+ links: { value: [], type: "smart_central-control[]" }
145
+ },
146
+ inputs: 1,
147
+ outputs: 3,
148
+ outputLabels: ["Up", "Down", "Status Position"],
149
+ icon: "font-awesome/fa-align-justify",
150
+ label: function ()
151
+ {
152
+ return this.name || "Shutter complex control";
153
+ },
154
+ oneditprepare: function ()
155
+ {
156
+ let node = this;
157
+
158
+ onEditPrepare(this, ["smart_central-control"]);
159
+ initTreeList(node, ["smart_central-control"]);
160
+
161
+ $("#node-input-max_time").spinner({
162
+ min: 1,
163
+ change: function (event, ui)
164
+ {
165
+ var value = parseInt(this.value);
166
+ value = isNaN(value) ? 0 : value;
167
+ value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
168
+ // value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
169
+ if (value !== this.value)
170
+ $(this).spinner("value", value);
171
+ }
172
+ });
173
+
174
+ $("#node-input-revert_time_ms").spinner({
175
+ min: 1,
176
+ change: function (event, ui)
177
+ {
178
+ var value = parseInt(this.value);
179
+ value = isNaN(value) ? 0 : value;
180
+ value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
181
+ // value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
182
+ if (value !== this.value)
183
+ $(this).spinner("value", value);
184
+ }
185
+ });
186
+
187
+ $("#node-input-alarm_action").typedInput({
188
+ types: [
189
+ {
190
+ default: "NOTHING",
191
+ options: [
192
+ { value: "NOTHING", label: "Keine Aktion" },
193
+ { value: "UP", label: "Hoch / Öffnen" },
194
+ { value: "DOWN", label: "Runter / Schließen" }
195
+ ],
196
+ },
197
+ ],
198
+ });
199
+ },
200
+ onadd: function ()
201
+ {
202
+ this.links = [];
203
+ },
204
+ oneditresize: resizeNodeList
205
+ });
206
+ })();
207
+ </script>
208
+
209
+ <script type="text/html" data-template-name="smart_shutter-complex-control">
210
+ <div class="form-row">
211
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
212
+ <input type="text" id="node-input-name" placeholder="Name" />
213
+ </div>
214
+ <div class="form-row">
215
+ <label for="node-input-max_time"><i class="fa fa-clock-o"></i> Zeit [s]</label>
216
+ <input id="node-input-max_time" placeholder="Zeit für eine komplette Fahrt [s]" />
217
+ </div>
218
+ <div class="form-row">
219
+ <label for="node-input-revert_time_ms"><i class="fa fa-clock-o"></i> Pause Wechsel [ms]</label>
220
+ <input id="node-input-revert_time_ms" placeholder="Pause zwischen Richtungswechsel [ms]" />
221
+ </div>
222
+ <div class="form-row">
223
+ <label for="node-input-alarm_action"><i class="fa fa-exclamation-triangle"></i> Alarm Aktion</label>
224
+ <input id="node-input-alarm_action"/>
225
+ </div>
226
+ <span><i class="fa fa-link"></i> Dieser Baustein wird von folgenden Zentralbausteinen gesteuert:</span>
227
+ <div class="form-row node-input-link-row node-input-link-rows"></div>
228
+ </script>
229
+
230
+ <script type="text/html" data-help-name="smart_shutter-complex-control">
231
+ <p>
232
+ <b>Hinweis:</b> Diese Node wurde entwickelt, falls die KNX Rollladensteuerung nicht verwendet werden kann um man gezwungen ist 2 separate Ausgänge für Auf und Ab zu verwenden.
233
+ Es ist dabei sehr zu empfehlen, dass die Ausgänge sich gegenseitig verriegeln um ein gleichzeitiges Ansteuern zu verhindern. Für etwaige Schäden am Rollladen ist jeder selbst verantwortlich.
234
+ </p>
235
+ <p>
236
+ <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/>
237
+ Diese Node verwendet nur den Teil <code>name</code>. <code>#</code> und <code>nummer</code> sind dabei optional.
238
+ </p>
239
+ <p>
240
+ Diese Node steuert Rollladen oder Jalousien.
241
+ Es gibt 3 Ausgänge die angesteuert werden können:
242
+ <ol>
243
+ <li><b>Auf:</b> <code>msg.payload = true;</code> startet den Rollladen um nach oben zu fahren und <code>msg.payload = false;</code> stoppt diesen wieder.</li>
244
+ <li><b>Ab:</b> <code>msg.payload = true;</code> startet den Rollladen um nach unten zu fahren und <code>msg.payload = false;</code> stoppt diesen wieder.</li>
245
+ <li><b>Status Position:</b> <code>msg.payload = 42;</code>Gibt den aktuellen Status der Rollladen aus. 0 = offen, 100 = geschlossen.</li>
246
+ </ol>
247
+ Die Ausgänge sind den jeweiligen KNX Gruppenadressen zuzuordnen.
248
+ </p>
249
+ <p>
250
+ Diese Node erwartet folgende Topics als Eingang:<br/>
251
+ <table>
252
+ <thead>
253
+ <tr>
254
+ <th>Topic</th>
255
+ <th>Beschreibung</th>
256
+ </tr>
257
+ </thead>
258
+ <tbody>
259
+ <tr>
260
+ <td><code>up_stop</code></td>
261
+ <td>Sendet abwechselnd einen Stop- und einen Hochfahrbefehl.</td>
262
+ </tr>
263
+ <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>
266
+ </tr>
267
+ <tr>
268
+ <td><code>down_stop</code></td>
269
+ <td>Sendet abwechselnd einen Stop- und einen Runterfahrbefehl.</td>
270
+ </tr>
271
+ <tr>
272
+ <td><code>up_down</code></td>
273
+ <td>Nimmt einen Befehl von Home Assistant entgegen und leitet die entsprechende Aktion ein.</td>
274
+ </tr>
275
+ <tr>
276
+ <td><code>stop</code></td>
277
+ <td>Sendet einen Stopbefehl.</td>
278
+ </tr>
279
+ <tr>
280
+ <td><code>position</code></td>
281
+ <td>Sendet einen Positionsbefehl. <code>msg.payload</code> sollte ein Wert zwischen 0 (offen) und 100 (geschlossen) haben.</td>
282
+ </tr>
283
+ <tr>
284
+ <td><code>alarm</code></td>
285
+ <td>Setzt den aktuellen Alarmzustand.</td>
286
+ </tr>
287
+ <tr>
288
+ <td><code>toggle</code> (default)</td>
289
+ <td>Schaltet den Rollladen abwechselnd auf hoch, stop, runter, stop.</td>
290
+ </tr>
291
+ </tbody>
292
+ </table>
293
+ </p>
294
+ <p>
295
+ Diese Node verwaltet keine Laufzeit für den Rollladen selbst. Diese muss über ETS für den Ausgang konfiguriert werden.
296
+ Es ist jedoch möglich, den Rollladen nach einer definierten Zeit automatisch abzuschalten.
297
+ Beispiel: <code>msg = { "topic": "up", "time_on": 5000 }</code> oder <code>msg = { "topic": "up", "time_on": "5s" }</code><br/>
298
+ 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.
299
+ Als Einheit für die Zeit können folgende Werte verwendet werden:
300
+ <table>
301
+ <thead>
302
+ <tr>
303
+ <th>Einheit</th>
304
+ <th>Beschreibung</th>
305
+ </tr>
306
+ </thead>
307
+ <tbody>
308
+ <tr>
309
+ <td><code>ms</code> (default)</td>
310
+ <td>Millisekunden</td>
311
+ </tr>
312
+ <tr>
313
+ <td><code>s</code> oder <code>sec</code></td>
314
+ <td>Sekunden</td>
315
+ </tr>
316
+ <tr>
317
+ <td><code>m</code> oder <code>min</code></td>
318
+ <td>Mintun.</td>
319
+ </tr>
320
+ <tr>
321
+ <td><code>h</code></td>
322
+ <td>Stunden</td>
323
+ </tr>
324
+ </tbody>
325
+ </table>
326
+ </p>
327
+ <p>
328
+ Die Angabe einer Zeit funktioniert nicht mit dem topic <b>position</b>.
329
+ </p>
330
+ </script>