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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 BergenSoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Samuel Bergen
4
+
5
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ > of this software and associated documentation files (the "Software"), to deal
7
+ > in the Software without restriction, including without limitation the rights
8
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ > copies of the Software, and to permit persons to whom the Software is
10
+ > furnished to do so, subject to the following conditions:
11
+ >
12
+ > The above copyright notice and this permission notice shall be included in
13
+ > all copies or substantial portions of the Software.
14
+ >
15
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ > THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # General information
2
+
3
+ The smart nodes was created to control smart home devices like lights, power outlets shutter and some more.
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
+
6
+ Sometimes one source node should be connected to multiple smart nodes which requires different topics.
7
+ 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
+ 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.
9
+
10
+ This is usefull when one node provide information to e.g. a light control node, as well as to a logic node.
11
+
12
+ This file only describes the general function of the node. See the documentation shown in NodeRed to find out how to use them, or see the included example flows.
13
+ As I'm german, the internal documentation is only available in german. If you need another language, feel free to add localizations and adding more languages via a pull request.
14
+
15
+ # Nodes
16
+ ## 1. Light control
17
+ This node is able to control a light or a power outlet.
18
+
19
+ ### **Features:**
20
+ * Auto turn off the light after a fixed time.
21
+ * Auto turn off the light with a custom time which is part of the message object.
22
+ * Toggle light between on and off.
23
+ * Can be triggered by motion sensors.
24
+ * Has an alarm function to go to a specific state (on or off) when the alarm is activated.
25
+
26
+ ## 2. Shutter control
27
+ This node is able to control a shutter.
28
+ There is no support for slats and it is also not planned as I don't need them, but feel free to send a pull request.
29
+
30
+ ### **Features:**
31
+ * One button control which switch between `up`, `stop`, `down`, `stop`.
32
+ * One button control for each direction `up and stop` or `down and stop`.
33
+ * Send direct position the shutter should be.
34
+ * Stop shutter immediately.
35
+ * Has an alarm function to go to a specific state (Open or close) when the alarm is activated.
36
+
37
+ ## 3. Scene control
38
+ This node is tracking the state of multiple outputs and controls them by switching to multiple defined scenes.
39
+
40
+ ### **Features:**
41
+ * An input message could name multiple scenes that should be iterated one by one by receiving the same message multiple times
42
+
43
+ ## 4. Central control
44
+ This node can control multiple light/scene controls or shutter controls at the same time
45
+
46
+ ### **Features:**
47
+ * Take care that multiple light/scene controls are turned of if any of them is one before executing a toggle command. This is helpful to avoid turning one light on and the other off.
48
+ * The same is used for shutters with the up_stop or down_stop commands. A toggle is not supported, it will only be forwarded.
49
+
50
+ ## 5. Long press control
51
+ This control is used to detect a short or a long press.
52
+ The time can be configured in this control.
53
+
54
+ ### **Features:**
55
+ * Imediately send a message to the long press output when the time is reached. It is not waiting until the button is released.
56
+
57
+ ## 6. Multi press control
58
+ This control is used to detect multiple presses on a button.
59
+ The max wait time between presses can be configured in this control.
60
+ You can also choose 2-4 press detection.
61
+
62
+ ### **Features:**
63
+ * Imediately send a message to the last output when the max presses are reached. It is not waiting until the button is released.
64
+
65
+ ## 7. Hysteresis
66
+ This control is checking if the input value reachs a defined value until the upper message is send. When the lower level is reached, the lower masssage will be send
67
+
68
+ ### **Features:**
69
+ * The state can be saved between NodeRed restarts.
70
+ * The last message can automatically be sent 10 seconds after a deployment.
71
+
72
+ ## 8. Logic
73
+ This control can be used for AND, OR and XOR logics.
74
+
75
+ ### **Features:**
76
+ * All input values could be individual converted as well as the output messsage.
77
+ * A NOT logic is possible when selecting 1 input and convert the output.
78
+ * The setpoint and hysteresis value can be changed in runtime.
79
+ * The state can be saved between NodeRed restarts.
80
+ * The last message can automatically be sent 10 seconds after a deployment.
81
+
82
+ ## 9. Statistic
83
+ This control can be used for getting the following types of values: Minimum, Maximum, Sum, Difference, Absolute Value, Absolute Difference, Average and Running average.
84
+
85
+ ### **Features:**
86
+ * All input values could be individual converted as well as the output messsage.
87
+ * A NOT logic is possible when selecting 1 input and convert the output.
88
+ * The state can be saved between NodeRed restarts.
89
+ * The last message can automatically be sent 10 seconds after a deployment.
90
+
91
+ ## 10. Compare
92
+ This control can compare 2 values with the following operators: < <= == >= > !=
93
+
94
+ ### **Features:**
95
+ * Can compare also texts like js would do.
96
+ * The state can be saved between NodeRed restarts.
97
+ * The last message can automatically be sent 10 seconds after a deployment.
98
+
99
+ ## 11. Delay
100
+ This control can delay incomming messages.
101
+
102
+ ### **Features:**
103
+ * Different times for `msg.payload = true` and `msg.payload = false`.
104
+ * Delays can be changed in runtime.
105
+ * The state can be saved between NodeRed restarts.
106
+ * The last message can automatically be sent 10 seconds after a deployment.
107
+
108
+ ## 12. Forwarder
109
+ This control can control if an incomming message should be forwarded or not.
110
+
111
+ ### **Features:**
112
+ * The forwarding can be enabled or disabled in runtime.
113
+ * The state can be saved between NodeRed restarts.
114
+ * The last message can automatically be sent 10 seconds after a deployment.
115
+
116
+ ## 13. Scheduler
117
+ This control can send a defined message on defined times.
118
+
119
+ ### **Features:**
120
+ * The weekdays and the time can be selected as a trigger.
121
+ * Multiple trigger and messages can be defined in one node.
122
+ * The scheduler can be activated or deactivated in runtime.
123
+ * The state can be saved between NodeRed restarts.
124
+ * The last message can automatically be sent 10 seconds after a deployment.
125
+
126
+ # Create node package
127
+ run `node pack` to create a new *.tgz node module.
@@ -0,0 +1,328 @@
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
+
105
+ function initTreeList(node, targetTypes)
106
+ {
107
+ candidateNodesCount = 0;
108
+ for (const key in flowMap)
109
+ {
110
+ flowMap[key].children = [];
111
+ }
112
+
113
+ let candidateNodes = [];
114
+
115
+ targetTypes.forEach(function (targetType)
116
+ {
117
+ candidateNodes = candidateNodes.concat(RED.nodes.filterNodes({ type: targetType }));
118
+ });
119
+
120
+ // Add all nodes, matching the given types
121
+ candidateNodes.forEach(function (n)
122
+ {
123
+ if (flowMap[n.z])
124
+ {
125
+ const isChecked = node.links.indexOf(n.id) !== -1 || (n.links || []).indexOf(node.id) !== -1;
126
+ if (isChecked) node.oldLinks.push(n.id);
127
+
128
+ flowMap[n.z].children.push({
129
+ id: n.id,
130
+ node: n,
131
+ label: n.name || n.id,
132
+ selected: isChecked,
133
+ checkbox: true,
134
+ radio: false,
135
+ });
136
+ candidateNodesCount++;
137
+ }
138
+ });
139
+
140
+ // Sort nodes by name
141
+ for (const key in flowMap)
142
+ {
143
+ flowMap[key].children.sort((a, b) => a.label.localeCompare(b.label));
144
+ }
145
+
146
+ // filter empty flows
147
+ const flowsFiltered = flows.filter(function (f)
148
+ {
149
+ return f.children.length > 0;
150
+ });
151
+
152
+ // clear list and refill it
153
+ treeList.treeList("empty");
154
+ treeList.treeList("data", flowsFiltered);
155
+ }
156
+
157
+ function resizeNodeList()
158
+ {
159
+ // calculate new heigt if browser is beeing resized
160
+ var rows = $("#dialog-form>div:not(.node-input-link-row)");
161
+ var height = $("#dialog-form").height();
162
+ for (var i = 0; i < rows.length; i++)
163
+ {
164
+ height -= $(rows[i]).outerHeight(true);
165
+ }
166
+ var editorRow = $("#dialog-form>div.node-input-link-row");
167
+ height -= parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom"));
168
+ $(".node-input-link-row").css("height", height + "px");
169
+ }
170
+
171
+ function onEditSave(node)
172
+ {
173
+ var flows = treeList.treeList("data");
174
+ node.links = [];
175
+
176
+ // Set own links for selected nodes
177
+ flows.forEach(function (f)
178
+ {
179
+ f.children.forEach(function (n)
180
+ {
181
+ if (n.selected)
182
+ node.links.push(n.id);
183
+ });
184
+ });
185
+
186
+ node.oldLinks.sort();
187
+ node.links.sort();
188
+
189
+ // Mark new and old links
190
+ var nodeMap = {};
191
+ var length = Math.max(node.oldLinks.length, node.links.length);
192
+ for (var i = 0; i < length; i++)
193
+ {
194
+ if (i < node.oldLinks.length)
195
+ {
196
+ nodeMap[node.oldLinks[i]] = nodeMap[node.oldLinks[i]] || {};
197
+ nodeMap[node.oldLinks[i]].old = true;
198
+ }
199
+ if (i < node.links.length)
200
+ {
201
+ nodeMap[node.links[i]] = nodeMap[node.links[i]] || {};
202
+ nodeMap[node.links[i]].new = true;
203
+ }
204
+ }
205
+
206
+ // Remove own node from old linked nodes
207
+ // Add own node to new linked nodes
208
+ var n;
209
+ for (var id in nodeMap)
210
+ {
211
+ if (nodeMap.hasOwnProperty(id))
212
+ {
213
+ n = RED.nodes.node(id);
214
+ if (n)
215
+ {
216
+ // init links and remove duplicate links
217
+ if (!n.links)
218
+ n.links = [];
219
+ else
220
+ n.links = n.links.filter((value, index, array) => array.indexOf(value) === index);
221
+
222
+
223
+ if (nodeMap[id].old && !nodeMap[id].new)
224
+ {
225
+ // Removed id
226
+ i = n.links.indexOf(node.id);
227
+ if (i > -1) n.links.splice(i, 1);
228
+ }
229
+ else if (nodeMap[id].new)
230
+ {
231
+ // Added id
232
+ i = n.links.indexOf(node.id);
233
+ if (i === -1) n.links.push(node.id);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // This function is called, when copying and pasting central node
241
+ function onAdd()
242
+ {
243
+ for (var i = 0; i < this.links.length; i++)
244
+ {
245
+ var n = RED.nodes.node(this.links[i]);
246
+ if (n && n.links.indexOf(this.id) === -1)
247
+ {
248
+ n.links.push(this.id);
249
+ }
250
+ }
251
+ }
252
+
253
+ RED.nodes.registerType("smart_central-control", {
254
+ category: "Smart Nodes",
255
+ paletteLabel: "Central control",
256
+ color: "#6EE2D9",
257
+ defaults: {
258
+ name: { value: "" },
259
+ mode: { value: "shutter" }, // shutter | light
260
+ links: { value: [], type: "smart_shutter-complex-control[]" },
261
+ },
262
+ inputs: 1,
263
+ outputs: 0,
264
+ icon: function ()
265
+ {
266
+ if (this.mode === "shutter")
267
+ return "font-awesome/fa-align-justify";
268
+
269
+ return "font-awesome/fa-lightbulb-o";
270
+ },
271
+ label: function ()
272
+ {
273
+ return this.name || "Central control";
274
+ },
275
+ oneditprepare: function ()
276
+ {
277
+ let node = this;
278
+
279
+ onEditPrepare(this, ["smart_shutter-complex-control", "smart_shutter-control"]);
280
+ $("#node-input-mode").on("change", function ()
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"]);
284
+ });
285
+
286
+ if (!this.mode)
287
+ {
288
+ $("#node-input-mode").val("shutter").trigger("change");
289
+ }
290
+ },
291
+ oneditsave: function ()
292
+ {
293
+ onEditSave(this);
294
+ },
295
+ onadd: onAdd,
296
+ oneditresize: resizeNodeList,
297
+ });
298
+ })();
299
+ </script>
300
+
301
+ <script type="text/html" data-template-name="smart_central-control">
302
+ <div class="form-row">
303
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
304
+ <input type="text" id="node-input-name" />
305
+ </div>
306
+ <div class="form-row">
307
+ <label for="node-input-mode">Typ</label>
308
+ <select id="node-input-mode" style="width: 70%">
309
+ <option value="shutter" selected>Rollladen</option>
310
+ <option value="light">Licht/Steckdose</option>
311
+ </select>
312
+ </div>
313
+ <div class="node-input-link-rows" style="position:relative; height: 30px; text-align: right;"
314
+ ><div style="display:inline-block"><input type="text" id="node-input-link-target-filter" /></div
315
+ ></div>
316
+ <div class="form-row node-input-link-row node-input-link-rows"></div>
317
+ </script>
318
+
319
+ <script type="text/html" data-help-name="smart_central-control">
320
+ <p>
321
+ Der Zentralbaustein leitet einkommende Nachrichten an alle verlinkten Unterbausteine weiter.
322
+ Dabei beachtet der Baustein, die Funktion der Unterbausteine synchron zu halten.
323
+ </p>
324
+ <p>
325
+ D.h. Wird ein <code>toggle</code> an mehrere Lichtbausteine geschickt, so werden diese zuerst ausgeschaltet, falls mindestens eines eingeschaltet war.<br />
326
+ Rollladen werden ebenfalls erst gestoppt, falls einer gerade aktiv ist. Ein <code>toggle</code> an mehrere Rollladen wird derzeit nicht unterstützt.
327
+ </p>
328
+ </script>
@@ -0,0 +1,95 @@
1
+ module.exports = function (RED)
2
+ {
3
+ function CentralControlNode(config)
4
+ {
5
+ const node = this;
6
+ RED.nodes.createNode(node, config);
7
+
8
+ // runtime values
9
+ let states = new Map();
10
+
11
+ // listen to all events sended directly to this node
12
+ let event = "node:" + config.id;
13
+ let handler = function (msg)
14
+ {
15
+ if (typeof msg.source == "undefined" || typeof msg.state == "undefined")
16
+ {
17
+ console.warn("Unknown message received in smart_node central", msg);
18
+ return;
19
+ }
20
+
21
+ // Save feedback from linked node
22
+ states.set(msg.source, msg.state);
23
+
24
+ if (isAllOff())
25
+ node.status({ fill: "red", shape: "dot", text: "All off" });
26
+ else
27
+ node.status({ fill: "green", shape: "dot", text: "Something is on" });
28
+ }
29
+ RED.events.on(event, handler);
30
+
31
+ node.status({});
32
+
33
+ node.on("input", function (msg)
34
+ {
35
+ // copy message, so original message stays unchanged
36
+ let newMsg = Object.assign({}, msg);
37
+
38
+ // make connected nodes behavior homogenous
39
+ switch (config.mode)
40
+ {
41
+ case "shutter":
42
+ if (msg.topic == "up_stop")
43
+ {
44
+ if (isAllOff())
45
+ newMsg.topic = "up";
46
+ else
47
+ newMsg.topic = "stop";
48
+ }
49
+ else if (msg.topic == "down_stop")
50
+ {
51
+ if (isAllOff())
52
+ newMsg.topic = "down";
53
+ else
54
+ newMsg.topic = "stop";
55
+ }
56
+ break;
57
+
58
+ case "light":
59
+ if (msg.topic == "toggle")
60
+ {
61
+ if (isAllOff())
62
+ newMsg.topic = "on";
63
+ else
64
+ newMsg.topic = "off";
65
+ }
66
+ break;
67
+ }
68
+
69
+ // Send cloned message to all linked nodes
70
+ config.links.forEach(link =>
71
+ {
72
+ RED.events.emit("node:" + link, newMsg);
73
+ });
74
+ });
75
+
76
+ node.on("close", function ()
77
+ {
78
+ // Cleanup event listener
79
+ RED.events.removeListener(event, handler);
80
+ });
81
+
82
+ // This functions return true if all lights are off or all shutter are stopped
83
+ let isAllOff = () =>
84
+ {
85
+ for (let [key, state] of states)
86
+ {
87
+ if (state)
88
+ return false;
89
+ }
90
+
91
+ return true;
92
+ }
93
+ }
94
+ RED.nodes.registerType("smart_central-control", CentralControlNode);
95
+ };