@bldgblocks/node-red-contrib-control 0.1.37 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,70 @@
1
+ // ============================================================================
2
+ // Tstat Block - Thermostat Controller
3
+ // ============================================================================
4
+ // Controls heating/cooling calls based on temperature input and setpoints.
5
+ // Works as a companion to changeover-block: changeover selects the mode,
6
+ // tstat generates the actual heating/cooling call signals.
7
+ //
8
+ // Supports three algorithms:
9
+ // - single: one setpoint ± diff/2 defines on/off thresholds
10
+ // - split: separate heating/cooling setpoints with diff hysteresis
11
+ // - specified: explicit on/off temperatures for heating and cooling
12
+ //
13
+ // Outputs (3 ports):
14
+ // 1. isHeating (boolean) - current mode passthrough
15
+ // 2. above (boolean) - cooling call active
16
+ // 3. below (boolean) - heating call active
17
+ //
18
+ // Anticipator adjusts turn-off points to prevent overshoot.
19
+ // ignoreAnticipatorCycles disables anticipator after mode changes.
20
+ // All configuration via typed inputs (editor, msg, flow, global).
21
+ // ============================================================================
22
+
1
23
  module.exports = function(RED) {
2
24
  const utils = require('./utils')(RED);
3
-
25
+
26
+ const VALID_ALGORITHMS = ["single", "split", "specified"];
27
+
4
28
  function TstatBlockNode(config) {
5
29
  RED.nodes.createNode(this, config);
6
30
  const node = this;
7
31
  node.isBusy = false;
8
32
 
9
- // Store typed-input properties
33
+ // ====================================================================
34
+ // Configuration — safe parseFloat that doesn't clobber zero
35
+ // ====================================================================
36
+ const num = (v, fallback) => { const n = parseFloat(v); return isNaN(n) ? fallback : n; };
37
+
10
38
  node.name = config.name;
11
- node.setpoint = parseFloat(config.setpoint);
12
- node.heatingSetpoint = parseFloat(config.heatingSetpoint);
13
- node.coolingSetpoint = parseFloat(config.coolingSetpoint);
14
- node.coolingOn = parseFloat(config.coolingOn);
15
- node.coolingOff = parseFloat(config.coolingOff);
16
- node.heatingOff = parseFloat(config.heatingOff);
17
- node.heatingOn = parseFloat(config.heatingOn);
18
- node.diff = parseFloat(config.diff);
19
- node.anticipator = parseFloat(config.anticipator);
20
- node.ignoreAnticipatorCycles = Math.floor(config.ignoreAnticipatorCycles);
21
- node.isHeating = config.isHeating === true;
22
- node.algorithm = config.algorithm;
39
+ node.setpoint = num(config.setpoint, 70);
40
+ node.heatingSetpoint = num(config.heatingSetpoint, 68);
41
+ node.coolingSetpoint = num(config.coolingSetpoint, 74);
42
+ node.coolingOn = num(config.coolingOn, 74);
43
+ node.coolingOff = num(config.coolingOff, 72);
44
+ node.heatingOff = num(config.heatingOff, 68);
45
+ node.heatingOn = num(config.heatingOn, 66);
46
+ node.diff = num(config.diff, 2);
47
+ node.anticipator = num(config.anticipator, 0.5);
48
+ node.ignoreAnticipatorCycles = Math.floor(num(config.ignoreAnticipatorCycles, 1));
49
+ node.isHeating = config.isHeating === true || config.isHeating === "true";
50
+ node.algorithm = VALID_ALGORITHMS.includes(config.algorithm) ? config.algorithm : "single";
51
+
52
+ // Startup delay: suppress above/below calls until mode has settled
53
+ node.startupDelay = Math.max(num(config.startupDelay, 30), 0);
54
+ node.startupComplete = node.startupDelay === 0;
55
+ node.startupTimer = null;
56
+ if (!node.startupComplete) {
57
+ utils.setStatusWarn(node, `startup delay: ${node.startupDelay}s`);
58
+ node.startupTimer = setTimeout(() => {
59
+ node.startupComplete = true;
60
+ node.startupTimer = null;
61
+ utils.setStatusOK(node, "startup delay complete");
62
+ }, node.startupDelay * 1000);
63
+ }
23
64
 
65
+ // ====================================================================
66
+ // Runtime state
67
+ // ====================================================================
24
68
  let above = false;
25
69
  let below = false;
26
70
  let lastAbove = false;
@@ -29,6 +73,35 @@ module.exports = function(RED) {
29
73
  let cyclesSinceModeChange = 0;
30
74
  let modeChanged = false;
31
75
 
76
+ // ====================================================================
77
+ // Typed-input evaluation helpers
78
+ // ====================================================================
79
+ function evalNumeric(configValue, configType, fallback, msg) {
80
+ return utils.evaluateNodeProperty(configValue, configType, node, msg)
81
+ .then(val => { const n = parseFloat(val); return isNaN(n) ? fallback : n; })
82
+ .catch(() => fallback);
83
+ }
84
+
85
+ function evalBool(configValue, configType, fallback, msg) {
86
+ return utils.evaluateNodeProperty(configValue, configType, node, msg)
87
+ .then(val => {
88
+ if (typeof val === "boolean") return val;
89
+ if (val === "true") return true;
90
+ if (val === "false") return false;
91
+ return fallback;
92
+ })
93
+ .catch(() => fallback);
94
+ }
95
+
96
+ function evalEnum(configValue, configType, allowed, fallback, msg) {
97
+ return utils.evaluateNodeProperty(configValue, configType, node, msg)
98
+ .then(val => allowed.includes(val) ? val : fallback)
99
+ .catch(() => fallback);
100
+ }
101
+
102
+ // ====================================================================
103
+ // Main input handler
104
+ // ====================================================================
32
105
  node.on("input", async function(msg, send, done) {
33
106
  send = send || function() { node.send.apply(node, arguments); };
34
107
 
@@ -38,244 +111,65 @@ module.exports = function(RED) {
38
111
  return;
39
112
  }
40
113
 
41
- // Evaluate dynamic properties
114
+ // ----------------------------------------------------------------
115
+ // 1. Evaluate typed inputs (async phase)
116
+ // ----------------------------------------------------------------
117
+ if (node.isBusy) {
118
+ utils.setStatusBusy(node, "busy - dropped msg");
119
+ if (done) done();
120
+ return;
121
+ }
122
+ node.isBusy = true;
123
+
42
124
  try {
125
+ const results = await Promise.all([
126
+ evalNumeric(config.setpoint, config.setpointType, node.setpoint, msg), // 0
127
+ evalNumeric(config.heatingSetpoint, config.heatingSetpointType, node.heatingSetpoint, msg), // 1
128
+ evalNumeric(config.coolingSetpoint, config.coolingSetpointType, node.coolingSetpoint, msg), // 2
129
+ evalNumeric(config.coolingOn, config.coolingOnType, node.coolingOn, msg), // 3
130
+ evalNumeric(config.coolingOff, config.coolingOffType, node.coolingOff, msg), // 4
131
+ evalNumeric(config.heatingOff, config.heatingOffType, node.heatingOff, msg), // 5
132
+ evalNumeric(config.heatingOn, config.heatingOnType, node.heatingOn, msg), // 6
133
+ evalNumeric(config.diff, config.diffType, node.diff, msg), // 7
134
+ evalNumeric(config.anticipator, config.anticipatorType, node.anticipator, msg), // 8
135
+ evalNumeric(config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node.ignoreAnticipatorCycles, msg), // 9
136
+ evalBool(config.isHeating, config.isHeatingType, node.isHeating, msg), // 10
137
+ evalEnum(config.algorithm, config.algorithmType, VALID_ALGORITHMS, node.algorithm, msg), // 11
138
+ ]);
43
139
 
44
- // Check busy lock
45
- if (node.isBusy) {
46
- // Update status to let user know they are pushing too fast
47
- utils.setStatusBusy(node, "busy - dropped msg");
48
- if (done) done();
49
- return;
50
- }
140
+ node.setpoint = results[0];
141
+ node.heatingSetpoint = results[1];
142
+ node.coolingSetpoint = results[2];
143
+ node.coolingOn = results[3];
144
+ node.coolingOff = results[4];
145
+ node.heatingOff = results[5];
146
+ node.heatingOn = results[6];
147
+ node.diff = results[7];
148
+ node.anticipator = results[8];
149
+ node.ignoreAnticipatorCycles = Math.floor(results[9]);
150
+ node.isHeating = results[10];
151
+ node.algorithm = results[11];
51
152
 
52
- // Lock node during evaluation
53
- node.isBusy = true;
54
-
55
- // Begin evaluations
56
- const evaluations = [];
57
-
58
- //0
59
- evaluations.push(
60
- utils.requiresEvaluation(config.setpointType)
61
- ? utils.evaluateNodeProperty(config.setpoint, config.setpointType, node, msg)
62
- .then(val => parseFloat(val))
63
- : Promise.resolve(node.setpoint),
64
- );
65
- //1
66
- evaluations.push(
67
- utils.requiresEvaluation(config.heatingSetpointType)
68
- ? utils.evaluateNodeProperty(config.heatingSetpoint, config.heatingSetpointType, node, msg)
69
- .then(val => parseFloat(val))
70
- : Promise.resolve(node.heatingSetpoint),
71
- );
72
- //2
73
- evaluations.push(
74
- utils.requiresEvaluation(config.coolingSetpointType)
75
- ? utils.evaluateNodeProperty(config.coolingSetpoint, config.coolingSetpointType, node, msg)
76
- .then(val => parseFloat(val))
77
- : Promise.resolve(node.coolingSetpoint),
78
- );
79
- //3
80
- evaluations.push(
81
- utils.requiresEvaluation(config.coolingOnType)
82
- ? utils.evaluateNodeProperty(config.coolingOn, config.coolingOnType, node, msg)
83
- .then(val => parseFloat(val))
84
- : Promise.resolve(node.coolingOn),
85
- );
86
- //4
87
- evaluations.push(
88
- utils.requiresEvaluation(config.coolingOffType)
89
- ? utils.evaluateNodeProperty(config.coolingOff, config.coolingOffType, node, msg)
90
- .then(val => parseFloat(val))
91
- : Promise.resolve(node.coolingOff),
92
- );
93
- //5
94
- evaluations.push(
95
- utils.requiresEvaluation(config.heatingOffType)
96
- ? utils.evaluateNodeProperty(config.heatingOff, config.heatingOffType, node, msg)
97
- .then(val => parseFloat(val))
98
- : Promise.resolve(node.heatingOff),
99
- );
100
- //6
101
- evaluations.push(
102
- utils.requiresEvaluation(config.heatingOnType)
103
- ? utils.evaluateNodeProperty(config.heatingOn, config.heatingOnType, node, msg)
104
- .then(val => parseFloat(val))
105
- : Promise.resolve(node.heatingOn),
106
- );
107
- //7
108
- evaluations.push(
109
- utils.requiresEvaluation(config.diffType)
110
- ? utils.evaluateNodeProperty(config.diff, config.diffType, node, msg)
111
- .then(val => parseFloat(val))
112
- : Promise.resolve(node.diff),
113
- );
114
- //8
115
- evaluations.push(
116
- utils.requiresEvaluation(config.anticipatorType)
117
- ? utils.evaluateNodeProperty(config.anticipator, config.anticipatorType, node, msg)
118
- .then(val => parseFloat(val))
119
- : Promise.resolve(node.anticipator),
120
- );
121
- //9
122
- evaluations.push(
123
- utils.requiresEvaluation(config.ignoreAnticipatorCyclesType)
124
- ? utils.evaluateNodeProperty(config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node, msg)
125
- .then(val => Math.floor(val))
126
- : Promise.resolve(node.ignoreAnticipatorCycles),
127
- );
128
- //10
129
- evaluations.push(
130
- utils.requiresEvaluation(config.isHeatingType)
131
- ? utils.evaluateNodeProperty(config.isHeating, config.isHeatingType, node, msg)
132
- .then(val => val === true)
133
- : Promise.resolve(node.isHeating),
134
- );
135
- //11
136
- evaluations.push(
137
- utils.requiresEvaluation(config.algorithmType)
138
- ? utils.evaluateNodeProperty(config.algorithm, config.algorithmType, node, msg)
139
- : Promise.resolve(node.algorithm),
140
- );
141
-
142
- const results = await Promise.all(evaluations);
143
-
144
- // Update runtime with evaluated values
145
- if (!isNaN(results[0])) node.setpoint = results[0];
146
- if (!isNaN(results[1])) node.heatingSetpoint = results[1];
147
- if (!isNaN(results[2])) node.coolingSetpoint = results[2];
148
- if (!isNaN(results[3])) node.coolingOn = results[3];
149
- if (!isNaN(results[4])) node.coolingOff = results[4];
150
- if (!isNaN(results[5])) node.heatingOff = results[5];
151
- if (!isNaN(results[6])) node.heatingOn = results[6];
152
- if (!isNaN(results[7])) node.diff = results[7];
153
- if (!isNaN(results[8])) node.anticipator = results[8];
154
- if (!isNaN(results[9])) node.ignoreAnticipatorCycles = results[9];
155
- if (results[10] !== null) node.isHeating = results[10];
156
- if (results[11]) node.algorithm = results[11];
157
153
  } catch (err) {
158
154
  node.error(`Error evaluating properties: ${err.message}`);
159
155
  if (done) done();
160
156
  return;
161
157
  } finally {
162
- // Release, all synchronous from here on
163
158
  node.isBusy = false;
164
159
  }
165
160
 
166
- // Handle configuration messages
167
- if (msg.hasOwnProperty("context")) {
168
- if (!msg.hasOwnProperty("payload")) {
169
- utils.setStatusError(node, "missing payload");
170
- if (done) done();
171
- return;
172
- }
173
-
174
- switch (msg.context) {
175
- case "algorithm":
176
- if (["single", "split", "specified"].includes(msg.payload)) {
177
- node.algorithm = msg.payload;
178
- utils.setStatusOK(node, `algorithm: ${msg.payload}`);
179
- } else {
180
- utils.setStatusError(node, "invalid algorithm");
181
- }
182
- break;
183
- case "setpoint":
184
- if (typeof msg.payload === 'number') {
185
- node.setpoint = msg.payload;
186
- utils.setStatusOK(node, `setpoint: ${msg.payload.toFixed(2)}`);
187
- } else {
188
- utils.setStatusError(node, "invalid setpoint");
189
- }
190
- break;
191
- case "heatingSetpoint":
192
- if (typeof msg.payload === 'number') {
193
- node.heatingSetpoint = msg.payload;
194
- utils.setStatusOK(node, `heatingSetpoint: ${msg.payload.toFixed(2)}`);
195
- } else {
196
- utils.setStatusError(node, "invalid heatingSetpoint");
197
- }
198
- break;
199
- case "coolingSetpoint":
200
- if (typeof msg.payload === 'number') {
201
- node.coolingSetpoint = msg.payload;
202
- utils.setStatusOK(node, `coolingSetpoint: ${msg.payload.toFixed(2)}`);
203
- } else {
204
- utils.setStatusError(node, "invalid coolingSetpoint");
205
- }
206
- break;
207
- case "coolingOn":
208
- if (typeof msg.payload === 'number') {
209
- node.coolingOn = msg.payload;
210
- utils.setStatusOK(node, `coolingOn: ${msg.payload.toFixed(2)}`);
211
- } else {
212
- utils.setStatusError(node, "invalid coolingOn");
213
- }
214
- break;
215
- case "coolingOff":
216
- if (typeof msg.payload === 'number') {
217
- node.coolingOff = msg.payload;
218
- utils.setStatusOK(node, `coolingOff: ${msg.payload.toFixed(2)}`);
219
- } else {
220
- utils.setStatusError(node, "invalid coolingOff");
221
- }
222
- break;
223
- case "heatingOff":
224
- if (typeof msg.payload === 'number') {
225
- node.heatingOff = msg.payload;
226
- utils.setStatusOK(node, `heatingOff: ${msg.payload.toFixed(2)}`);
227
- } else {
228
- utils.setStatusError(node, "invalid heatingOff");
229
- }
230
- break;
231
- case "heatingOn":
232
- if (typeof msg.payload === 'number') {
233
- node.heatingOn = msg.payload;
234
- utils.setStatusOK(node, `heatingOn: ${msg.payload.toFixed(2)}`);
235
- } else {
236
- utils.setStatusError(node, "invalid heatingOn");
237
- }
238
- break;
239
- case "diff":
240
- if (typeof msg.payload === 'number' && msg.payload >= 0.01) {
241
- node.diff = msg.payload;
242
- utils.setStatusOK(node, `diff: ${msg.payload.toFixed(2)}`);
243
- } else {
244
- utils.setStatusError(node, "invalid diff");
245
- }
246
- break;
247
- case "anticipator":
248
- if (typeof msg.payload === 'number' && msg.payload >= -2) {
249
- node.anticipator = msg.payload;
250
- utils.setStatusOK(node, `anticipator: ${msg.payload.toFixed(2)}`);
251
- } else {
252
- utils.setStatusError(node, "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
- utils.setStatusOK(node, `ignoreAnticipatorCycles: ${Math.floor(msg.payload)}`);
259
- } else {
260
- utils.setStatusError(node, "invalid ignoreAnticipatorCycles");
261
- }
262
- break;
263
- case "isHeating":
264
- if (typeof msg.payload === "boolean") {
265
- node.isHeating = msg.payload;
266
- utils.setStatusOK(node, `isHeating: ${msg.payload}`);
267
- } else {
268
- utils.setStatusError(node, "invalid isHeating");
269
- }
270
- break;
271
- default:
272
- utils.setStatusWarn(node, "unknown context");
273
- break;
274
- }
161
+ // ----------------------------------------------------------------
162
+ // 2. Validate constraints
163
+ // ----------------------------------------------------------------
164
+ if (node.diff < 0.01) {
165
+ utils.setStatusError(node, "diff must be >= 0.01");
275
166
  if (done) done();
276
167
  return;
277
168
  }
278
169
 
170
+ // ----------------------------------------------------------------
171
+ // 4. Read temperature from msg.payload
172
+ // ----------------------------------------------------------------
279
173
  if (!msg.hasOwnProperty("payload")) {
280
174
  utils.setStatusError(node, "missing payload");
281
175
  if (done) done();
@@ -289,19 +183,14 @@ module.exports = function(RED) {
289
183
  return;
290
184
  }
291
185
 
292
- const isHeating = msg.hasOwnProperty("isHeating") && typeof msg.isHeating === "boolean" ? msg.isHeating : node.isHeating;
293
- if (msg.hasOwnProperty("isHeating") && typeof msg.isHeating !== "boolean") {
294
- utils.setStatusError(node, "invalid isHeating (must be boolean)");
295
- if (done) done();
296
- return;
297
- }
298
-
299
- // Handle mode changes and anticipator logic
186
+ // ----------------------------------------------------------------
187
+ // 4. Anticipator mode-change logic
188
+ // ----------------------------------------------------------------
300
189
  if (lastIsHeating !== null && node.isHeating !== lastIsHeating) {
301
190
  modeChanged = true;
302
191
  cyclesSinceModeChange = 0;
303
192
  }
304
- lastIsHeating = node.isHeating;
193
+ lastIsHeating = node.isHeating;
305
194
  if ((below && !lastBelow) || (above && !lastAbove)) {
306
195
  cyclesSinceModeChange++;
307
196
  }
@@ -316,155 +205,144 @@ module.exports = function(RED) {
316
205
 
317
206
  lastAbove = above;
318
207
  lastBelow = below;
319
- let delta = 0;
320
- let hiValue = 0;
321
- let loValue = 0;
322
- let hiOffValue = 0;
323
- let loOffValue = 0;
324
- let activeHeatingSetpoint = 0;
325
- let activeCoolingSetpoint = 0;
326
-
327
- // Main thermostat logic
328
- // The Tstat node does not control heating/cooling mode, only operates heating or cooling according to the mode set and respective setpoints.
208
+
209
+ // ----------------------------------------------------------------
210
+ // 5. Thermostat logic — compute above/below calls
211
+ // ----------------------------------------------------------------
212
+ let activeSetpoint = 0;
213
+ let onThreshold = 0;
214
+ let offThreshold = 0;
215
+
329
216
  if (node.algorithm === "single") {
330
- // Note:
331
- // Make sure your mode selection is handled upstream and does not oscillate modes.
332
- // This was changed to allow for broader anticipator authority, or even negative (overshoot) so duty cycle can be better managed.
333
- // So the same setpoint can be used year round and maintain tight control.
334
- // Alternatively, you would need a larger diff value to prevent oscillation.
335
- delta = node.diff / 2;
336
- hiValue = node.setpoint + delta;
337
- loValue = node.setpoint - delta;
338
- hiOffValue = node.setpoint + effectiveAnticipator;
339
- loOffValue = node.setpoint - effectiveAnticipator;
340
- activeHeatingSetpoint = node.setpoint;
341
- activeCoolingSetpoint = node.setpoint;
342
-
343
- if (isHeating) {
344
- if (input < loValue) {
217
+ const delta = node.diff / 2;
218
+ activeSetpoint = node.setpoint;
219
+
220
+ if (node.isHeating) {
221
+ onThreshold = node.setpoint - delta;
222
+ offThreshold = node.setpoint - effectiveAnticipator;
223
+ if (input < onThreshold) {
345
224
  below = true;
346
- } else if (below && input > loOffValue) {
225
+ } else if (below && input > offThreshold) {
347
226
  below = false;
348
227
  }
349
228
  above = false;
350
229
  } else {
351
- if (input > hiValue) {
230
+ onThreshold = node.setpoint + delta;
231
+ offThreshold = node.setpoint + effectiveAnticipator;
232
+ if (input > onThreshold) {
352
233
  above = true;
353
- } else if (above && input < hiOffValue) {
234
+ } else if (above && input < offThreshold) {
354
235
  above = false;
355
236
  }
356
237
  below = false;
357
238
  }
358
239
  } else if (node.algorithm === "split") {
359
- activeHeatingSetpoint = node.heatingSetpoint;
360
- activeCoolingSetpoint = node.coolingSetpoint;
361
240
  if (node.isHeating) {
362
- delta = node.diff / 2;
363
- loValue = node.heatingSetpoint - delta;
364
- loOffValue = node.heatingSetpoint - effectiveAnticipator;
365
-
366
- if (input < loValue) {
241
+ const delta = node.diff / 2;
242
+ activeSetpoint = node.heatingSetpoint;
243
+ onThreshold = node.heatingSetpoint - delta;
244
+ offThreshold = node.heatingSetpoint - effectiveAnticipator;
245
+ if (input < onThreshold) {
367
246
  below = true;
368
- } else if (below && input > loOffValue) {
247
+ } else if (below && input > offThreshold) {
369
248
  below = false;
370
249
  }
371
250
  above = false;
372
251
  } else {
373
- delta = node.diff / 2;
374
- hiValue = node.coolingSetpoint + delta;
375
- hiOffValue = node.coolingSetpoint + effectiveAnticipator;
376
-
377
- if (input > hiValue) {
252
+ const delta = node.diff / 2;
253
+ activeSetpoint = node.coolingSetpoint;
254
+ onThreshold = node.coolingSetpoint + delta;
255
+ offThreshold = node.coolingSetpoint + effectiveAnticipator;
256
+ if (input > onThreshold) {
378
257
  above = true;
379
- } else if (above && input < hiOffValue) {
258
+ } else if (above && input < offThreshold) {
380
259
  above = false;
381
260
  }
382
261
  below = false;
383
262
  }
384
263
  } else if (node.algorithm === "specified") {
385
- activeHeatingSetpoint = node.heatingOn;
386
- activeCoolingSetpoint = node.coolingOn;
387
264
  if (node.isHeating) {
388
- if (input < node.heatingOn) {
265
+ activeSetpoint = node.heatingOn;
266
+ onThreshold = node.heatingOn;
267
+ offThreshold = node.heatingOff - effectiveAnticipator;
268
+ if (input < onThreshold) {
389
269
  below = true;
390
- } else if (below && input > node.heatingOff - effectiveAnticipator) {
270
+ } else if (below && input > offThreshold) {
391
271
  below = false;
392
272
  }
393
273
  above = false;
394
274
  } else {
395
- if (input > node.coolingOn) {
275
+ activeSetpoint = node.coolingOn;
276
+ onThreshold = node.coolingOn;
277
+ offThreshold = node.coolingOff + effectiveAnticipator;
278
+ if (input > onThreshold) {
396
279
  above = true;
397
- } else if (above && input < node.coolingOff + effectiveAnticipator) {
280
+ } else if (above && input < offThreshold) {
398
281
  above = false;
399
282
  }
400
283
  below = false;
401
284
  }
402
285
  }
403
-
404
- // Add status information to every output message
286
+
287
+ // ----------------------------------------------------------------
288
+ // 6. Startup suppression
289
+ // ----------------------------------------------------------------
290
+ const outputAbove = node.startupComplete ? above : false;
291
+ const outputBelow = node.startupComplete ? below : false;
292
+
293
+ // ----------------------------------------------------------------
294
+ // 7. Build and send outputs
295
+ // ----------------------------------------------------------------
405
296
  const statusInfo = {
406
297
  algorithm: node.algorithm,
407
- input: input,
298
+ input,
408
299
  isHeating: node.isHeating,
409
- above: above,
410
- below: below,
411
- modeChanged: modeChanged,
412
- cyclesSinceModeChange: cyclesSinceModeChange,
413
- effectiveAnticipator: effectiveAnticipator
300
+ above: outputAbove,
301
+ below: outputBelow,
302
+ activeSetpoint,
303
+ onThreshold,
304
+ offThreshold,
305
+ diff: node.diff,
306
+ anticipator: node.anticipator,
307
+ effectiveAnticipator,
308
+ modeChanged,
309
+ cyclesSinceModeChange
414
310
  };
415
311
 
416
- // Add algorithm-specific status
417
- statusInfo.activeHeatingSetpoint = activeHeatingSetpoint;
418
- statusInfo.activeCoolingSetpoint = activeCoolingSetpoint;
419
- statusInfo.diff = node.diff;
420
- statusInfo.anticipator = node.anticipator;
421
- statusInfo.loValue = loValue;
422
- statusInfo.hiValue = hiValue;
423
- statusInfo.loOffValue = loOffValue;
424
- statusInfo.hiOffValue = hiOffValue;
312
+ send([
313
+ { payload: node.isHeating, status: statusInfo },
314
+ { payload: outputAbove, status: statusInfo },
315
+ { payload: outputBelow, status: statusInfo }
316
+ ]);
425
317
 
426
- if (node.algorithm === "single") {
427
- statusInfo.setpoint = node.setpoint;
428
- } else if (node.algorithm === "split") {
429
- statusInfo.heatingSetpoint = node.heatingSetpoint;
430
- statusInfo.coolingSetpoint = node.coolingSetpoint;
431
- } else {
432
- statusInfo.hiValue = node.coolingOn;
433
- statusInfo.hiOffValue = node.coolingOff;
434
- statusInfo.loOffValue = node.heatingOff;
435
- statusInfo.loValue = node.heatingOn;
436
- statusInfo.anticipator = node.anticipator;
437
- }
318
+ // ----------------------------------------------------------------
319
+ // 8. Status display
320
+ // ----------------------------------------------------------------
321
+ const mode = node.isHeating ? "heat" : "cool";
322
+ const call = node.isHeating ? outputBelow : outputAbove;
323
+ const threshold = node.isHeating
324
+ ? `<${onThreshold.toFixed(1)}`
325
+ : `>${onThreshold.toFixed(1)}`;
326
+ const suffix = !node.startupComplete ? " [startup]" : "";
327
+ const text = `${input.toFixed(1)}° ${threshold} [${mode}] call:${call}${suffix}`;
438
328
 
439
- // Create outputs with status information
440
- const outputs = [
441
- {
442
- payload: node.isHeating,
443
- context: "isHeating",
444
- status: statusInfo
445
- },
446
- {
447
- payload: above,
448
- status: statusInfo
449
- },
450
- {
451
- payload: below,
452
- status: statusInfo
453
- }
454
- ];
455
-
456
- send(outputs);
457
-
458
- if (above === lastAbove && below === lastBelow) {
459
- utils.setStatusUnchanged(node, `in: ${input.toFixed(2)}, out: ${node.isHeating ? "heating" : "cooling"}, above: ${above}, below: ${below}`);
329
+ if (outputAbove === lastAbove && outputBelow === lastBelow) {
330
+ utils.setStatusUnchanged(node, text);
460
331
  } else {
461
- utils.setStatusChanged(node, `in: ${input.toFixed(2)}, out: ${node.isHeating ? "heating" : "cooling"}, above: ${above}, below: ${below}`);
332
+ utils.setStatusChanged(node, text);
462
333
  }
463
334
 
464
335
  if (done) done();
465
336
  });
466
337
 
338
+ // ====================================================================
339
+ // Cleanup
340
+ // ====================================================================
467
341
  node.on("close", function(done) {
342
+ if (node.startupTimer) {
343
+ clearTimeout(node.startupTimer);
344
+ node.startupTimer = null;
345
+ }
468
346
  done();
469
347
  });
470
348
  }