@bldgblocks/node-red-contrib-control 0.1.27 → 0.1.28

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.
@@ -5,15 +5,12 @@
5
5
  </div>
6
6
  <div class="form-row">
7
7
  <label for="node-input-algorithm" title="Algorithm: single setpoint with diff, split setpoints, or specified setpoints"><i class="fa fa-cog"></i> Algorithm</label>
8
- <select id="node-input-algorithm">
9
- <option value="single">Single Setpoint</option>
10
- <option value="split">Split Setpoint</option>
11
- <option value="specified">Specified Setpoint</option>
12
- </select>
8
+ <input type="text" id="node-input-algorithm" placeholder="single">
9
+ <input type="hidden" id="node-input-algorithmType">
13
10
  </div>
14
11
  <div class="form-row single-setpoint">
15
12
  <label for="node-input-setpoint" title="Target temperature setpoint (number from num, msg, flow, or global)"><i class="fa fa-crosshairs"></i> Setpoint</label>
16
- <input type="text" id="node-input-setpoint" laceholder="70">
13
+ <input type="text" id="node-input-setpoint" placeholder="70">
17
14
  <input type="hidden" id="node-input-setpointType">
18
15
  </div>
19
16
  <div class="form-row split-setpoint" style="display: none;">
@@ -63,7 +60,8 @@
63
60
  </div>
64
61
  <div class="form-row">
65
62
  <label for="node-input-isHeating" title="Heating mode (true) or cooling mode (false)"><i class="fa fa-fire"></i> Heating Mode</label>
66
- <input type="checkbox" id="node-input-isHeating" style="width: auto; vertical-align: middle;">
63
+ <input type="text" id="node-input-isHeating" style="width: auto; vertical-align: middle;">
64
+ <input type="hidden" id="node-input-isHeatingType">
67
65
  </div>
68
66
  </script>
69
67
 
@@ -74,6 +72,7 @@
74
72
  defaults: {
75
73
  name: { value: "" },
76
74
  algorithm: { value: "single" },
75
+ algorithmType: { value: "dropdown" },
77
76
  setpoint: { value: "70" },
78
77
  setpointType: { value: "num" },
79
78
  heatingSetpoint: { value: "68" },
@@ -94,7 +93,8 @@
94
93
  anticipatorType: { value: "num" },
95
94
  ignoreAnticipatorCycles: { value: "1" },
96
95
  ignoreAnticipatorCyclesType: { value: "num" },
97
- isHeating: { value: false }
96
+ isHeating: { value: false },
97
+ isHeatingType: { value: "bool" }
98
98
  },
99
99
  inputs: 1,
100
100
  outputs: 3,
@@ -112,41 +112,85 @@
112
112
  const $singleFields = $(".single-setpoint");
113
113
  const $splitFields = $(".split-setpoint");
114
114
  const $specifiedFields = $(".specified-setpoint");
115
+
116
+ $("#node-input-algorithm").typedInput({
117
+ default: "dropdown",
118
+ types: [{
119
+ value: "dropdown",
120
+ options: [
121
+ { value: "single", label: "Single"},
122
+ { value: "split", label: "Split"},
123
+ { value: "specified", label: "Specified"},
124
+ ]
125
+ }, "msg", "flow", "global"],
126
+ typeField: "#node-input-algorithmType"
127
+ }).typedInput("type", node.algorithmType).typedInput("value", node.algorithm);
115
128
 
116
- $("#node-input-name").val(node.name || "");
117
- $("#node-input-algorithm").val(node.algorithm || "single");
118
- $("#node-input-isHeating").prop("checked", node.isHeating === true);
129
+ $("#node-input-setpoint").typedInput({
130
+ default: "num",
131
+ types: ["num", "msg", "flow", "global"],
132
+ typeField: "#node-input-setpointType"
133
+ }).typedInput("type", node.setpointType || "num").typedInput("value", node.setpoint);
119
134
 
120
- const typedInputs = [
121
- { id: "node-input-setpoint", typeId: "node-input-setpointType", defaultValue: "70", defaultType: "num" },
122
- { id: "node-input-heatingSetpoint", typeId: "node-input-heatingSetpointType", defaultValue: "68", defaultType: "num" },
123
- { id: "node-input-coolingSetpoint", typeId: "node-input-coolingSetpointType", defaultValue: "74", defaultType: "num" },
124
- { id: "node-input-coolingOn", typeId: "node-input-coolingOnType", defaultValue: "74", defaultType: "num" },
125
- { id: "node-input-coolingOff", typeId: "node-input-coolingOffType", defaultValue: "72", defaultType: "num" },
126
- { id: "node-input-heatingOff", typeId: "node-input-heatingOffType", defaultValue: "68", defaultType: "num" },
127
- { id: "node-input-heatingOn", typeId: "node-input-heatingOnType", defaultValue: "66", defaultType: "num" },
128
- { id: "node-input-diff", typeId: "node-input-diffType", defaultValue: "2", defaultType: "num" },
129
- { id: "node-input-anticipator", typeId: "node-input-anticipatorType", defaultValue: "0.5", defaultType: "num" },
130
- { id: "node-input-ignoreAnticipatorCycles", typeId: "node-input-ignoreAnticipatorCyclesType", defaultValue: "1", defaultType: "num" }
131
- ];
135
+ $("#node-input-heatingSetpoint").typedInput({
136
+ default: "num",
137
+ types: ["num", "msg", "flow", "global"],
138
+ typeField: "#node-input-heatingSetpointType"
139
+ }).typedInput("type", node.heatingSetpointType || "num").typedInput("value", node.heatingSetpoint);
132
140
 
133
- typedInputs.forEach(input => {
134
- try {
135
- const fieldName = input.id.replace("node-input-", "");
136
- const storedValue = node[fieldName] !== undefined ? node[fieldName] : input.defaultValue;
137
- const storedType = node[`${fieldName}Type`] || input.defaultType;
141
+ $("#node-input-coolingSetpoint").typedInput({
142
+ default: "num",
143
+ types: ["num", "msg", "flow", "global"],
144
+ typeField: "#node-input-coolingSetpointType"
145
+ }).typedInput("type", node.coolingSetpointType || "num").typedInput("value", node.coolingSetpoint);
138
146
 
139
- $(`#${input.id}`).typedInput({
140
- default: input.defaultType,
141
- types: ["num", "msg", "flow", "global"],
142
- typeField: `#${input.typeId}`
143
- }).typedInput("type", storedType)
144
- .typedInput("value", storedValue);
145
- } catch (err) {
146
- console.error(`Error initializing typedInput for ${input.id}:`, err);
147
- }
148
- });
147
+ $("#node-input-coolingOn").typedInput({
148
+ default: "num",
149
+ types: ["num", "msg", "flow", "global"],
150
+ typeField: "#node-input-coolingOnType"
151
+ }).typedInput("type", node.coolingOnType || "num").typedInput("value", node.coolingOn);
152
+
153
+ $("#node-input-coolingOff").typedInput({
154
+ default: "num",
155
+ types: ["num", "msg", "flow", "global"],
156
+ typeField: "#node-input-coolingOffType"
157
+ }).typedInput("type", node.coolingOffType || "num").typedInput("value", node.coolingOff);
158
+
159
+ $("#node-input-heatingOff").typedInput({
160
+ default: "num",
161
+ types: ["num", "msg", "flow", "global"],
162
+ typeField: "#node-input-heatingOffType"
163
+ }).typedInput("type", node.heatingOffType || "num").typedInput("value", node.heatingOff);
149
164
 
165
+ $("#node-input-heatingOn").typedInput({
166
+ default: "num",
167
+ types: ["num", "msg", "flow", "global"],
168
+ typeField: "#node-input-heatingOnType"
169
+ }).typedInput("type", node.heatingOnType || "num").typedInput("value", node.heatingOn);
170
+
171
+ $("#node-input-diff").typedInput({
172
+ default: "num",
173
+ types: ["num", "msg", "flow", "global"],
174
+ typeField: "#node-input-diffType"
175
+ }).typedInput("type", node.diffType || "num").typedInput("value", node.diff);
176
+
177
+ $("#node-input-anticipator").typedInput({
178
+ default: "num",
179
+ types: ["num", "msg", "flow", "global"],
180
+ typeField: "#node-input-anticipatorType"
181
+ }).typedInput("type", node.anticipatorType || "num").typedInput("value", node.anticipator);
182
+
183
+ $("#node-input-ignoreAnticipatorCycles").typedInput({
184
+ default: "num",
185
+ types: ["num", "msg", "flow", "global"],
186
+ typeField: "#node-input-ignoreAnticipatorCyclesType"
187
+ }).typedInput("type", node.ignoreAnticipatorCyclesType || "num").typedInput("value", node.ignoreAnticipatorCycles);
188
+
189
+ $("#node-input-isHeating").typedInput({
190
+ default: "bool",
191
+ types: ["bool", "msg", "flow", "global"],
192
+ typeField: "#node-input-isHeatingType"
193
+ }).typedInput("type", node.isHeatingType || "bool").typedInput("value", node.isHeating);
150
194
 
151
195
  function toggleFields() {
152
196
  const algorithm = $algorithm.val();
@@ -201,7 +245,7 @@ All output messages include a `msg.status` object containing runtime information
201
245
  - `effectiveAnticipator`: Current anticipator value after mode change adjustments
202
246
 
203
247
  ### Status Monitoring
204
- Instead of a dedicated status output, all outputs include comprehensive status information
248
+ All outputs include comprehensive status information in `msg.status`. Example:
205
249
  ```json
206
250
  {
207
251
  "status": {
@@ -218,6 +262,8 @@ Instead of a dedicated status output, all outputs include comprehensive status i
218
262
  "effectiveAnticipator": 0.5
219
263
  }
220
264
  }
265
+ ```
266
+
221
267
  ### Algorithms
222
268
  - **Single Setpoint**:
223
269
  - Uses `setpoint`, `diff`, and `anticipator`.
@@ -21,7 +21,9 @@ module.exports = function(RED) {
21
21
  node.heatingOn = parseFloat(RED.util.evaluateNodeProperty( config.heatingOn, config.heatingOnType, node ));
22
22
  node.diff = parseFloat(RED.util.evaluateNodeProperty( config.diff, config.diffType, node ));
23
23
  node.anticipator = parseFloat(RED.util.evaluateNodeProperty( config.anticipator, config.anticipatorType, node ));
24
- node.ignoreAnticipatorCycles = Math.floor(RED.util.evaluateNodeProperty( config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node ));
24
+ node.ignoreAnticipatorCycles = Math.floor(RED.util.evaluateNodeProperty( config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node ));
25
+ node.isHeating = RED.util.evaluateNodeProperty( config.isHeating, config.isHeatingType, node ) === true;
26
+ node.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node );
25
27
  } catch (err) {
26
28
  node.error(`Error evaluating properties: ${err.message}`);
27
29
  }
@@ -75,6 +77,12 @@ module.exports = function(RED) {
75
77
  if (utils.requiresEvaluation(config.ignoreAnticipatorCyclesType)) {
76
78
  node.ignoreAnticipatorCycles = Math.floor(RED.util.evaluateNodeProperty( config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node, msg ));
77
79
  }
80
+ if (utils.requiresEvaluation(config.isHeatingType)) {
81
+ node.isHeating = RED.util.evaluateNodeProperty( config.isHeating, config.isHeatingType, node, msg ) === true;
82
+ }
83
+ if (utils.requiresEvaluation(config.algorithmType)) {
84
+ node.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node, msg );
85
+ }
78
86
  } catch (err) {
79
87
  node.error(`Error evaluating properties: ${err.message}`);
80
88
  if (done) done();
@@ -197,14 +205,14 @@ module.exports = function(RED) {
197
205
  }
198
206
 
199
207
  if (!msg.hasOwnProperty("payload")) {
200
- node.status({ fill: "red", shape: "ring", text: "missing input" });
208
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
201
209
  if (done) done();
202
210
  return;
203
211
  }
204
212
 
205
213
  const input = parseFloat(msg.payload);
206
214
  if (isNaN(input)) {
207
- node.status({ fill: "red", shape: "ring", text: "invalid input" });
215
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
208
216
  if (done) done();
209
217
  return;
210
218
  }
@@ -236,31 +244,52 @@ module.exports = function(RED) {
236
244
 
237
245
  lastAbove = above;
238
246
  lastBelow = below;
247
+ let delta = 0;
248
+ let hiValue = 0;
249
+ let loValue = 0;
250
+ let hiOffValue = 0;
251
+ let loOffValue = 0;
252
+ let activeHeatingSetpoint = 0;
253
+ let activeCoolingSetpoint = 0;
239
254
 
240
255
  // Main thermostat logic
256
+ // The Tstat node does not control heating/cooling mode, only operates heating or cooling according to the mode set and respective setpoints.
241
257
  if (node.algorithm === "single") {
242
- const delta = node.diff / 2;
243
- const hiValue = node.setpoint + delta;
244
- const loValue = node.setpoint - delta;
245
- const hiOffValue = node.setpoint + effectiveAnticipator;
246
- const loOffValue = node.setpoint - effectiveAnticipator;
258
+ // Note:
259
+ // Make sure your mode selection is handled upstream and does not osciallate modes.
260
+ // This was changed to allow for broader anticipator authority, or even negative (overshoot) so duty cycle can be better managed.
261
+ // So the same setpoint can be used year round and maintain tight control.
262
+ // Alternatively, you would need a larger diff value to prevent oscillation.
263
+ delta = node.diff / 2;
264
+ hiValue = node.setpoint + delta;
265
+ loValue = node.setpoint - delta;
266
+ hiOffValue = node.setpoint + effectiveAnticipator;
267
+ loOffValue = node.setpoint - effectiveAnticipator;
268
+ activeHeatingSetpoint = node.setpoint;
269
+ activeCoolingSetpoint = node.setpoint;
247
270
 
248
- if (input > hiValue) {
249
- above = true;
250
- below = false;
251
- } else if (input < loValue) {
252
- above = false;
253
- below = true;
254
- } else if (above && input < hiOffValue) {
271
+ if (isHeating) {
272
+ if (input < loValue) {
273
+ below = true;
274
+ } else if (below && input > loOffValue) {
275
+ below = false;
276
+ }
255
277
  above = false;
256
- } else if (below && input > loOffValue) {
278
+ } else {
279
+ if (input > hiValue) {
280
+ above = true;
281
+ } else if (above && input < hiOffValue) {
282
+ above = false;
283
+ }
257
284
  below = false;
258
285
  }
259
286
  } else if (node.algorithm === "split") {
287
+ activeHeatingSetpoint = node.heatingSetpoint;
288
+ activeCoolingSetpoint = node.coolingSetpoint;
260
289
  if (node.isHeating) {
261
- const delta = node.diff / 2;
262
- const loValue = node.heatingSetpoint - delta;
263
- const loOffValue = node.heatingSetpoint - effectiveAnticipator;
290
+ delta = node.diff / 2;
291
+ loValue = node.heatingSetpoint - delta;
292
+ loOffValue = node.heatingSetpoint - effectiveAnticipator;
264
293
 
265
294
  if (input < loValue) {
266
295
  below = true;
@@ -269,9 +298,9 @@ module.exports = function(RED) {
269
298
  }
270
299
  above = false;
271
300
  } else {
272
- const delta = node.diff / 2;
273
- const hiValue = node.coolingSetpoint + delta;
274
- const hiOffValue = node.coolingSetpoint + effectiveAnticipator;
301
+ delta = node.diff / 2;
302
+ hiValue = node.coolingSetpoint + delta;
303
+ hiOffValue = node.coolingSetpoint + effectiveAnticipator;
275
304
 
276
305
  if (input > hiValue) {
277
306
  above = true;
@@ -281,6 +310,8 @@ module.exports = function(RED) {
281
310
  below = false;
282
311
  }
283
312
  } else if (node.algorithm === "specified") {
313
+ activeHeatingSetpoint = node.heatingOn;
314
+ activeCoolingSetpoint = node.coolingOn;
284
315
  if (node.isHeating) {
285
316
  if (input < node.heatingOn) {
286
317
  below = true;
@@ -311,20 +342,25 @@ module.exports = function(RED) {
311
342
  };
312
343
 
313
344
  // Add algorithm-specific status
345
+ statusInfo.activeHeatingSetpoint = activeHeatingSetpoint;
346
+ statusInfo.activeCoolingSetpoint = activeCoolingSetpoint;
347
+ statusInfo.diff = node.diff;
348
+ statusInfo.anticipator = node.anticipator;
349
+ statusInfo.loValue = loValue;
350
+ statusInfo.hiValue = hiValue;
351
+ statusInfo.loOffValue = loOffValue;
352
+ statusInfo.hiOffValue = hiOffValue;
353
+
314
354
  if (node.algorithm === "single") {
315
355
  statusInfo.setpoint = node.setpoint;
316
- statusInfo.diff = node.diff;
317
- statusInfo.anticipator = node.anticipator;
318
356
  } else if (node.algorithm === "split") {
319
357
  statusInfo.heatingSetpoint = node.heatingSetpoint;
320
358
  statusInfo.coolingSetpoint = node.coolingSetpoint;
321
- statusInfo.diff = node.diff;
322
- statusInfo.anticipator = node.anticipator;
323
359
  } else {
324
- statusInfo.coolingOn = node.coolingOn;
325
- statusInfo.coolingOff = node.coolingOff;
326
- statusInfo.heatingOff = node.heatingOff;
327
- statusInfo.heatingOn = node.heatingOn;
360
+ statusInfo.hiValue = node.coolingOn;
361
+ statusInfo.hiOffValue = node.coolingOff;
362
+ statusInfo.loOffValue = node.heatingOff;
363
+ statusInfo.loValue = node.heatingOn;
328
364
  statusInfo.anticipator = node.anticipator;
329
365
  }
330
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [ "node-red", "sedona", "control", "hvac" ],
6
6
  "files": ["nodes/*.js", "nodes/*.html"],
@@ -33,6 +33,7 @@
33
33
  "frequency-block": "nodes/frequency-block.js",
34
34
  "hysteresis-block": "nodes/hysteresis-block.js",
35
35
  "interpolate-block": "nodes/interpolate-block.js",
36
+ "latch-block": "nodes/latch-block.js",
36
37
  "load-sequence-block": "nodes/load-sequence-block.js",
37
38
  "max-block": "nodes/max-block.js",
38
39
  "memory-block": "nodes/memory-block.js",
@@ -48,10 +49,12 @@
48
49
  "pid-block": "nodes/pid-block.js",
49
50
  "priority-block": "nodes/priority-block.js",
50
51
  "rate-limit-block": "nodes/rate-limit-block.js",
52
+ "rate-of-change-block": "nodes/rate-of-change-block.js",
51
53
  "round-block": "nodes/round-block.js",
52
54
  "saw-tooth-wave-block": "nodes/saw-tooth-wave-block.js",
53
55
  "scale-range-block": "nodes/scale-range-block.js",
54
56
  "sine-wave-block": "nodes/sine-wave-block.js",
57
+ "string-builder-block": "nodes/string-builder-block.js",
55
58
  "subtract-block": "nodes/subtract-block.js",
56
59
  "thermistor-block": "nodes/thermistor-block.js",
57
60
  "tick-tock-block": "nodes/tick-tock-block.js",