@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.
- package/README.md +43 -0
- package/nodes/accumulate-block.html +71 -0
- package/nodes/accumulate-block.js +104 -0
- package/nodes/add-block.html +67 -0
- package/nodes/add-block.js +97 -0
- package/nodes/analog-switch-block.html +65 -0
- package/nodes/analog-switch-block.js +129 -0
- package/nodes/and-block.html +64 -0
- package/nodes/and-block.js +73 -0
- package/nodes/average-block.html +97 -0
- package/nodes/average-block.js +137 -0
- package/nodes/boolean-switch-block.html +59 -0
- package/nodes/boolean-switch-block.js +88 -0
- package/nodes/boolean-to-number-block.html +59 -0
- package/nodes/boolean-to-number-block.js +45 -0
- package/nodes/cache-block.html +69 -0
- package/nodes/cache-block.js +106 -0
- package/nodes/call-status-block.html +111 -0
- package/nodes/call-status-block.js +274 -0
- package/nodes/changeover-block.html +234 -0
- package/nodes/changeover-block.js +392 -0
- package/nodes/comment-block.html +83 -0
- package/nodes/comment-block.js +53 -0
- package/nodes/compare-block.html +64 -0
- package/nodes/compare-block.js +84 -0
- package/nodes/contextual-label-block.html +67 -0
- package/nodes/contextual-label-block.js +52 -0
- package/nodes/convert-block.html +179 -0
- package/nodes/convert-block.js +289 -0
- package/nodes/count-block.html +57 -0
- package/nodes/count-block.js +92 -0
- package/nodes/debounce-block.html +64 -0
- package/nodes/debounce-block.js +140 -0
- package/nodes/delay-block.html +104 -0
- package/nodes/delay-block.js +180 -0
- package/nodes/divide-block.html +65 -0
- package/nodes/divide-block.js +123 -0
- package/nodes/edge-block.html +71 -0
- package/nodes/edge-block.js +120 -0
- package/nodes/frequency-block.html +55 -0
- package/nodes/frequency-block.js +140 -0
- package/nodes/hysteresis-block.html +131 -0
- package/nodes/hysteresis-block.js +142 -0
- package/nodes/interpolate-block.html +74 -0
- package/nodes/interpolate-block.js +141 -0
- package/nodes/load-sequence-block.html +134 -0
- package/nodes/load-sequence-block.js +272 -0
- package/nodes/max-block.html +76 -0
- package/nodes/max-block.js +103 -0
- package/nodes/memory-block.html +90 -0
- package/nodes/memory-block.js +241 -0
- package/nodes/min-block.html +77 -0
- package/nodes/min-block.js +106 -0
- package/nodes/minmax-block.html +89 -0
- package/nodes/minmax-block.js +119 -0
- package/nodes/modulo-block.html +73 -0
- package/nodes/modulo-block.js +126 -0
- package/nodes/multiply-block.html +63 -0
- package/nodes/multiply-block.js +115 -0
- package/nodes/negate-block.html +55 -0
- package/nodes/negate-block.js +91 -0
- package/nodes/nullify-block.html +111 -0
- package/nodes/nullify-block.js +78 -0
- package/nodes/on-change-block.html +79 -0
- package/nodes/on-change-block.js +191 -0
- package/nodes/oneshot-block.html +96 -0
- package/nodes/oneshot-block.js +169 -0
- package/nodes/or-block.html +64 -0
- package/nodes/or-block.js +73 -0
- package/nodes/pid-block.html +205 -0
- package/nodes/pid-block.js +407 -0
- package/nodes/priority-block.html +66 -0
- package/nodes/priority-block.js +239 -0
- package/nodes/rate-limit-block.html +99 -0
- package/nodes/rate-limit-block.js +221 -0
- package/nodes/round-block.html +73 -0
- package/nodes/round-block.js +89 -0
- package/nodes/saw-tooth-wave-block.html +87 -0
- package/nodes/saw-tooth-wave-block.js +161 -0
- package/nodes/scale-range-block.html +90 -0
- package/nodes/scale-range-block.js +137 -0
- package/nodes/sine-wave-block.html +88 -0
- package/nodes/sine-wave-block.js +142 -0
- package/nodes/subtract-block.html +64 -0
- package/nodes/subtract-block.js +103 -0
- package/nodes/thermistor-block.html +81 -0
- package/nodes/thermistor-block.js +146 -0
- package/nodes/tick-tock-block.html +66 -0
- package/nodes/tick-tock-block.js +110 -0
- package/nodes/time-sequence-block.html +67 -0
- package/nodes/time-sequence-block.js +144 -0
- package/nodes/triangle-wave-block.html +86 -0
- package/nodes/triangle-wave-block.js +154 -0
- package/nodes/tstat-block.html +311 -0
- package/nodes/tstat-block.js +499 -0
- package/nodes/units-block.html +150 -0
- package/nodes/units-block.js +106 -0
- 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>
|