@bldgblocks/node-red-contrib-control 0.1.4

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 (98) hide show
  1. package/README.md +43 -0
  2. package/nodes/accumulate-block.html +71 -0
  3. package/nodes/accumulate-block.js +104 -0
  4. package/nodes/add-block.html +67 -0
  5. package/nodes/add-block.js +97 -0
  6. package/nodes/analog-switch-block.html +65 -0
  7. package/nodes/analog-switch-block.js +129 -0
  8. package/nodes/and-block.html +64 -0
  9. package/nodes/and-block.js +73 -0
  10. package/nodes/average-block.html +97 -0
  11. package/nodes/average-block.js +137 -0
  12. package/nodes/boolean-switch-block.html +59 -0
  13. package/nodes/boolean-switch-block.js +88 -0
  14. package/nodes/boolean-to-number-block.html +59 -0
  15. package/nodes/boolean-to-number-block.js +45 -0
  16. package/nodes/cache-block.html +69 -0
  17. package/nodes/cache-block.js +106 -0
  18. package/nodes/call-status-block.html +111 -0
  19. package/nodes/call-status-block.js +274 -0
  20. package/nodes/changeover-block.html +234 -0
  21. package/nodes/changeover-block.js +392 -0
  22. package/nodes/comment-block.html +83 -0
  23. package/nodes/comment-block.js +53 -0
  24. package/nodes/compare-block.html +64 -0
  25. package/nodes/compare-block.js +84 -0
  26. package/nodes/contextual-label-block.html +67 -0
  27. package/nodes/contextual-label-block.js +52 -0
  28. package/nodes/convert-block.html +179 -0
  29. package/nodes/convert-block.js +289 -0
  30. package/nodes/count-block.html +57 -0
  31. package/nodes/count-block.js +92 -0
  32. package/nodes/debounce-block.html +64 -0
  33. package/nodes/debounce-block.js +140 -0
  34. package/nodes/delay-block.html +104 -0
  35. package/nodes/delay-block.js +180 -0
  36. package/nodes/divide-block.html +65 -0
  37. package/nodes/divide-block.js +123 -0
  38. package/nodes/edge-block.html +71 -0
  39. package/nodes/edge-block.js +120 -0
  40. package/nodes/frequency-block.html +55 -0
  41. package/nodes/frequency-block.js +140 -0
  42. package/nodes/hysteresis-block.html +131 -0
  43. package/nodes/hysteresis-block.js +142 -0
  44. package/nodes/interpolate-block.html +74 -0
  45. package/nodes/interpolate-block.js +141 -0
  46. package/nodes/load-sequence-block.html +134 -0
  47. package/nodes/load-sequence-block.js +272 -0
  48. package/nodes/max-block.html +76 -0
  49. package/nodes/max-block.js +103 -0
  50. package/nodes/memory-block.html +90 -0
  51. package/nodes/memory-block.js +241 -0
  52. package/nodes/min-block.html +77 -0
  53. package/nodes/min-block.js +106 -0
  54. package/nodes/minmax-block.html +89 -0
  55. package/nodes/minmax-block.js +119 -0
  56. package/nodes/modulo-block.html +73 -0
  57. package/nodes/modulo-block.js +126 -0
  58. package/nodes/multiply-block.html +63 -0
  59. package/nodes/multiply-block.js +115 -0
  60. package/nodes/negate-block.html +55 -0
  61. package/nodes/negate-block.js +91 -0
  62. package/nodes/nullify-block.html +111 -0
  63. package/nodes/nullify-block.js +78 -0
  64. package/nodes/on-change-block.html +79 -0
  65. package/nodes/on-change-block.js +191 -0
  66. package/nodes/oneshot-block.html +96 -0
  67. package/nodes/oneshot-block.js +169 -0
  68. package/nodes/or-block.html +64 -0
  69. package/nodes/or-block.js +73 -0
  70. package/nodes/pid-block.html +205 -0
  71. package/nodes/pid-block.js +407 -0
  72. package/nodes/priority-block.html +66 -0
  73. package/nodes/priority-block.js +239 -0
  74. package/nodes/rate-limit-block.html +99 -0
  75. package/nodes/rate-limit-block.js +221 -0
  76. package/nodes/round-block.html +73 -0
  77. package/nodes/round-block.js +89 -0
  78. package/nodes/saw-tooth-wave-block.html +87 -0
  79. package/nodes/saw-tooth-wave-block.js +161 -0
  80. package/nodes/scale-range-block.html +90 -0
  81. package/nodes/scale-range-block.js +137 -0
  82. package/nodes/sine-wave-block.html +88 -0
  83. package/nodes/sine-wave-block.js +142 -0
  84. package/nodes/subtract-block.html +64 -0
  85. package/nodes/subtract-block.js +103 -0
  86. package/nodes/thermistor-block.html +81 -0
  87. package/nodes/thermistor-block.js +146 -0
  88. package/nodes/tick-tock-block.html +66 -0
  89. package/nodes/tick-tock-block.js +110 -0
  90. package/nodes/time-sequence-block.html +67 -0
  91. package/nodes/time-sequence-block.js +144 -0
  92. package/nodes/triangle-wave-block.html +86 -0
  93. package/nodes/triangle-wave-block.js +154 -0
  94. package/nodes/tstat-block.html +311 -0
  95. package/nodes/tstat-block.js +499 -0
  96. package/nodes/units-block.html +150 -0
  97. package/nodes/units-block.js +106 -0
  98. package/package.json +73 -0
@@ -0,0 +1,499 @@
1
+ module.exports = function(RED) {
2
+ function TstatBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Store typed-input properties
7
+ node.setpoint = config.setpoint;
8
+ node.setpointType = config.setpointType;
9
+ node.heatingSetpoint = config.heatingSetpoint;
10
+ node.heatingSetpointType = config.heatingSetpointType;
11
+ node.coolingSetpoint = config.coolingSetpoint;
12
+ node.coolingSetpointType = config.coolingSetpointType;
13
+ node.coolingOn = config.coolingOn;
14
+ node.coolingOnType = config.coolingOnType;
15
+ node.coolingOff = config.coolingOff;
16
+ node.coolingOffType = config.coolingOffType;
17
+ node.heatingOff = config.heatingOff;
18
+ node.heatingOffType = config.heatingOffType;
19
+ node.heatingOn = config.heatingOn;
20
+ node.heatingOnType = config.heatingOnType;
21
+ node.diff = config.diff;
22
+ node.diffType = config.diffType;
23
+ node.anticipator = config.anticipator;
24
+ node.anticipatorType = config.anticipatorType;
25
+ node.ignoreAnticipatorCycles = config.ignoreAnticipatorCycles;
26
+ node.ignoreAnticipatorCyclesType = config.ignoreAnticipatorCyclesType;
27
+ node.isHeating = config.isHeating;
28
+ node.algorithm = config.algorithm;
29
+ node.name = config.name;
30
+
31
+ let above = false;
32
+ let below = false;
33
+ let lastAbove = false;
34
+ let lastBelow = false;
35
+ let lastIsHeating = null;
36
+ let cyclesSinceModeChange = 0;
37
+ let modeChanged = false;
38
+
39
+ node.on("input", function(msg, send, done) {
40
+ send = send || function() { node.send.apply(node, arguments); };
41
+
42
+ if (!msg) {
43
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
44
+ if (done) done();
45
+ return;
46
+ }
47
+
48
+ if (msg.hasOwnProperty("context")) {
49
+ if (!msg.hasOwnProperty("payload")) {
50
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
51
+ if (done) done();
52
+ return;
53
+ }
54
+
55
+ if (msg.context === "status") {
56
+ const statusPayload = {
57
+ algorithm: node.algorithm,
58
+ diff: node.diff,
59
+ diffType: node.diffType,
60
+ anticipator: node.anticipator,
61
+ anticipatorType: node.anticipatorType,
62
+ ignoreAnticipatorCycles: node.ignoreAnticipatorCycles,
63
+ ignoreAnticipatorCyclesType: node.ignoreAnticipatorCyclesType,
64
+ isHeating: node.isHeating
65
+ };
66
+ if (node.algorithm === "single") {
67
+ statusPayload.setpoint = node.setpoint;
68
+ statusPayload.setpointType = node.setpointType;
69
+ } else if (node.algorithm === "split") {
70
+ statusPayload.heatingSetpoint = node.heatingSetpoint;
71
+ statusPayload.heatingSetpointType = node.heatingSetpointType;
72
+ statusPayload.coolingSetpoint = node.coolingSetpoint;
73
+ statusPayload.coolingSetpointType = node.coolingSetpointType;
74
+ } else {
75
+ statusPayload.coolingOn = node.coolingOn;
76
+ statusPayload.coolingOnType = node.coolingOnType;
77
+ statusPayload.coolingOff = node.coolingOff;
78
+ statusPayload.coolingOffType = node.coolingOffType;
79
+ statusPayload.heatingOff = node.heatingOff;
80
+ statusPayload.heatingOffType = node.heatingOffType;
81
+ statusPayload.heatingOn = node.heatingOn;
82
+ statusPayload.heatingOnType = node.heatingOnType;
83
+ }
84
+ send([null, null, { payload: statusPayload }]);
85
+ node.status({ fill: "blue", shape: "dot", text: "status requested" });
86
+ if (done) done();
87
+ return;
88
+ }
89
+
90
+ switch (msg.context) {
91
+ case "algorithm":
92
+ if (["single", "split", "specified"].includes(msg.payload)) {
93
+ node.algorithm = msg.payload;
94
+ node.status({
95
+ fill: "green",
96
+ shape: "dot",
97
+ text: `algorithm: ${msg.payload}`
98
+ });
99
+ } else {
100
+ node.status({ fill: "red", shape: "ring", text: "invalid algorithm" });
101
+ }
102
+ break;
103
+ case "setpoint":
104
+ if (node.algorithm !== "single") {
105
+ node.status({ fill: "red", shape: "ring", text: "setpoint not used in this algorithm" });
106
+ if (done) done();
107
+ return;
108
+ }
109
+ if (typeof msg.payload === 'number') {
110
+ node.setpoint = msg.payload;
111
+ node.setpointType = "num";
112
+ node.status({
113
+ fill: "green",
114
+ shape: "dot",
115
+ text: `setpoint: ${msg.payload.toFixed(2)}`
116
+ });
117
+ } else {
118
+ node.status({ fill: "red", shape: "ring", text: "invalid setpoint" });
119
+ }
120
+ break;
121
+ case "heatingSetpoint":
122
+ if (node.algorithm !== "split") {
123
+ node.status({ fill: "red", shape: "ring", text: "heatingSetpoint not used in this algorithm" });
124
+ if (done) done();
125
+ return;
126
+ }
127
+ if (typeof msg.payload === 'number') {
128
+ node.heatingSetpoint = msg.payload;
129
+ node.heatingSetpointType = "num";
130
+ node.status({
131
+ fill: "green",
132
+ shape: "dot",
133
+ text: `heatingSetpoint: ${msg.payload.toFixed(2)}`
134
+ });
135
+ } else {
136
+ node.status({ fill: "red", shape: "ring", text: "invalid heatingSetpoint" });
137
+ }
138
+ break;
139
+ case "coolingSetpoint":
140
+ if (node.algorithm !== "split") {
141
+ node.status({ fill: "red", shape: "ring", text: "coolingSetpoint not used in this algorithm" });
142
+ if (done) done();
143
+ return;
144
+ }
145
+ if (typeof msg.payload === 'number') {
146
+ node.coolingSetpoint = msg.payload;
147
+ node.coolingSetpointType = "num";
148
+ node.status({
149
+ fill: "green",
150
+ shape: "dot",
151
+ text: `coolingSetpoint: ${msg.payload.toFixed(2)}`
152
+ });
153
+ } else {
154
+ node.status({ fill: "red", shape: "ring", text: "invalid coolingSetpoint" });
155
+ }
156
+ break;
157
+ case "coolingOn":
158
+ if (node.algorithm !== "specified") {
159
+ node.status({ fill: "red", shape: "ring", text: "coolingOn not used in this algorithm" });
160
+ if (done) done();
161
+ return;
162
+ }
163
+ if (typeof msg.payload === 'number') {
164
+ node.coolingOn = msg.payload;
165
+ node.coolingOnType = "num";
166
+ node.status({
167
+ fill: "green",
168
+ shape: "dot",
169
+ text: `coolingOn: ${msg.payload.toFixed(2)}`
170
+ });
171
+ } else {
172
+ node.status({ fill: "red", shape: "ring", text: "invalid coolingOn" });
173
+ }
174
+ break;
175
+ case "coolingOff":
176
+ if (node.algorithm !== "specified") {
177
+ node.status({ fill: "red", shape: "ring", text: "coolingOff not used in this algorithm" });
178
+ if (done) done();
179
+ return;
180
+ }
181
+ if (typeof msg.payload === 'number') {
182
+ node.coolingOff = msg.payload;
183
+ node.coolingOffType = "num";
184
+ node.status({
185
+ fill: "green",
186
+ shape: "dot",
187
+ text: `coolingOff: ${msg.payload.toFixed(2)}`
188
+ });
189
+ } else {
190
+ node.status({ fill: "red", shape: "ring", text: "invalid coolingOff" });
191
+ }
192
+ break;
193
+ case "heatingOff":
194
+ if (node.algorithm !== "specified") {
195
+ node.status({ fill: "red", shape: "ring", text: "heatingOff not used in this algorithm" });
196
+ if (done) done();
197
+ return;
198
+ }
199
+ if (typeof msg.payload === 'number') {
200
+ node.heatingOff = msg.payload;
201
+ node.heatingOffType = "num";
202
+ node.status({
203
+ fill: "green",
204
+ shape: "dot",
205
+ text: `heatingOff: ${msg.payload.toFixed(2)}`
206
+ });
207
+ } else {
208
+ node.status({ fill: "red", shape: "ring", text: "invalid heatingOff" });
209
+ }
210
+ break;
211
+ case "heatingOn":
212
+ if (node.algorithm !== "specified") {
213
+ node.status({ fill: "red", shape: "ring", text: "heatingOn not used in this algorithm" });
214
+ if (done) done();
215
+ return;
216
+ }
217
+ if (typeof msg.payload === 'number') {
218
+ node.heatingOn = msg.payload;
219
+ node.heatingOnType = "num";
220
+ node.status({
221
+ fill: "green",
222
+ shape: "dot",
223
+ text: `heatingOn: ${msg.payload.toFixed(2)}`
224
+ });
225
+ } else {
226
+ node.status({ fill: "red", shape: "ring", text: "invalid heatingOn" });
227
+ }
228
+ break;
229
+ case "diff":
230
+ if (typeof msg.payload === 'number' && msg.payload >= 0.01) {
231
+ node.diff = msg.payload;
232
+ node.diffType = "num";
233
+ node.status({
234
+ fill: "green",
235
+ shape: "dot",
236
+ text: `diff: ${msg.payload.toFixed(2)}`
237
+ });
238
+ } else {
239
+ node.status({ fill: "red", shape: "ring", text: "invalid diff" });
240
+ }
241
+ break;
242
+ case "anticipator":
243
+ if (typeof msg.payload === 'number' && msg.payload >= -2) {
244
+ node.anticipator = msg.payload;
245
+ node.anticipatorType = "num";
246
+ node.status({
247
+ fill: "green",
248
+ shape: "dot",
249
+ text: `anticipator: ${msg.payload.toFixed(2)}`
250
+ });
251
+ } else {
252
+ node.status({ fill: "red", shape: "ring", text: "invalid anticipator" });
253
+ }
254
+ break;
255
+ case "ignoreAnticipatorCycles":
256
+ if (typeof msg.payload === 'number' && msg.payload >= 0) {
257
+ node.ignoreAnticipatorCycles = Math.floor(msg.payload);
258
+ node.ignoreAnticipatorCyclesType = "num";
259
+ node.status({
260
+ fill: "green",
261
+ shape: "dot",
262
+ text: `ignoreAnticipatorCycles: ${Math.floor(msg.payload)}`
263
+ });
264
+ } else {
265
+ node.status({ fill: "red", shape: "ring", text: "invalid ignoreAnticipatorCycles" });
266
+ }
267
+ break;
268
+ case "isHeating":
269
+ if (typeof msg.payload === "boolean") {
270
+ node.isHeating = msg.payload;
271
+ node.status({
272
+ fill: "green",
273
+ shape: "dot",
274
+ text: `isHeating: ${msg.payload}`
275
+ });
276
+ } else {
277
+ node.status({ fill: "red", shape: "ring", text: "invalid isHeating" });
278
+ }
279
+ break;
280
+ default:
281
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
282
+ break;
283
+ }
284
+ if (done) done();
285
+ return;
286
+ }
287
+
288
+ if (!msg.hasOwnProperty("payload")) {
289
+ node.status({ fill: "red", shape: "ring", text: "missing input" });
290
+ if (done) done();
291
+ return;
292
+ }
293
+
294
+ const input = parseFloat(msg.payload);
295
+ if (isNaN(input)) {
296
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
297
+ if (done) done();
298
+ return;
299
+ }
300
+
301
+ const isHeating = msg.hasOwnProperty("isHeating") && typeof msg.isHeating === "boolean" ? msg.isHeating : node.isHeating;
302
+ if (msg.hasOwnProperty("isHeating") && typeof msg.isHeating !== "boolean") {
303
+ node.status({ fill: "red", shape: "ring", text: "invalid isHeating (must be boolean)" });
304
+ if (done) done();
305
+ return;
306
+ }
307
+
308
+ // Evaluate all properties using typed-input system
309
+ let setpoint, heatingSetpoint, coolingSetpoint, coolingOn, coolingOff, heatingOff, heatingOn, diff, anticipator, ignoreAnticipatorCycles;
310
+
311
+ try {
312
+ setpoint = RED.util.evaluateNodeProperty(node.setpoint, node.setpointType, node, msg);
313
+ heatingSetpoint = RED.util.evaluateNodeProperty(node.heatingSetpoint, node.heatingSetpointType, node, msg);
314
+ coolingSetpoint = RED.util.evaluateNodeProperty(node.coolingSetpoint, node.coolingSetpointType, node, msg);
315
+ coolingOn = RED.util.evaluateNodeProperty(node.coolingOn, node.coolingOnType, node, msg);
316
+ coolingOff = RED.util.evaluateNodeProperty(node.coolingOff, node.coolingOffType, node, msg);
317
+ heatingOff = RED.util.evaluateNodeProperty(node.heatingOff, node.heatingOffType, node, msg);
318
+ heatingOn = RED.util.evaluateNodeProperty(node.heatingOn, node.heatingOnType, node, msg);
319
+ diff = RED.util.evaluateNodeProperty(node.diff, node.diffType, node, msg);
320
+ anticipator = RED.util.evaluateNodeProperty(node.anticipator, node.anticipatorType, node, msg);
321
+ ignoreAnticipatorCycles = RED.util.evaluateNodeProperty(node.ignoreAnticipatorCycles, node.ignoreAnticipatorCyclesType, node, msg);
322
+ } catch (err) {
323
+ node.error(`Error evaluating properties: ${err.message}`, msg);
324
+ if (done) done();
325
+ return;
326
+ }
327
+
328
+ // Set defaults for invalid values
329
+ if (typeof setpoint !== 'number' || isNaN(setpoint)) setpoint = 70;
330
+ if (typeof heatingSetpoint !== 'number' || isNaN(heatingSetpoint)) heatingSetpoint = 68;
331
+ if (typeof coolingSetpoint !== 'number' || isNaN(coolingSetpoint)) coolingSetpoint = 74;
332
+ if (typeof coolingOn !== 'number' || isNaN(coolingOn)) coolingOn = 74;
333
+ if (typeof coolingOff !== 'number' || isNaN(coolingOff)) coolingOff = 72;
334
+ if (typeof heatingOff !== 'number' || isNaN(heatingOff)) heatingOff = 68;
335
+ if (typeof heatingOn !== 'number' || isNaN(heatingOn)) heatingOn = 66;
336
+ if (typeof diff !== 'number' || isNaN(diff) || diff < 0.01) diff = 2;
337
+ if (typeof anticipator !== 'number' || isNaN(anticipator) || anticipator < -2) anticipator = 0.5;
338
+ if (typeof ignoreAnticipatorCycles !== 'number' || isNaN(ignoreAnticipatorCycles) || ignoreAnticipatorCycles < 0) {
339
+ ignoreAnticipatorCycles = 1;
340
+ }
341
+
342
+ // Handle mode changes and anticipator logic
343
+ if (lastIsHeating !== null && isHeating !== lastIsHeating) {
344
+ modeChanged = true;
345
+ cyclesSinceModeChange = 0;
346
+ }
347
+ lastIsHeating = isHeating;
348
+ if ((below && !lastBelow) || (above && !lastAbove)) {
349
+ cyclesSinceModeChange++;
350
+ }
351
+
352
+ let effectiveAnticipator = anticipator;
353
+ if (modeChanged && ignoreAnticipatorCycles > 0 && cyclesSinceModeChange <= ignoreAnticipatorCycles) {
354
+ effectiveAnticipator = 0;
355
+ }
356
+ if (cyclesSinceModeChange > ignoreAnticipatorCycles) {
357
+ modeChanged = false;
358
+ }
359
+
360
+ lastAbove = above;
361
+ lastBelow = below;
362
+
363
+ // Main thermostat logic
364
+ if (node.algorithm === "single") {
365
+ const delta = diff / 2;
366
+ const hiValue = setpoint + delta;
367
+ const loValue = setpoint - delta;
368
+ const hiOffValue = setpoint + effectiveAnticipator;
369
+ const loOffValue = setpoint - effectiveAnticipator;
370
+
371
+ if (input > hiValue) {
372
+ above = true;
373
+ below = false;
374
+ } else if (input < loValue) {
375
+ above = false;
376
+ below = true;
377
+ } else if (above && input < hiOffValue) {
378
+ above = false;
379
+ } else if (below && input > loOffValue) {
380
+ below = false;
381
+ }
382
+ } else if (node.algorithm === "split") {
383
+ if (isHeating) {
384
+ const delta = diff / 2;
385
+ const loValue = heatingSetpoint - delta;
386
+ const loOffValue = heatingSetpoint - effectiveAnticipator;
387
+
388
+ if (input < loValue) {
389
+ below = true;
390
+ } else if (below && input > loOffValue) {
391
+ below = false;
392
+ }
393
+ above = false;
394
+ } else {
395
+ const delta = diff / 2;
396
+ const hiValue = coolingSetpoint + delta;
397
+ const hiOffValue = coolingSetpoint + effectiveAnticipator;
398
+
399
+ if (input > hiValue) {
400
+ above = true;
401
+ } else if (above && input < hiOffValue) {
402
+ above = false;
403
+ }
404
+ below = false;
405
+ }
406
+ } else if (node.algorithm === "specified") {
407
+ if (isHeating) {
408
+ const heatingOffValue = heatingOff - effectiveAnticipator;
409
+ if (input < heatingOn) {
410
+ below = true;
411
+ } else if (below && input > heatingOffValue) {
412
+ below = false;
413
+ }
414
+ above = false;
415
+ } else {
416
+ const coolingOffValue = coolingOff + effectiveAnticipator;
417
+ if (input > coolingOn) {
418
+ above = true;
419
+ } else if (above && input < coolingOffValue) {
420
+ above = false;
421
+ }
422
+ below = false;
423
+ }
424
+ }
425
+
426
+ // Add status information to every output message
427
+ const statusInfo = {
428
+ algorithm: node.algorithm,
429
+ input: input,
430
+ isHeating: isHeating,
431
+ above: above,
432
+ below: below,
433
+ modeChanged: modeChanged,
434
+ cyclesSinceModeChange: cyclesSinceModeChange,
435
+ effectiveAnticipator: effectiveAnticipator
436
+ };
437
+
438
+ // Add algorithm-specific status
439
+ if (node.algorithm === "single") {
440
+ statusInfo.setpoint = setpoint;
441
+ statusInfo.diff = diff;
442
+ statusInfo.anticipator = anticipator;
443
+ } else if (node.algorithm === "split") {
444
+ statusInfo.heatingSetpoint = heatingSetpoint;
445
+ statusInfo.coolingSetpoint = coolingSetpoint;
446
+ statusInfo.diff = diff;
447
+ statusInfo.anticipator = anticipator;
448
+ } else {
449
+ statusInfo.coolingOn = coolingOn;
450
+ statusInfo.coolingOff = coolingOff;
451
+ statusInfo.heatingOff = heatingOff;
452
+ statusInfo.heatingOn = heatingOn;
453
+ statusInfo.anticipator = anticipator;
454
+ }
455
+
456
+ // Create outputs with status information
457
+ const outputs = [
458
+ {
459
+ payload: isHeating,
460
+ context: "isHeating",
461
+ status: statusInfo
462
+ },
463
+ {
464
+ payload: above,
465
+ status: statusInfo
466
+ },
467
+ {
468
+ payload: below,
469
+ status: statusInfo
470
+ }
471
+ ];
472
+
473
+ send(outputs);
474
+
475
+ if (above === lastAbove && below === lastBelow) {
476
+ node.status({
477
+ fill: "blue",
478
+ shape: "ring",
479
+ text: `in: ${input.toFixed(2)}, out: ${isHeating ? "heating" : "cooling"}, above: ${above}, below: ${below}`
480
+ });
481
+ } else {
482
+ node.status({
483
+ fill: "blue",
484
+ shape: "dot",
485
+ text: `in: ${input.toFixed(2)}, out: ${isHeating ? "heating" : "cooling"}, above: ${above}, below: ${below}`
486
+ });
487
+ }
488
+
489
+ if (done) done();
490
+ });
491
+
492
+ node.on("close", function(done) {
493
+ node.status({});
494
+ done();
495
+ });
496
+ }
497
+
498
+ RED.nodes.registerType("tstat-block", TstatBlockNode);
499
+ };
@@ -0,0 +1,150 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="units-block">
3
+ <div class="form-row">
4
+ <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
5
+ <input type="text" id="node-input-name" placeholder="Name">
6
+ </div>
7
+ <div class="form-row">
8
+ <label for="node-input-unit" title="Unit to append to msg.units"><i class="fa fa-tag"></i> Unit</label>
9
+ <input type="text" id="node-input-unit">
10
+ </div>
11
+ </script>
12
+
13
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
14
+ <script type="text/javascript">
15
+ RED.nodes.registerType("units-block", {
16
+ category: "control",
17
+ color: "#301934",
18
+ defaults: {
19
+ name: { value: "" },
20
+ unit: { value: "°F", required: true }
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ inputLabels: ["input"],
25
+ outputLabels: ["output with units"],
26
+ icon: "font-awesome/fa-tag",
27
+ paletteLabel: "units",
28
+ label: function() {
29
+ return this.name || `units ${this.unit}`;
30
+ },
31
+ oneditprepare: function() {
32
+ // TODO: create a shared configuration file for conversion and units.
33
+ const validUnits = [
34
+ // Temperature
35
+ "°C", "°F", "K", "°R",
36
+
37
+ // Humidity/Pressure
38
+ "%RH", "Pa", "kPa", "bar", "mbar", "psi", "atm", "inH₂O", "mmH₂O", "inHg",
39
+
40
+ // Flow
41
+ "CFM", "m³/h", "L/s",
42
+
43
+ // Electrical
44
+ "V", "mV", "A", "mA", "W", "kW", "hp", "Ω",
45
+
46
+ // General/Math
47
+ "%",
48
+
49
+ // Length
50
+ "m", "cm", "mm", "km", "ft", "in",
51
+
52
+ // Mass
53
+ "kg", "g", "lb",
54
+
55
+ // Time
56
+ "s", "min", "h",
57
+
58
+ // Volume
59
+ "L", "mL", "gal",
60
+
61
+ // Other
62
+ "lx", "cd", "B", "T"
63
+ ];
64
+ $("#node-input-unit").typedInput({
65
+ default: "str",
66
+ types: [{
67
+ value: "str",
68
+ options: [
69
+ { value: "°C", label: "°C (Celsius)" },
70
+ { value: "°F", label: "°F (Fahrenheit)" },
71
+ { value: "K", label: "K (Kelvin)" },
72
+ { value: "°R", label: "°R (Rankine)" },
73
+ { value: "%RH", label: "%RH (Relative Humidity)" },
74
+ { value: "%", label: "% (Percent)" },
75
+ { value: "Pa", label: "Pa (Pascal)" },
76
+ { value: "kPa", label: "kPa (Kilopascal)" },
77
+ { value: "bar", label: "bar" },
78
+ { value: "mbar", label: "mbar (Millibar)" },
79
+ { value: "psi", label: "psi" },
80
+ { value: "inHg", label: "inHg (Inches of Mercury)" },
81
+ { value: "atm", label: "atm (Atmosphere)" },
82
+ { value: "inH₂O", label: "inH₂O (Inches of Water)" },
83
+ { value: "mmH₂O", label: "mmH₂O (Millimeters of Water)" },
84
+ { value: "CFM", label: "CFM (Cubic Feet per Minute)" },
85
+ { value: "m³/h", label: "m³/h (Cubic Meters per Hour)" },
86
+ { value: "L/s", label: "L/s (Liters per Second)" },
87
+ { value: "V", label: "V (Volt)" },
88
+ { value: "mV", label: "mV (Millivolt)" },
89
+ { value: "A", label: "A (Ampere)" },
90
+ { value: "mA", label: "mA (Milliampere)" },
91
+ { value: "W", label: "W (Watt)" },
92
+ { value: "kW", label: "kW (Kilowatt)" },
93
+ { value: "hp", label: "hp (Horsepower)" },
94
+ { value: "Ω", label: "Ω (Ohm)" },
95
+ { value: "m", label: "m (Meter)" },
96
+ { value: "cm", label: "cm (Centimeter)" },
97
+ { value: "mm", label: "mm (Millimeter)" },
98
+ { value: "km", label: "km (Kilometer)" },
99
+ { value: "ft", label: "ft (Foot)" },
100
+ { value: "in", label: "in (Inch)" },
101
+ { value: "kg", label: "kg (Kilogram)" },
102
+ { value: "g", label: "g (Gram)" },
103
+ { value: "lb", label: "lb (Pound)" },
104
+ { value: "s", label: "s (Second)" },
105
+ { value: "min", label: "min (Minute)" },
106
+ { value: "h", label: "h (Hour)" },
107
+ { value: "L", label: "L (Liter)" },
108
+ { value: "mL", label: "mL (Milliliter)" },
109
+ { value: "gal", label: "gal (Gallon)" },
110
+ { value: "lx", label: "lx (Lux)" },
111
+ { value: "cd", label: "cd (Candela)" },
112
+ { value: "B", label: "B (Bel)" },
113
+ { value: "T", label: "T (Tesla)" }
114
+ ]
115
+ }],
116
+ typeField: false
117
+ }).typedInput("value", this.unit || "°F");
118
+ }
119
+ });
120
+ </script>
121
+
122
+ <!-- Help Section -->
123
+ <script type="text/markdown" data-help-name="units-block">
124
+ Appends a selected unit to `msg.units` of every input message.
125
+
126
+ ### Inputs
127
+ : context (string) : Configures unit (`"unit"`) if provided. Ignored otherwise.
128
+ : payload (any) : Input payload to pass through unchanged.
129
+
130
+ ### Outputs
131
+ : payload (any) : Original payload.
132
+ : units (string) : Selected unit (e.g., °F, %RH, inH₂O).
133
+
134
+ ### Details
135
+ Appends `msg.units` with the configured unit to every input message.
136
+ Unit can be set via editor or dynamically with `msg.context = "unit"` and a valid unit in `msg.payload`.
137
+ Supports units like °C, °F, %RH, inH₂O, CFM for HVAC and control systems.
138
+ Processes every input message, preserving all original properties, just adding `msg.units`.
139
+
140
+ ### Status
141
+ - Green (dot): Configuration update
142
+ - Blue (dot): State changed
143
+ - Blue (ring): State unchanged
144
+ - Red (ring): Error
145
+ - Yellow (ring): Warning
146
+
147
+ ### References
148
+ - [Node-RED Documentation](https://nodered.org/docs/)
149
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
150
+ </script>