smart-nodes 0.3.15 → 0.3.26

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.
@@ -0,0 +1,188 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("smart_mixing-valve", {
3
+ category: "Smart Nodes",
4
+ paletteLabel: "Mixing Valve",
5
+ color: "#3FADB5",
6
+ defaults: {
7
+ name: { value: "" },
8
+ enabled: { value: true },
9
+ setpoint: { value: 45 },
10
+ time_total: { value: 60 },
11
+ time_sampling: { value: 60 },
12
+ off_mode: { value: "NOTHING" }, // NOTHING | OPEN | CLOSE
13
+ valve_mode: { value: "HEATING" }, // HEATING | COOLING
14
+ },
15
+ inputs: 1,
16
+ outputs: 3,
17
+ outputLabels: ["Open", "Close", "Status Position"],
18
+ icon: "font-awesome/fa-arrows-h",
19
+ label: function ()
20
+ {
21
+ return this.name || "Mixing Valve";
22
+ },
23
+ oneditprepare: function ()
24
+ {
25
+ $("#node-input-setpoint")
26
+ .spinner({
27
+ min: -30,
28
+ max: 100,
29
+ change: function (event, ui)
30
+ {
31
+ var value = parseInt(this.value);
32
+ value = isNaN(value) ? 0 : value;
33
+ value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
34
+ value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
35
+ if (value !== this.value) $(this).spinner("value", value);
36
+ },
37
+ }).css("max-width", "4rem");
38
+
39
+ $("#node-input-time_total")
40
+ .spinner({
41
+ min: 0,
42
+ change: function (event, ui)
43
+ {
44
+ var value = parseInt(this.value);
45
+ value = isNaN(value) ? 0 : value;
46
+ value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
47
+ // value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
48
+ if (value !== this.value) $(this).spinner("value", value);
49
+ },
50
+ }).css("max-width", "4rem");
51
+
52
+ $("#node-input-time_sampling")
53
+ .spinner({
54
+ min: 0,
55
+ change: function (event, ui)
56
+ {
57
+ var value = parseInt(this.value);
58
+ value = isNaN(value) ? 0 : value;
59
+ value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
60
+ // value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
61
+ if (value !== this.value) $(this).spinner("value", value);
62
+ },
63
+ }).css("max-width", "4rem");
64
+
65
+ $("#node-input-off_mode").typedInput({
66
+ types: [
67
+ {
68
+ default: "NOTHING",
69
+ options: [
70
+ { value: "NOTHING", label: "Nichts" },
71
+ { value: "OPEN", label: "Öffnen (100%)" },
72
+ { value: "CLOSE", label: "Schließen (0%)" }
73
+ ],
74
+ },
75
+ ],
76
+ });
77
+
78
+ $("#node-input-valve_mode").typedInput({
79
+ types: [
80
+ {
81
+ default: "HEATING",
82
+ options: [
83
+ { value: "HEATING", label: "Heizen (normal)" },
84
+ { value: "COOLING", label: "Kühlen (invertiert)" }
85
+ ],
86
+ },
87
+ ],
88
+ });
89
+ },
90
+ });
91
+ </script>
92
+
93
+ <script type="text/html" data-template-name="smart_mixing-valve">
94
+ <div class="form-row">
95
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
96
+ <input type="text" id="node-input-name" placeholder="Name" />
97
+ </div>
98
+ <div class="form-row">
99
+ <input type="checkbox" id="node-input-enabled" style="width: 20px;" />
100
+ <span for="node-input-enabled" style="width: 200px;"> Aktiviert</span>
101
+ </div>
102
+ <div class="form-row">
103
+ <label for="node-input-setpoint"><i class="fa fa-sliders"></i> Sollwert</label>
104
+ <input id="node-input-setpoint" value="0" /> °C
105
+ </div>
106
+ <div class="form-row">
107
+ <label for="node-input-time_total"><i class="fa fa-clock-o"></i> Laufzeit</label>
108
+ <input id="node-input-time_total" value="0" /> s
109
+ </div>
110
+ <div class="form-row">
111
+ <label for="node-input-time_sampling"><i class="fa fa-clock-o"></i> Abtastzeit</label>
112
+ <input id="node-input-time_sampling" value="0" /> s
113
+ </div>
114
+ <div class="form-row">
115
+ <label for="node-input-off_mode"><i class="fa fa-power-off"></i> Aus-Modus</label>
116
+ <input id="node-input-off_mode" />
117
+ </div>
118
+ <div class="form-row">
119
+ <label for="node-input-valve_mode"><i class="fa fa-fire"></i> Modus</label>
120
+ <input id="node-input-valve_mode" />
121
+ </div>
122
+ </script>
123
+
124
+ <script type="text/html" data-help-name="smart_mixing-valve">
125
+ <p>
126
+ Diese Node steuert einen Heizungsmischer. Dieser kann sowohl fürs Heizen als auch fürs Kühlen verwendet werden.
127
+ Nach der eingestellten Abtastzeit wird geprüft, ob die Position des Mischers korrigiert werden muss.
128
+ </p>
129
+ <p>
130
+ Beim ersten Verwenden wird eine Kalibrierungsfahrt gestartet. D.h. der Mischer schließt für die eingestellte Laufzeit und befindet sich dann auf 0%.
131
+ Danach kann der Mischvorgang starten. Die zuletzt angefahrene Position wird persistent gespeichert, wodurch weitere Kalibrierungsfahrten in der Regel nicht mehr notwending sind.
132
+ </p>
133
+ <p>
134
+ <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/>
135
+ Diese Node verwendet nur den Teil <code>name</code>. <code>#</code> und <code>nummer</code> sind dabei optional.
136
+ </p>
137
+ <p>
138
+ Folgende topics werden akzeptiert:
139
+ <table>
140
+ <thead>
141
+ <tr>
142
+ <th>Topic</th>
143
+ <th>Beschreibung</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>
147
+ <tr>
148
+ <td><code>enable</code></td>
149
+ <td>Aktiviert die Mischeransteuerung.</td>
150
+ </tr>
151
+ <tr>
152
+ <td><code>disable</code></td>
153
+ <td>Deaktiviert die Mischeransteuerung und fährt die Position an, die als Aus-Modus festgelegt ist.</td>
154
+ </tr>
155
+ <tr>
156
+ <td><code>set_state</code></td>
157
+ <td>Aktiviert das Weiterleiten, wenn <code>msg.payload = true</code> oder deaktiviert das Weiterleiten, wenn <code>msg.payload = false</code>.</td>
158
+ </tr>
159
+ <tr>
160
+ <td><code>setpoint</code></td>
161
+ <td>Überschreibt den Sollwert mit <code>msg.payload</code> °C.</td>
162
+ </tr>
163
+ <tr>
164
+ <td><code>off_mode</code></td>
165
+ <td>
166
+ Überschreibt den Aus-Modus mit <code>msg.payload</code>.
167
+ Gültige Werte für <code>msg.payload</code> sind <code>"NOTHING"</code>, <code>"OPEN"</code> und <code>"CLOSE"</code>.
168
+ </td>
169
+ </tr>
170
+ <tr>
171
+ <td><code>valve_mode</code></td>
172
+ <td>
173
+ Überschreibt den Modus mit <code>msg.payload</code>.
174
+ Gültige Werte für <code>msg.payload</code> sind <code>"HEATING"</code> und <code>"COOLING"</code>.
175
+ </td>
176
+ </tr>
177
+ <tr>
178
+ <td><code>current_temperature</code></td>
179
+ <td>Setzt die aktuelle Temperatur auf <code>msg.payload</code> °C.</td>
180
+ </tr>
181
+ <tr>
182
+ <td><code>calibrate</code></td>
183
+ <td>Erzwingt eine Kalibrierungsfahrt.</td>
184
+ </tr>
185
+ </tbody>
186
+ </table>
187
+ </p>
188
+ </script>
@@ -0,0 +1,357 @@
1
+ module.exports = function (RED)
2
+ {
3
+ "use strict";
4
+
5
+ function MixingValveNode(config)
6
+ {
7
+ const node = this;
8
+ RED.nodes.createNode(node, config);
9
+
10
+ const smart_context = require("../persistence.js")(RED);
11
+ const helper = require("../smart_helper.js");
12
+
13
+ // persistent values
14
+ var node_settings = Object.assign({}, {
15
+ enabled: config.enabled,
16
+ setpoint: config.setpoint,
17
+ off_mode: config.off_mode,
18
+ valve_mode: config.valve_mode,
19
+ last_position: null
20
+ }, smart_context.get(node.id));
21
+
22
+ // dynamic config
23
+ let time_total = config.time_total;
24
+ let time_sampling = config.time_sampling;
25
+
26
+ // runtime values
27
+ let sampling_interval = null;
28
+ let calibration_timeout = null;
29
+ let changing_timeout = null;
30
+ let changing_open = null;
31
+ let current_temperature = null;
32
+ let changing_start_time = null;
33
+
34
+ node.on("input", function (msg)
35
+ {
36
+ handleTopic(msg);
37
+ });
38
+
39
+ node.on("close", function ()
40
+ {
41
+ stopSampling();
42
+
43
+ if (sampling_interval !== null)
44
+ clearInterval(sampling_interval);
45
+
46
+ if (calibration_timeout !== null)
47
+ clearTimeout(calibration_timeout);
48
+
49
+ if (changing_timeout !== null)
50
+ clearTimeout(changing_timeout);
51
+
52
+ stopChanging();
53
+ });
54
+
55
+
56
+ let handleTopic = msg =>
57
+ {
58
+ let real_topic = helper.getTopicName(msg.topic);
59
+ switch (real_topic)
60
+ {
61
+ case "enable":
62
+ node_settings.enabled = true;
63
+ smart_context.set(node.id, node_settings);
64
+
65
+ stopChanging();
66
+ startSampling();
67
+ break;
68
+
69
+ case "disable":
70
+ node_settings.enabled = false;
71
+ smart_context.set(node.id, node_settings);
72
+
73
+ stopSampling();
74
+ doOffMode();
75
+ break;
76
+
77
+ case "set_state":
78
+ node_settings.enabled = !!msg.payload; // force boolean
79
+ smart_context.set(node.id, node_settings);
80
+
81
+ if (node_settings.enabled)
82
+ {
83
+ startSampling();
84
+ }
85
+ else
86
+ {
87
+ stopSampling();
88
+ doOffMode();
89
+ }
90
+ break;
91
+
92
+ case "setpoint":
93
+ let new_setpoint = parseFloat(msg.payload);
94
+ if (isNaN(new_setpoint) && !isFinite(new_setpoint))
95
+ {
96
+ node.error("Invalid payload: " + msg.payload);
97
+ return;
98
+ }
99
+ node_settings.setpoint = new_setpoint;
100
+ smart_context.set(node.id, node_settings);
101
+ node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": New setpoint " + new_setpoint });
102
+ break;
103
+
104
+ case "off_mode":
105
+ switch (msg.payload)
106
+ {
107
+ case "NOTHING":
108
+ case "OPEN":
109
+ case "CLOSE":
110
+ node_settings.off_mode = msg.payload;
111
+ smart_context.set(node.id, node_settings);
112
+
113
+ if (!node_settings.enabled)
114
+ doOffMode();
115
+ break;
116
+ }
117
+ break;
118
+
119
+ case "valve_mode":
120
+ switch (msg.payload)
121
+ {
122
+ case "HEATING":
123
+ case "COOLING":
124
+ node_settings.valve_mode = msg.payload;
125
+ smart_context.set(node.id, node_settings);
126
+ setStatus();
127
+ break;
128
+ }
129
+ node_settings.enabled = true;
130
+ smart_context.set(node.id, node_settings);
131
+
132
+ startSampling();
133
+ break;
134
+
135
+ case "current_temperature":
136
+ let new_temp = parseFloat(msg.payload);
137
+ if (isNaN(new_temp) && !isFinite(new_temp))
138
+ {
139
+ node.error("Invalid payload: " + msg.payload);
140
+ return;
141
+ }
142
+ current_temperature = new_temp;
143
+ setStatus();
144
+ break;
145
+
146
+ case "calibrate":
147
+ calibrate();
148
+ break;
149
+
150
+ default:
151
+ node.error("Invalid topic: " + real_topic);
152
+ return;
153
+ }
154
+ }
155
+
156
+ let startSampling = () =>
157
+ {
158
+ // Wait for calibration first
159
+ if (node_settings.last_position === null || calibration_timeout !== null || !node_settings.enabled)
160
+ return;
161
+
162
+ // sampling is already running, so do nothing
163
+ if (sampling_interval !== null)
164
+ return;
165
+
166
+ // start sampling
167
+ sampling_interval = setInterval(sample, time_sampling * 1000);
168
+
169
+ setStatus();
170
+ }
171
+
172
+ let stopSampling = () =>
173
+ {
174
+ // Wait for calibration first
175
+ if (node_settings.last_position === null || calibration_timeout !== null)
176
+ return;
177
+
178
+ // sampling is not running, so do nothing
179
+ if (sampling_interval === null)
180
+ return;
181
+
182
+ // stop sampling
183
+ clearInterval(sampling_interval);
184
+ sampling_interval = null;
185
+
186
+ setStatus();
187
+ }
188
+
189
+ let sample = () =>
190
+ {
191
+ // No current temperature available or in calibration => no action
192
+ if (current_temperature === null || calibration_timeout !== null || !node_settings.enabled)
193
+ return;
194
+
195
+ // +/- 1°C => already good enough, do nothing
196
+ let temp_diff = Math.abs(current_temperature - node_settings.setpoint);
197
+ if (temp_diff < 1)
198
+ return;
199
+
200
+ // Calculate change time
201
+ // Change time in ms for 1%
202
+ let moving_time = time_total * 1000 / 100;
203
+ if (temp_diff > 5)
204
+ {
205
+ // 0 °C diff => 0% change
206
+ // 20 °C diff => 5% change
207
+ moving_time *= helper.scale(Math.min(temp_diff, 20), 0, 20, 0, 5);
208
+ }
209
+
210
+ // calculate direction
211
+ let do_open = false;
212
+ if (current_temperature < node_settings.setpoint)
213
+ {
214
+ if (node_settings.valve_mode == "HEATING")
215
+ do_open = true;
216
+ }
217
+ else
218
+ {
219
+ if (node_settings.valve_mode == "COOLING")
220
+ do_open = true;
221
+ }
222
+
223
+ // start moving
224
+ startChanging(do_open, moving_time);
225
+
226
+ setStatus();
227
+ }
228
+
229
+ let startChanging = (do_open, time_ms) =>
230
+ {
231
+ stopChanging();
232
+
233
+ // Already oppened/closed
234
+ if (do_open && node_settings.last_position == 100)
235
+ return;
236
+ if (!do_open && node_settings.last_position == 0)
237
+ return;
238
+
239
+ changing_start_time = Date.now();
240
+ if (do_open)
241
+ node.send([{ payload: true }, { payload: false }, null]);
242
+ else
243
+ node.send([{ payload: false }, { payload: true }, null]);
244
+
245
+ node.status({
246
+ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": Start " + (do_open ? "opening" : "closing") + " for " + helper.formatMsToStatus(time_ms)
247
+ });
248
+
249
+ changing_open = do_open;
250
+ changing_timeout = setTimeout(() =>
251
+ {
252
+ changing_timeout = null;
253
+ stopChanging();
254
+ }, time_ms);
255
+
256
+ }
257
+
258
+ let stopChanging = () =>
259
+ {
260
+ if (changing_timeout !== null)
261
+ {
262
+ clearTimeout(changing_timeout);
263
+ changing_timeout = null;
264
+ }
265
+
266
+ // No changing in progress
267
+ if (changing_start_time == null)
268
+ return;
269
+
270
+ // Calculate moved percentage
271
+ let time_passed = (Date.now() - changing_start_time) / 1000;
272
+ changing_start_time = null;
273
+
274
+ let changed_value = (time_passed / time_total) * 100; // calculate in % value (0-100)
275
+ if (changing_open)
276
+ node_settings.last_position += changed_value;
277
+ else
278
+ node_settings.last_position -= changed_value;
279
+
280
+ // Only values from 0 to 100 are allowed
281
+ node_settings.last_position = Math.min(Math.max(node_settings.last_position, 0), 100);
282
+
283
+ // Save state
284
+ smart_context.set(node.id, node_settings);
285
+
286
+ node.send([{ payload: false }, { payload: false }, { payload: node_settings.last_position.toFixed(1) }]);
287
+
288
+ setStatus();
289
+ }
290
+
291
+ let calibrate = () =>
292
+ {
293
+ // start closing
294
+ node.send([{ payload: false }, { payload: true }, null]);
295
+
296
+ calibration_timeout = setTimeout(() =>
297
+ {
298
+ // stop closing
299
+ node.send([{ payload: false }, { payload: false }, null]);
300
+ node_settings.last_position = 0;
301
+ smart_context.set(node.id, node_settings);
302
+
303
+ // Calibration finished, start sampling if enabled
304
+ calibration_timeout = null;
305
+ if (node_settings.enabled)
306
+ startSampling();
307
+ else
308
+ stopSampling();
309
+
310
+ setStatus();
311
+ }, time_total * 1000);
312
+
313
+ setStatus();
314
+ }
315
+
316
+ let doOffMode = () =>
317
+ {
318
+ switch (node_settings.off_mode)
319
+ {
320
+ case "OPEN":
321
+ startChanging(true, time_total * 1000);
322
+ break;
323
+
324
+ case "CLOSE":
325
+ startChanging(false, time_total * 1000);
326
+ break;
327
+
328
+ case "NOTHING":
329
+ default:
330
+ break;
331
+ }
332
+ }
333
+
334
+ let setStatus = () =>
335
+ {
336
+ if (calibration_timeout !== null)
337
+ node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": In calibration" });
338
+ else
339
+ node.status({ fill: node_settings.enabled ? "green" : "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": " + (node_settings.valve_mode == "HEATING" ? "🔥" : "❄️") + " Set: " + node_settings.setpoint.toFixed(1) + "°C, Cur: " + current_temperature?.toFixed(1) + "°C, Pos: " + node_settings.last_position.toFixed(1) + "%" });
340
+ }
341
+
342
+ if (node_settings.last_position === null)
343
+ {
344
+ // Start calibration after 10s
345
+ setTimeout(calibrate, 10 * 1000);
346
+ }
347
+ else if (node_settings.enabled)
348
+ {
349
+ startSampling();
350
+ node.send([null, null, { payload: node_settings.last_position.toFixed(1) }]);
351
+ }
352
+
353
+ setStatus();
354
+ }
355
+
356
+ RED.nodes.registerType("smart_mixing-valve", MixingValveNode);
357
+ }
@@ -1,5 +1,7 @@
1
1
  module.exports = function (RED)
2
2
  {
3
+ "use strict";
4
+
3
5
  function MultiPressControlNode(config)
4
6
  {
5
7
  const node = this;
@@ -55,7 +57,7 @@ module.exports = function (RED)
55
57
 
56
58
  let sendResult = () =>
57
59
  {
58
- node.status({ fill: "green", shape: "dot", text: (new Date()).toLocaleString() + ": Last was press " + count + " time" + (count == 1 ? "" : "s") });
60
+ node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Last was press " + count + " time" + (count == 1 ? "" : "s") });
59
61
  let data = Array.from({ length: config.outputs }).fill(null);
60
62
  data[count - 1] = outs[count - 1];
61
63
  node.send(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-nodes",
3
- "version": "0.3.15",
3
+ "version": "0.3.26",
4
4
  "description": "Smart Nodes",
5
5
  "keywords": [
6
6
  "node-red",
@@ -18,8 +18,12 @@
18
18
  "compare",
19
19
  "comparator",
20
20
  "statistic",
21
+ "counter",
21
22
  "scheduler",
22
23
  "central",
24
+ "heating",
25
+ "mixing valve",
26
+ "heating curve",
23
27
  "text execution"
24
28
  ],
25
29
  "scripts": {
@@ -41,9 +45,12 @@
41
45
  "compare": "compare/compare.js",
42
46
  "hysteresis": "hysteresis/hysteresis.js",
43
47
  "statistic": "statistic/statistic.js",
48
+ "counter": "counter/counter.js",
44
49
  "scheduler": "scheduler/scheduler.js",
45
50
  "delay": "delay/delay.js",
46
51
  "central": "central/central.js",
52
+ "mixing-valve": "mixing-valve/mixing-valve.js",
53
+ "heating-curve": "heating-curve/heating-curve.js",
47
54
  "text-exec": "text-exec/text-exec.js"
48
55
  }
49
56
  },