smart-nodes 0.3.22 → 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.
@@ -1,33 +1,35 @@
1
1
  module.exports = function (RED)
2
2
  {
3
+ "use strict";
4
+
3
5
  function ForwarderNode(config)
4
6
  {
5
7
  const node = this;
6
8
  RED.nodes.createNode(node, config);
7
9
 
8
- const smartContext = require("../persistence.js")(RED);
10
+ const smart_context = require("../persistence.js")(RED);
9
11
  const helper = require("../smart_helper.js");
10
12
 
11
- var nodeSettings = {
13
+ var node_settings = {
12
14
  enabled: config.enabled,
13
- lastMessage: null,
15
+ last_message: null,
14
16
  last_msg_was_sended: true
15
17
  };
16
18
 
17
19
  if (config.save_state)
18
20
  {
19
21
  // load old saved values
20
- nodeSettings = Object.assign(nodeSettings, smartContext.get(node.id));
22
+ node_settings = Object.assign(node_settings, smart_context.get(node.id));
21
23
  }
22
24
  else
23
25
  {
24
26
  // delete old saved values
25
- smartContext.del(node.id);
27
+ smart_context.del(node.id);
26
28
  }
27
29
 
28
30
  // dynamic config
29
- let forwardTrue = config.always_forward_true;
30
- let forwardFalse = config.always_forward_false;
31
+ let forward_true = config.always_forward_true;
32
+ let forward_false = config.always_forward_false;
31
33
  let forward_last_on_enable = config.forward_last_on_enable;
32
34
 
33
35
  // runtime values
@@ -35,29 +37,29 @@ module.exports = function (RED)
35
37
 
36
38
  node.on("input", function (msg)
37
39
  {
38
- let newState = null;
40
+ let new_state = null;
39
41
  if (msg.topic == "enable" || (msg.topic == "set_state" && msg.payload))
40
- newState = true;
42
+ new_state = true;
41
43
  else if (msg.topic == "disable" || (msg.topic == "set_state" && !msg.payload))
42
- newState = false;
44
+ new_state = false;
43
45
 
44
46
  // Already the correct state
45
- if (newState != null && nodeSettings.enabled == newState)
47
+ if (new_state != null && node_settings.enabled == new_state)
46
48
  return;
47
49
 
48
- switch (newState)
50
+ switch (new_state)
49
51
  {
50
52
  case true:
51
53
  case false:
52
- nodeSettings.enabled = newState;
54
+ node_settings.enabled = new_state;
53
55
 
54
56
  if (config.save_state)
55
- smartContext.set(node.id, nodeSettings);
57
+ smart_context.set(node.id, node_settings);
56
58
 
57
- if (nodeSettings.enabled && forward_last_on_enable && nodeSettings.lastMessage != null && !nodeSettings.last_msg_was_sended)
59
+ if (node_settings.enabled && forward_last_on_enable && node_settings.last_message != null && !node_settings.last_msg_was_sended)
58
60
  {
59
- node.send(nodeSettings.lastMessage);
60
- nodeSettings.last_msg_was_sended = true;
61
+ node.send(node_settings.last_message);
62
+ node_settings.last_msg_was_sended = true;
61
63
  }
62
64
 
63
65
  setStatus();
@@ -65,34 +67,34 @@ module.exports = function (RED)
65
67
 
66
68
  default:
67
69
  // Forward if enabled or forced
68
- if (nodeSettings.enabled || (forwardTrue && msg.payload) || (forwardFalse && !msg.payload))
70
+ if (node_settings.enabled || (forward_true && msg.payload) || (forward_false && !msg.payload))
69
71
  {
70
72
  node.send(msg);
71
- nodeSettings.last_msg_was_sended = true;
73
+ node_settings.last_msg_was_sended = true;
72
74
  }
73
75
  else
74
76
  {
75
- nodeSettings.last_msg_was_sended = false;
77
+ node_settings.last_msg_was_sended = false;
76
78
  }
77
79
 
78
- nodeSettings.lastMessage = msg;
80
+ node_settings.last_message = msg;
79
81
  break;
80
82
  }
81
83
  });
82
84
 
83
85
  let setStatus = () =>
84
86
  {
85
- if (nodeSettings.enabled)
86
- node.status({ fill: "green", shape: "dot", text: (new Date()).toLocaleString() + ": Forwarding enabled" });
87
+ if (node_settings.enabled)
88
+ node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Forwarding enabled" });
87
89
  else
88
- node.status({ fill: "red", shape: "dot", text: (new Date()).toLocaleString() + ": Forwarding disabled" });
90
+ node.status({ fill: "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Forwarding disabled" });
89
91
  }
90
92
 
91
- if (config.save_state && config.resend_on_start && nodeSettings.lastMessage != null)
93
+ if (config.save_state && config.resend_on_start && node_settings.last_message != null)
92
94
  {
93
95
  setTimeout(() =>
94
96
  {
95
- node.send(nodeSettings.lastMessage);
97
+ node.send(node_settings.last_message);
96
98
  }, 10000);
97
99
  }
98
100
 
@@ -0,0 +1,348 @@
1
+ <script type="text/javascript">
2
+ (function ()
3
+ {
4
+ let node;
5
+
6
+ RED.nodes.registerType("smart_heating-curve", {
7
+ category: "Smart Nodes",
8
+ paletteLabel: "Heating Curve",
9
+ color: "#3FADB5",
10
+ defaults: {
11
+ name: { value: "" },
12
+ room_setpoint: { value: 20 },
13
+ slope: { value: 1.3 },
14
+ offset: { value: 0 },
15
+ flow_max: { value: 75 },
16
+ flow_min: { value: 20 },
17
+ },
18
+ inputs: 1,
19
+ outputs: 1,
20
+ icon: "font-awesome/fa-line-chart",
21
+ label: function ()
22
+ {
23
+ return this.name || "Heating Curve";
24
+ },
25
+ oneditprepare: function ()
26
+ {
27
+ node = this;
28
+
29
+ $("#node-input-room_setpoint")
30
+ .spinner({
31
+ min: 15,
32
+ max: 25,
33
+ step: 0.5,
34
+ change: function (event, ui)
35
+ {
36
+ var value = parseFloat(this.value);
37
+ value = isNaN(value) ? 0 : value;
38
+ value = Math.max(value, parseFloat($(this).attr("aria-valuemin")));
39
+ value = Math.min(value, parseFloat($(this).attr("aria-valuemax")));
40
+ if (value !== this.value) $(this).spinner("value", value);
41
+ drawCanvas();
42
+ },
43
+ }).css("max-width", "4rem");
44
+
45
+ $("#node-input-slope")
46
+ .spinner({
47
+ min: 0.05,
48
+ max: 4,
49
+ step: 0.05,
50
+ change: function (event, ui)
51
+ {
52
+ var value = parseFloat(this.value);
53
+ value = isNaN(value) ? 0 : value;
54
+ value = Math.max(value, parseFloat($(this).attr("aria-valuemin")));
55
+ value = Math.min(value, parseFloat($(this).attr("aria-valuemax")));
56
+ if (value !== this.value) $(this).spinner("value", value);
57
+ drawCanvas();
58
+ },
59
+ }).css("max-width", "4rem");
60
+
61
+ $("#node-input-offset")
62
+ .spinner({
63
+ min: -20,
64
+ max: 20,
65
+ step: 0.5,
66
+ change: function (event, ui)
67
+ {
68
+ var value = parseFloat(this.value);
69
+ value = isNaN(value) ? 0 : value;
70
+ value = Math.max(value, parseFloat($(this).attr("aria-valuemin")));
71
+ value = Math.min(value, parseFloat($(this).attr("aria-valuemax")));
72
+ if (value !== this.value) $(this).spinner("value", value);
73
+ drawCanvas();
74
+ },
75
+ }).css("max-width", "4rem");
76
+
77
+ $("#node-input-flow_max")
78
+ .spinner({
79
+ min: 50,
80
+ max: 85,
81
+ change: function (event, ui)
82
+ {
83
+ var value = parseFloat(this.value);
84
+ value = isNaN(value) ? 0 : value;
85
+ value = Math.max(value, parseFloat($(this).attr("aria-valuemin")));
86
+ value = Math.min(value, parseFloat($(this).attr("aria-valuemax")));
87
+ if (value !== this.value) $(this).spinner("value", value);
88
+ drawCanvas();
89
+ },
90
+ }).css("max-width", "4rem");
91
+
92
+ $("#node-input-flow_min")
93
+ .spinner({
94
+ min: 5,
95
+ max: 30,
96
+ change: function (event, ui)
97
+ {
98
+ var value = parseFloat(this.value);
99
+ value = isNaN(value) ? 0 : value;
100
+ value = Math.max(value, parseFloat($(this).attr("aria-valuemin")));
101
+ value = Math.min(value, parseFloat($(this).attr("aria-valuemax")));
102
+ if (value !== this.value) $(this).spinner("value", value);
103
+ drawCanvas();
104
+ },
105
+ }).css("max-width", "4rem");
106
+
107
+ drawCanvas();
108
+ },
109
+ });
110
+
111
+ let drawCanvas = () =>
112
+ {
113
+ const canvas = $("#heating-curve-diagram")[0];
114
+ const ctx = canvas.getContext("2d");
115
+ ctx.reset();
116
+
117
+ drawAxis(canvas, ctx);
118
+ drawGrid(canvas, ctx);
119
+ drawCurve(canvas, ctx, "#808080", 0.5, 0);
120
+ drawCurve(canvas, ctx, "#808080", 1, 0);
121
+ drawCurve(canvas, ctx, "#808080", 1.5, 0);
122
+ drawCurve(canvas, ctx, "#808080", 2, 0);
123
+ drawCurve(canvas, ctx, "#808080", 2.5, 0);
124
+ drawCurve(canvas, ctx, "#808080", 3, 0);
125
+ drawCurve(canvas, ctx, "#808080", 3.5, 0);
126
+ drawCurve(canvas, ctx, "#808080", 4, 0);
127
+
128
+ drawCurve(canvas, ctx, "#0000FF", parseFloat($("#node-input-slope").val()), parseFloat($("#node-input-offset").val()), true);
129
+ }
130
+
131
+ let mapPoint = (out, flow) =>
132
+ {
133
+ const canvas = $("#heating-curve-diagram")[0];
134
+ return [scale(out, -20, 25, canvas.width, 0), scale(flow, 10, 100, canvas.height, 0)];
135
+ }
136
+
137
+ let scale = (number, inMin, inMax, outMin, outMax) =>
138
+ {
139
+ return (number - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
140
+ }
141
+
142
+ let drawAxis = (canvas, ctx) =>
143
+ {
144
+ const arrow_length = 10;
145
+
146
+ ctx.strokeStyle = "#000000";
147
+
148
+ ctx.beginPath();
149
+
150
+ // draw axis
151
+ ctx.moveTo(...mapPoint(20, 100));
152
+ ctx.lineTo(...mapPoint(20, 10));
153
+
154
+ ctx.moveTo(...mapPoint(25, 20));
155
+ ctx.lineTo(...mapPoint(-20, 20));
156
+
157
+ // draw arrow up
158
+ let point = mapPoint(20, 100);
159
+ ctx.moveTo(...point);
160
+ ctx.lineTo(point[0] - arrow_length / 2, point[1] + arrow_length);
161
+ ctx.moveTo(...point);
162
+ ctx.lineTo(point[0] + arrow_length / 2, point[1] + arrow_length);
163
+
164
+ // draw arrow right
165
+ point = mapPoint(-20, 20);
166
+ ctx.moveTo(...point);
167
+ ctx.lineTo(point[0] - arrow_length, point[1] - arrow_length / 2);
168
+ ctx.moveTo(...point);
169
+ ctx.lineTo(point[0] - arrow_length, point[1] + arrow_length / 2);
170
+
171
+ ctx.stroke();
172
+ }
173
+
174
+ let drawGrid = (canvas, ctx) =>
175
+ {
176
+ ctx.fillStyle = "#000000";
177
+ ctx.font = "15px Arial";
178
+ ctx.strokeStyle = "#CCCCCC";
179
+
180
+ ctx.beginPath();
181
+
182
+ // horizontal lines
183
+ ctx.textAlign = "right";
184
+ ctx.textBaseline = "middle";
185
+ for (let i = 30; i <= 90; i += 10)
186
+ {
187
+ let [x, y] = mapPoint(20, i);
188
+
189
+ ctx.moveTo(x - 15, y);
190
+ ctx.lineTo(canvas.width - 15, y);
191
+
192
+ ctx.fillText(i.toString(), 25, y);
193
+ }
194
+
195
+ // vertical lines
196
+ ctx.textAlign = "center";
197
+ for (let i = -15; i <= 15; i += 5)
198
+ {
199
+ let [x, y] = mapPoint(i, 20);
200
+
201
+ ctx.moveTo(x, 15);
202
+ ctx.lineTo(x, y + 15);
203
+
204
+ ctx.fillText(i.toString(), x, canvas.height - 10);
205
+ }
206
+
207
+ ctx.stroke();
208
+ }
209
+
210
+ let drawCurve = (canvas, ctx, color, slope, offset, useMinMax = false) =>
211
+ {
212
+ console.log("Test");
213
+ const room_setpoint = parseFloat($("#node-input-room_setpoint").val());
214
+ const flow_min = parseFloat($("#node-input-flow_min").val());
215
+ const flow_max = parseFloat($("#node-input-flow_max").val());
216
+
217
+ ctx.textAlign = "center";
218
+ ctx.textBaseline = "middle";
219
+ ctx.font = "12px Arial";
220
+ ctx.strokeStyle = color;
221
+
222
+ ctx.beginPath();
223
+ let first = true;
224
+
225
+ let labels = [];
226
+ debugger;
227
+
228
+ for (let out = 20; out >= -20; out -= 1)
229
+ {
230
+ let dar = out - room_setpoint;
231
+ let flow = room_setpoint + offset - slope * dar * (1.4347 + 0.021 * dar + 247.9 * Math.pow(10, -6) * Math.pow(dar, 2));
232
+
233
+ if (useMinMax)
234
+ flow = Math.min(Math.max(flow, flow_min), flow_max);
235
+
236
+ let point = mapPoint(out, flow);
237
+
238
+ if (first)
239
+ ctx.moveTo(...point);
240
+ else
241
+ ctx.lineTo(...point);
242
+ first = false;
243
+
244
+ if (point[0] >= canvas.width)
245
+ {
246
+ let x = canvas.width - 20;
247
+ let y = Math.max(point[1], 20);
248
+ labels.push({ x, y, text: slope.toString() });
249
+ break;
250
+ }
251
+ if (point[1] <= 0)
252
+ {
253
+ let x = Math.min(point[0] - 20, canvas.width - 20);
254
+ let y = 10;
255
+ labels.push({ x, y, text: slope.toString() });
256
+ break;
257
+ }
258
+ }
259
+ ctx.stroke();
260
+
261
+ ctx.beginPath();
262
+ for (let label of labels)
263
+ {
264
+ ctx.fillStyle = "#FFFFFF";
265
+
266
+ ctx.rect(label.x - 10.5, label.y - 8.5, 21, 16);
267
+ ctx.fillRect(label.x - 10, label.y - 8, 20, 15);
268
+
269
+ ctx.fillStyle = "#000000";
270
+ if (useMinMax)
271
+ ctx.fillStyle = color;
272
+
273
+ ctx.fillText(label.text, label.x, label.y);
274
+ }
275
+ ctx.stroke();
276
+ }
277
+ })();
278
+
279
+ </script>
280
+
281
+ <script type="text/html" data-template-name="smart_heating-curve">
282
+ <div class="form-row">
283
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
284
+ <input type="text" id="node-input-name" placeholder="Name" />
285
+ </div>
286
+ <div class="form-row">
287
+ <label for="node-input-room_setpoint"><i class="fa fa-thermometer-three-quarters"></i> Raum Soll</label>
288
+ <input id="node-input-room_setpoint" value="0" /> °C [15 - 25]
289
+ </div>
290
+ <div class="form-row">
291
+ <label for="node-input-slope"><i class="fa fa-expand"></i> Steilheit</label>
292
+ <input id="node-input-slope" value="0" /> [0,05 - 4]
293
+ </div>
294
+ <div class="form-row">
295
+ <label for="node-input-offset"><i class="fa fa-arrows-v"></i> Verschiebung</label>
296
+ <input id="node-input-offset" value="0" />
297
+ </div>
298
+ <div class="form-row">
299
+ <label for="node-input-flow_max"><i class="fa fa-arrow-up"></i> Max Soll</label>
300
+ <input id="node-input-flow_max" value="0" />
301
+ </div>
302
+ <div class="form-row">
303
+ <label for="node-input-flow_min"><i class="fa fa-arrow-down"></i> Min Soll</label>
304
+ <input id="node-input-flow_min" value="0" />
305
+ </div>
306
+ <div class="form-row">
307
+ <canvas id="heating-curve-diagram" width="450" height="300"></canvas>
308
+ </div>
309
+ </script>
310
+
311
+ <script type="text/html" data-help-name="smart_heating-curve">
312
+ <p>
313
+ Diese Node berechnet anhand den eingestellten Werten, die Vorlaufsolltemperatur. Diese kann verwendet werden um ein Mischventil anzusteuern.
314
+ </p>
315
+ <p>
316
+ <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/>
317
+ Diese Node verwendet nur den Teil <code>name</code>. <code>#</code> und <code>nummer</code> sind dabei optional.
318
+ </p>
319
+ <p>
320
+ Folgende topics werden akzeptiert:
321
+ <table>
322
+ <thead>
323
+ <tr>
324
+ <th>Topic</th>
325
+ <th>Beschreibung</th>
326
+ </tr>
327
+ </thead>
328
+ <tbody>
329
+ <tr>
330
+ <td><code>room_setpoint</code></td>
331
+ <td>Überschreibt die Raum-Solltemperatur.</td>
332
+ </tr>
333
+ <tr>
334
+ <td><code>flow_min</code></td>
335
+ <td>Überschreibt die minimale Vorlaufsolltemperatur.</td>
336
+ </tr>
337
+ <tr>
338
+ <td><code>flow_max</code></td>
339
+ <td>Überschreibt die maximale Vorlaufsolltemperatur.</td>
340
+ </tr>
341
+ <tr>
342
+ <td><code>temperature_outside</code></td>
343
+ <td>Setzt die aktuelle Außentemperatur.</td>
344
+ </tr>
345
+ </tbody>
346
+ </table>
347
+ </p>
348
+ </script>
@@ -0,0 +1,134 @@
1
+ module.exports = function (RED)
2
+ {
3
+ "use strict";
4
+
5
+ function HeatingCurveNode(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
+ room_setpoint: config.room_setpoint,
16
+ flow_min: config.flow_min,
17
+ flow_max: config.flow_max,
18
+ temperature_outside: 10,
19
+ last_flow_temperature: null
20
+ }, smart_context.get(node.id));
21
+
22
+ // dynamic config
23
+ let slope = config.slope;
24
+ let offset = config.offset;
25
+
26
+ // runtime values
27
+
28
+ node.on("input", function (msg)
29
+ {
30
+ handleTopic(msg);
31
+ sendResult();
32
+ setStatus();
33
+ });
34
+
35
+ node.on("close", function ()
36
+ {
37
+ });
38
+
39
+
40
+ let handleTopic = msg =>
41
+ {
42
+ let real_topic = helper.getTopicName(msg.topic);
43
+ switch (real_topic)
44
+ {
45
+ case "room_setpoint":
46
+ let new_setpoint = parseFloat(msg.payload);
47
+ if (isNaN(new_setpoint) && !isFinite(new_setpoint))
48
+ {
49
+ node.error("Invalid payload: " + msg.payload);
50
+ return;
51
+ }
52
+
53
+ node_settings.room_setpoint = msg.payload;
54
+ smart_context.set(node.id, node_settings);
55
+ break;
56
+
57
+ case "temperature_outside":
58
+ let new_temp = parseFloat(msg.payload);
59
+ if (isNaN(new_temp) && !isFinite(new_temp))
60
+ {
61
+ node.error("Invalid payload: " + msg.payload);
62
+ return;
63
+ }
64
+
65
+ node_settings.temperature_outside = msg.payload;
66
+ smart_context.set(node.id, node_settings);
67
+ break;
68
+
69
+ case "flow_min":
70
+ let new_flow_min = parseFloat(msg.payload);
71
+ if (isNaN(new_flow_min) && !isFinite(new_flow_min))
72
+ {
73
+ node.error("Invalid payload: " + msg.payload);
74
+ return;
75
+ }
76
+
77
+ node_settings.flow_min = msg.payload;
78
+ smart_context.set(node.id, node_settings);
79
+ break;
80
+
81
+ case "flow_max":
82
+ let new_flow_max = parseFloat(msg.payload);
83
+ if (isNaN(new_flow_max) && !isFinite(new_flow_max))
84
+ {
85
+ node.error("Invalid payload: " + msg.payload);
86
+ return;
87
+ }
88
+
89
+ node_settings.flow_max = msg.payload;
90
+ smart_context.set(node.id, node_settings);
91
+ break;
92
+
93
+ default:
94
+ node.error("Invalid topic: " + real_topic);
95
+ return;
96
+ }
97
+ }
98
+
99
+ let sendResult = () =>
100
+ {
101
+ // Formula used was found here:
102
+ // https://community.viessmann.de/t5/Gas/Mathematische-Formel-fuer-Vorlauftemperatur-aus-den-vier/td-p/68843
103
+
104
+ let dar = node_settings.temperature_outside - node_settings.room_setpoint;
105
+ node_settings.last_flow_temperature = node_settings.room_setpoint + offset - slope * dar * (1.4347 + 0.021 * dar + 247.9 * Math.pow(10, -6) * Math.pow(dar, 2));
106
+
107
+ // console.log({
108
+ // set: node_settings.room_setpoint,
109
+ // offset,
110
+ // slope,
111
+ // dar,
112
+ // flow: node_settings.last_flow_temperature
113
+ // });
114
+
115
+ // Check borders
116
+ node_settings.last_flow_temperature = Math.min(Math.max(node_settings.last_flow_temperature, node_settings.flow_min), node_settings.flow_max);
117
+ smart_context.set(node.id, node_settings);
118
+
119
+ node.send({ payload: node_settings.last_flow_temperature });
120
+ }
121
+
122
+ let setStatus = () =>
123
+ {
124
+ node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Out: " + node_settings.temperature_outside.toFixed(1) + "°C, Flow: " + node_settings.last_flow_temperature?.toFixed(1) + "°C" });
125
+ }
126
+
127
+ if (node_settings.last_flow_temperature !== null)
128
+ setTimeout(sendResult, 10 * 1000);
129
+
130
+ setStatus();
131
+ }
132
+
133
+ RED.nodes.registerType("smart_heating-curve", HeatingCurveNode);
134
+ }