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,265 @@
1
+ module.exports = function (RED)
2
+ {
3
+ function SceneControlNode(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_values: [], // light is on or off for a scene
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
+
19
+ // runtime values
20
+ let max_time_on_timeout = null;
21
+ let isPermanent = false;
22
+ let current_timeout_ms = 0;
23
+
24
+ // central handling
25
+ var event = "node:" + config.id;
26
+ var handler = function (msg)
27
+ {
28
+ node.receive(msg);
29
+ }
30
+ RED.events.on(event, handler);
31
+
32
+
33
+ if (nodeSettings.last_values.length != config.scenes.length)
34
+ {
35
+ // Per default expect that all outputs are off
36
+ nodeSettings.last_values = new Array(config.outputs).fill(false);
37
+ }
38
+
39
+ node.status({});
40
+
41
+ node.on("input", function (msg)
42
+ {
43
+ handleTopic(msg);
44
+
45
+ // At least one light is on, now
46
+ if (getCurrentScene() != 0)
47
+ startAutoOffIfNeeded(helper.getTimeInMsFromString(msg.time_on ?? max_time_on));
48
+
49
+ status();
50
+ smartContext.set(node.id, nodeSettings);
51
+ });
52
+
53
+ node.on("close", function ()
54
+ {
55
+ stopAutoOff();
56
+ RED.events.off(event, handler);
57
+ });
58
+
59
+ let handleTopic = msg =>
60
+ {
61
+ let currentScene = getCurrentScene();
62
+ let [realTopic, scenes] = helper.getTopicName(msg.topic).split("_");
63
+ let number = helper.getTopicNumber(msg.topic) - 1; // number should be used 0-based
64
+
65
+ switch (realTopic)
66
+ {
67
+ case "status":
68
+ // Make sure it is bool
69
+ msg.payload = !!msg.payload;
70
+ nodeSettings.last_values[number] = msg.payload;
71
+
72
+ notifyCentral();
73
+
74
+ // All off? Stop permanent
75
+ if (getCurrentScene() == 0)
76
+ isPermanent = false;
77
+
78
+ // never forward status message to next node
79
+ return;
80
+
81
+ case "off":
82
+ nodeSettings.last_values = new Array(config.outputs).fill(false);
83
+ break;
84
+
85
+ case "on":
86
+ nodeSettings.last_values = new Array(config.outputs).fill(true);
87
+ break;
88
+
89
+ case "set":
90
+ // Make sure it is bool
91
+ msg.payload = !!msg.payload;
92
+ nodeSettings.last_values = new Array(config.outputs).fill(msg.payload);
93
+
94
+ // This happens because of splitting by _ for scenes
95
+ if (scenes == "permanent")
96
+ isPermanent = msg.payload;
97
+ break;
98
+
99
+ case "scene":
100
+ // Skip if button is released;
101
+ if (msg.payload === false)
102
+ return;
103
+
104
+ if (typeof scenes === "undefined")
105
+ {
106
+ node.error("called topic=scene without scene(s) set. Try topic=scene_0,1");
107
+ return;
108
+ }
109
+
110
+ scenes = scenes.split(",").map(s => parseInt(s, 10));
111
+
112
+ if (scenes.length == 0)
113
+ {
114
+ node.error("called topic=scene without scene(s) set. Try topic=scene_0,1");
115
+ return;
116
+ }
117
+
118
+ let nextSceneIndex = scenes.indexOf(currentScene);
119
+ if (nextSceneIndex === -1 || nextSceneIndex == scenes.length - 1)
120
+ nextSceneIndex = scenes[0];
121
+ else
122
+ nextSceneIndex = scenes[nextSceneIndex + 1];
123
+
124
+ // To be able to toggle if only one scene is set
125
+ if (currentScene == nextSceneIndex)
126
+ nextSceneIndex = 0;
127
+
128
+ if (nextSceneIndex == 0)
129
+ {
130
+ nodeSettings.last_values = new Array(config.outputs).fill(false);
131
+ }
132
+ else
133
+ {
134
+ const scene = config.scenes[nextSceneIndex - 1]; // scene numbers are 1 based
135
+ const expectedOn = scene.outputs.split(",");
136
+ for (let o = 0; o < nodeSettings.last_values.length; o++)
137
+ {
138
+ const output = nodeSettings.last_values[o];
139
+ // Check if output has to be changed
140
+ if ((output && !expectedOn.includes("" + (o + 1))) || (!output && expectedOn.includes("" + (o + 1))))
141
+ {
142
+ nodeSettings.last_values[o] = !output;
143
+ }
144
+ }
145
+ }
146
+ break;
147
+
148
+ case "toggle":
149
+ // Skip if button is released;
150
+ if (msg.payload === false)
151
+ return;
152
+
153
+ nodeSettings.last_values = new Array(config.outputs).fill(currentScene == 0);
154
+ break;
155
+ }
156
+
157
+ stopAutoOff();
158
+
159
+ node.send(nodeSettings.last_values.map(val => { return { payload: val }; }));
160
+ notifyCentral();
161
+ }
162
+
163
+ let getCurrentScene = () =>
164
+ {
165
+ // All off ist scene 0
166
+ if (!nodeSettings.last_values.includes(true))
167
+ return 0;
168
+
169
+ for (let s = 0; s < config.scenes.length; s++)
170
+ {
171
+ const scene = config.scenes[s];
172
+ const expectedOn = scene.outputs.split(",");
173
+ let skipScene = false;
174
+
175
+ for (let o = 0; o < nodeSettings.last_values.length; o++)
176
+ {
177
+ const output = nodeSettings.last_values[o];
178
+ // Check if one condition fails
179
+ if ((output && !expectedOn.includes("" + (o + 1))) || (!output && expectedOn.includes("" + (o + 1))))
180
+ {
181
+ skipScene = true;
182
+ break;
183
+ }
184
+ }
185
+
186
+ if (skipScene)
187
+ continue;
188
+
189
+ return s + 1; // Scene number is 1 based
190
+ }
191
+
192
+ // Not a scene
193
+ return null;
194
+ }
195
+
196
+ let startAutoOffIfNeeded = origTimeMs =>
197
+ {
198
+ let timeMs = parseInt(origTimeMs, 10);
199
+
200
+ if (isNaN(timeMs))
201
+ {
202
+ node.error("Invalid time_on value send: " + origTimeMs);
203
+ timeMs = max_time_on;
204
+ }
205
+
206
+ current_timeout_ms = timeMs;
207
+
208
+ // Stop if any timeout is set
209
+ stopAutoOff();
210
+
211
+ // 0 = Always on or already off
212
+ if (timeMs <= 0 || isPermanent || getCurrentScene() == 0)
213
+ return;
214
+
215
+ max_time_on_timeout = setTimeout(() =>
216
+ {
217
+ nodeSettings.last_values = new Array(config.outputs).fill(false);
218
+ node.send(nodeSettings.last_values.map(val => { return { payload: val }; }));
219
+ notifyCentral();
220
+
221
+ status();
222
+ smartContext.set(node.id, nodeSettings);
223
+ }, timeMs);
224
+ }
225
+
226
+ let stopAutoOff = () =>
227
+ {
228
+ if (max_time_on_timeout != null)
229
+ {
230
+ clearTimeout(max_time_on_timeout);
231
+ max_time_on_timeout = null;
232
+ }
233
+ }
234
+
235
+ let status = () =>
236
+ {
237
+ let scene = getCurrentScene();
238
+ if (scene != 0)
239
+ {
240
+ if (isPermanent || current_timeout_ms <= 0)
241
+ node.status({ fill: "green", shape: "dot", text: "Scene " + scene + " active" });
242
+ else if (max_time_on_timeout)
243
+ node.status({ fill: "yellow", shape: "ring", text: "Scene " + scene + " active, wait " + helper.formatMsToStatus(current_timeout_ms, "until") + " for auto off" });
244
+ }
245
+ else
246
+ {
247
+ node.status({ fill: "red", shape: "dot", text: "Off" });
248
+ }
249
+ }
250
+
251
+ let notifyCentral = () =>
252
+ {
253
+ if (!config.links)
254
+ return;
255
+
256
+ let state = getCurrentScene() !== 0;
257
+
258
+ config.links.forEach(link =>
259
+ {
260
+ RED.events.emit("node:" + link, { source: node.id, state: state });
261
+ });
262
+ }
263
+ }
264
+ RED.nodes.registerType("smart_scene-control", SceneControlNode);
265
+ };
@@ -0,0 +1,338 @@
1
+ <script type="text/html" data-template-name="smart_scheduler">
2
+ <div class="form-row">
3
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
4
+ <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name" />
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-input-enabled"></label>
8
+ <input type="checkbox" id="node-input-enabled" style="width: 20px;" />
9
+ <label for="node-input-enabled" style="width: 200px;">Scheduler aktiviert</label>
10
+ </div>
11
+ <div class="form-row" style="margin-bottom: 2px;">
12
+ <p class="text-center"><i class="fa fa-list"></i>&nbsp;<strong>Schedules</strong></p>
13
+ </div>
14
+ <div class="form-row node-scheduler-schedules-row">
15
+ <ol id="node-scheduler-schedules"></ol>
16
+ </div>
17
+ </script>
18
+
19
+ <script type="text/html" data-template-scheduler-row="">
20
+ <div>
21
+ <div style="display: inline-block; width: 30px; margin-left: 10px;">
22
+ <i class="fa fa-calendar"></i>
23
+ </div>
24
+ <input class="days"/>
25
+ </div>
26
+ <div style="margin-top: 8px;">
27
+ <div style="display: inline-block; width: 30px; margin-left: 10px;">
28
+ <i class="fa fa-clock-o"></i>
29
+ </div>
30
+ <select class="node-input-scheduler-select-hours hour fill24" style="display: inline-block; width: 75px;">
31
+ <option value="-1">hour</option>
32
+ </select>
33
+ :
34
+ <select class="node-input-scheduler-select-minutes minute fill60" style="display: inline-block; width: 75px;">
35
+ <option value="-1">min</option>
36
+ </select>
37
+ :
38
+ <select class="node-input-scheduler-select-seconds second fill60" style="display: inline-block; width: 75px;">
39
+ <option value="-1">sec</option>
40
+ </select>
41
+ </div>
42
+ <div style="margin-top: 8px;">
43
+ <div style="display: inline-block; width: 30px; margin-left: 10px;">
44
+ <i class="fa fa-envelope"></i>
45
+ </div>
46
+ <input type="text" class="message"/>
47
+ </div>
48
+ </script>
49
+
50
+ <script type="text/javascript">
51
+ RED.nodes.registerType("smart_scheduler", {
52
+ category: "Smart Nodes",
53
+ paletteLabel: "Scheduler",
54
+ color: "#6EE2D9",
55
+ defaults: {
56
+ name: { value: "" },
57
+ enabled: { value: false },
58
+ schedules: {
59
+ value: [],
60
+ validate: function (v)
61
+ {
62
+ let schedules = [];
63
+
64
+ if (v.length < 1)
65
+ {
66
+ return true;
67
+ }
68
+
69
+ /** make a first pass and create extra schedules if off is before on
70
+ * this so we can calculate overlaps later on */
71
+ for (let i = 0; i < v.length; i++)
72
+ {
73
+ v[i].valid = true;
74
+
75
+ days = v[i].days;
76
+ hour = parseInt(v[i].hour, 10);
77
+ minute = parseInt(v[i].minute, 10);
78
+ second = parseInt(v[i].second, 10);
79
+ message = v[i].message;
80
+
81
+ schedules.push({
82
+ days: v[i].days,
83
+ hour: hour,
84
+ minute: minute,
85
+ second: second,
86
+ message: message,
87
+ n: i,
88
+ valid: true,
89
+ });
90
+ }
91
+
92
+ /** validate schedules */
93
+ for (let i = 0; i < schedules.length; i++)
94
+ {
95
+ let s = schedules[i];
96
+
97
+ /** hours should be 0..23 */
98
+ if (s.hour < 0 || s.hour > 23)
99
+ {
100
+ v[s.n].valid = false;
101
+ }
102
+
103
+ /** minutes and seconds should be 0..59 */
104
+ if (s.minute < 0 || s.second < 0 || s.minute > 59 || s.second > 59)
105
+ {
106
+ v[s.n].valid = false;
107
+ }
108
+ }
109
+
110
+ /** if any are not valid return false */
111
+ for (i = 0; i < v.length; i++)
112
+ {
113
+ if (v[i].valid === false)
114
+ return false;
115
+ }
116
+
117
+ return true;
118
+ },
119
+ },
120
+ },
121
+ inputs: 1,
122
+ outputs: 1,
123
+ icon: "font-awesome/fa-clock-o",
124
+ label: function ()
125
+ {
126
+ return this.name || "Scheduler";
127
+ },
128
+ oneditprepare: function ()
129
+ {
130
+ let node = this;
131
+
132
+ /**
133
+ * prepare schedule
134
+ */
135
+ let scheduleList = $("#node-scheduler-schedules");
136
+
137
+ scheduleList
138
+ .css("min-height", "147px")
139
+ .css("min-width", "350px")
140
+ .editableList({
141
+ sortable: true,
142
+ removable: true,
143
+ addItem: function (row, index, data)
144
+ {
145
+ schedule = data.schedule || false;
146
+
147
+ let template = $("script[data-template-scheduler-row]").html() || "";
148
+ $(row).append(template);
149
+
150
+ fillOptions(row, schedule);
151
+ scheduleResize();
152
+ },
153
+ removeItem: function (data)
154
+ {
155
+ scheduleResize();
156
+ },
157
+ });
158
+
159
+ /**
160
+ * pass all existing schedules to editableList or pass an empty one
161
+ */
162
+ if (node.schedules.length == 0)
163
+ {
164
+ scheduleList.editableList("addItem");
165
+ }
166
+ else
167
+ {
168
+ for (let i = 0; i < node.schedules.length; i++)
169
+ {
170
+ let schedule = node.schedules[i];
171
+ scheduleList.editableList("addItem", { schedule: schedule, i: i });
172
+ }
173
+ }
174
+
175
+ /**
176
+ * resize the editableList
177
+ */
178
+ function scheduleResize()
179
+ {
180
+ height = 40 + scheduleList.editableList("length") * 135;
181
+ scheduleList.editableList("height", height);
182
+ }
183
+
184
+ /**
185
+ * fill the controls with days/hours/minutes/seconds/message
186
+ * @param row
187
+ */
188
+ function fillOptions(row, schedule)
189
+ {
190
+ schedule = schedule ? schedule : {
191
+ days: "",
192
+ hour: -1,
193
+ minute: -1,
194
+ second: -1,
195
+ message: '{"topic": ""}'
196
+ };
197
+
198
+ // Fill days list
199
+ row.find(".days")
200
+ .typedInput({
201
+ types: [
202
+ {
203
+ default: "",
204
+ multiple: true,
205
+ options: [
206
+ { value: "1", label: "Montag" },
207
+ { value: "2", label: "Dienstag" },
208
+ { value: "3", label: "Mittwoch" },
209
+ { value: "4", label: "Donnerstag" },
210
+ { value: "5", label: "Freitag" },
211
+ { value: "6", label: "Samstag" },
212
+ { value: "0", label: "Sonntag" }
213
+ ],
214
+ },
215
+ ],
216
+ });
217
+
218
+ // Fill hours
219
+ let hours = row.find(".fill24");
220
+ for (let i = 0; i < 24; i++)
221
+ {
222
+ i = pad(i);
223
+ hours.append($("<option></option>").val(i).text(i));
224
+ }
225
+
226
+ // Fill minutes and seconds
227
+ let minsec = row.find(".fill60");
228
+ for (let i = 0; i < 60; i++)
229
+ {
230
+ i = pad(i);
231
+ minsec.append($("<option></option>").val(i).text(i));
232
+ }
233
+
234
+ row.find(".message").typedInput({
235
+ type: "json",
236
+ types: ["json"]
237
+ });
238
+
239
+ /**
240
+ * set the value of the select boxes
241
+ */
242
+ row.find(".days").typedInput("value", schedule.days);
243
+ row.find(".hour").val(schedule.hour);
244
+ row.find(".minute").val(schedule.minute);
245
+ row.find(".second").val(schedule.second);
246
+ row.find(".message").typedInput("value", schedule.message);
247
+
248
+ /** set error on the li */
249
+ if (schedule.valid === false)
250
+ {
251
+ row.closest("li").css("background-color", "#f9b1bf");
252
+ }
253
+
254
+ /** modify some css */
255
+ row.parent().find(".red-ui-editableList-item-handle").css("color", "black").css("left", "10px");
256
+ row.parent().find(".red-ui-editableList-item-remove").css("right", "10px");
257
+ }
258
+
259
+ /**
260
+ * prepend string with 0
261
+ * @param s
262
+ * @returns {string|*}
263
+ */
264
+ function pad(s)
265
+ {
266
+ s = s.toString();
267
+ if (s.length < 2)
268
+ {
269
+ s = "0".concat(s);
270
+ }
271
+ return s;
272
+ }
273
+ },
274
+
275
+ oneditsave: function ()
276
+ {
277
+ let node = this;
278
+ node.schedules = [];
279
+
280
+ let scheduleList = $("#node-scheduler-schedules");
281
+ let schedules = scheduleList.editableList("items");
282
+
283
+ schedules.each(function (i)
284
+ {
285
+ let days = this.find(".days").val();
286
+ let hour = this.find(".hour").val();
287
+ let minute = this.find(".minute").val();
288
+ let second = this.find(".second").val();
289
+ let message = this.find(".message").val();
290
+
291
+ let schedule = {
292
+ days: days,
293
+ hour: hour,
294
+ minute: minute,
295
+ second: second,
296
+ message: message,
297
+ };
298
+
299
+ /** if any of the inputs was modified, save it */
300
+ if (days.length > 0 || hour != -1 || minute != -1 || second != -1 || message != "{}")
301
+ {
302
+ node.schedules.push(schedule);
303
+ }
304
+ });
305
+ },
306
+ });
307
+ </script>
308
+
309
+ <script type="text/html" data-help-name="smart_scheduler">
310
+ <p>
311
+ Diese Node sendet zu den angegebenen Zeitpunkten die entsprechenden Nachrichten an den Ausgang.
312
+ <p>
313
+ Folgende topics werden akzeptiert:
314
+ <table>
315
+ <thead>
316
+ <tr>
317
+ <th>Topic</th>
318
+ <th>Beschreibung</th>
319
+ </tr>
320
+ </thead>
321
+ <tbody>
322
+ <tr>
323
+ <td><code>enable</code></td>
324
+ <td>Aktiviert den Scheduler.</td>
325
+ </tr>
326
+ <tr>
327
+ <td><code>disable</code></td>
328
+ <td>Deaktiviert den Scheduler.</td>
329
+ </tr>
330
+ <tr>
331
+ <td><code>set_state</code></td>
332
+ <td>Aktiviert den Scheduler, wenn <code>msg.payload = true</code> oder deaktiviert den Scheduler, wenn <code>msg.payload = false</code>.</td>
333
+ </tr>
334
+ </tbody>
335
+ </table>
336
+ </p>
337
+ </p>
338
+ </script>