@bldgblocks/node-red-contrib-control 0.2.1 → 0.2.3

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.
@@ -90,14 +90,7 @@ module.exports = function(RED) {
90
90
  // Ignore unknown context
91
91
  }
92
92
 
93
- // Validate input payload
94
- if (!msg.hasOwnProperty("payload")) {
95
- utils.setStatusError(node, "missing payload");
96
- send(msg);
97
- if (done) done();
98
- return;
99
- }
100
-
93
+ // Get input value from configured property
101
94
  let inputValue;
102
95
  try {
103
96
  inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
@@ -199,16 +199,16 @@ module.exports = function(RED) {
199
199
  }
200
200
  }
201
201
 
202
- // Fall back to default or fallback
202
+ // Fall through to fallback, then default (matching global-setter hierarchy)
203
203
  if (selectedValue === null) {
204
- if (defaultValue !== null) {
205
- selectedValue = defaultValue;
206
- activePriority = "default";
207
- selectedMessage = messages.default;
208
- } else if (fallbackValue !== null) {
204
+ if (fallbackValue !== null) {
209
205
  selectedValue = fallbackValue;
210
206
  activePriority = "fallback";
211
207
  selectedMessage = messages.fallback;
208
+ } else if (defaultValue !== null) {
209
+ selectedValue = defaultValue;
210
+ activePriority = "default";
211
+ selectedMessage = messages.default;
212
212
  }
213
213
  }
214
214
 
@@ -35,7 +35,7 @@ module.exports = function(RED) {
35
35
  }
36
36
 
37
37
  // Set initial status
38
- utils.setStatusOK(node, `mode: ${node.mode}, out: ${node.currentValue.toFixed(2)}`);
38
+ utils.setStatusOK(node, `${node.currentValue.toFixed(2)}`);
39
39
 
40
40
  let updateTimer = null;
41
41
 
@@ -58,7 +58,7 @@ module.exports = function(RED) {
58
58
  node.lastUpdate = now;
59
59
  const msg = RED.util.cloneMessage(node.lastInputMsg);
60
60
  msg.payload = node.currentValue;
61
- utils.setStatusOK(node, `mode: rate-limit, out: ${node.currentValue.toFixed(2)}`);
61
+ utils.setStatusOK(node, `${node.currentValue.toFixed(2)}`);
62
62
  node.send(msg);
63
63
  }
64
64
  }
@@ -151,7 +151,7 @@ module.exports = function(RED) {
151
151
 
152
152
  if (node.mode === "rate-limit") {
153
153
  node.targetValue = inputValue;
154
- utils.setStatusOK(node, `mode: rate-limit, target: ${node.targetValue.toFixed(2)}`);
154
+ utils.setStatusOK(node, `target: ${node.targetValue.toFixed(2)}`);
155
155
  updateRateLimitOutput();
156
156
  startTimer();
157
157
  } else if (node.mode === "threshold") {
@@ -159,15 +159,15 @@ module.exports = function(RED) {
159
159
  if (diff > node.threshold) {
160
160
  msg.payload = inputValue;
161
161
  node.currentValue = inputValue;
162
- utils.setStatusChanged(node, `mode: threshold, out: ${node.currentValue.toFixed(2)}`);
162
+ utils.setStatusChanged(node, `${node.currentValue.toFixed(2)}`);
163
163
  send(msg);
164
164
  } else {
165
- utils.setStatusUnchanged(node, `mode: threshold, out: ${node.currentValue.toFixed(2)}`);
165
+ utils.setStatusUnchanged(node, `${node.currentValue.toFixed(2)}`);
166
166
  }
167
167
  } else if (node.mode === "full-value") {
168
168
  node.currentValue = inputValue;
169
169
  msg.payload = inputValue;
170
- utils.setStatusChanged(node, `mode: full-value, out: ${node.currentValue.toFixed(2)}`);
170
+ utils.setStatusChanged(node, `${node.currentValue.toFixed(2)}`);
171
171
  send(msg);
172
172
  }
173
173
 
@@ -84,7 +84,7 @@ module.exports = function(RED) {
84
84
  }
85
85
 
86
86
  msg.payload = result;
87
- utils.setStatusOK(node, `in: ${inputValue.toFixed(3)}, out: ${result}`);
87
+ utils.setStatusOK(node, `${result}`);
88
88
  send(msg);
89
89
  if (done) done();
90
90
  });
@@ -122,7 +122,7 @@ module.exports = function(RED) {
122
122
  node.lastInput = inputValue;
123
123
  const out = calculate(inputValue, node.inMin, node.inMax, node.outMin, node.outMax, node.clamp);
124
124
  msg.payload = out;
125
- utils.setStatusOK(node, `in: ${inputValue.toFixed(2)}, out: ${out.toFixed(2)}`);
125
+ utils.setStatusOK(node, `${out.toFixed(2)}`);
126
126
  send(msg);
127
127
 
128
128
  if (done) done();
@@ -129,76 +129,84 @@
129
129
  ]
130
130
  }, "msg", "flow", "global"],
131
131
  typeField: "#node-input-algorithmType"
132
- }).typedInput("type", node.algorithmType).typedInput("value", node.algorithm);
132
+ });
133
133
 
134
134
  $("#node-input-setpoint").typedInput({
135
135
  default: "num",
136
136
  types: ["num", "msg", "flow", "global"],
137
137
  typeField: "#node-input-setpointType"
138
- }).typedInput("type", node.setpointType || "num").typedInput("value", node.setpoint);
138
+ });
139
139
 
140
140
  $("#node-input-heatingSetpoint").typedInput({
141
141
  default: "num",
142
142
  types: ["num", "msg", "flow", "global"],
143
143
  typeField: "#node-input-heatingSetpointType"
144
- }).typedInput("type", node.heatingSetpointType || "num").typedInput("value", node.heatingSetpoint);
144
+ });
145
145
 
146
146
  $("#node-input-coolingSetpoint").typedInput({
147
147
  default: "num",
148
148
  types: ["num", "msg", "flow", "global"],
149
149
  typeField: "#node-input-coolingSetpointType"
150
- }).typedInput("type", node.coolingSetpointType || "num").typedInput("value", node.coolingSetpoint);
150
+ });
151
151
 
152
152
  $("#node-input-coolingOn").typedInput({
153
153
  default: "num",
154
154
  types: ["num", "msg", "flow", "global"],
155
155
  typeField: "#node-input-coolingOnType"
156
- }).typedInput("type", node.coolingOnType || "num").typedInput("value", node.coolingOn);
156
+ });
157
157
 
158
158
  $("#node-input-coolingOff").typedInput({
159
159
  default: "num",
160
160
  types: ["num", "msg", "flow", "global"],
161
161
  typeField: "#node-input-coolingOffType"
162
- }).typedInput("type", node.coolingOffType || "num").typedInput("value", node.coolingOff);
162
+ });
163
163
 
164
164
  $("#node-input-heatingOff").typedInput({
165
165
  default: "num",
166
166
  types: ["num", "msg", "flow", "global"],
167
167
  typeField: "#node-input-heatingOffType"
168
- }).typedInput("type", node.heatingOffType || "num").typedInput("value", node.heatingOff);
168
+ });
169
169
 
170
170
  $("#node-input-heatingOn").typedInput({
171
171
  default: "num",
172
172
  types: ["num", "msg", "flow", "global"],
173
173
  typeField: "#node-input-heatingOnType"
174
- }).typedInput("type", node.heatingOnType || "num").typedInput("value", node.heatingOn);
174
+ });
175
175
 
176
176
  $("#node-input-diff").typedInput({
177
177
  default: "num",
178
178
  types: ["num", "msg", "flow", "global"],
179
179
  typeField: "#node-input-diffType"
180
- }).typedInput("type", node.diffType || "num").typedInput("value", node.diff);
180
+ });
181
181
 
182
182
  $("#node-input-anticipator").typedInput({
183
183
  default: "num",
184
184
  types: ["num", "msg", "flow", "global"],
185
185
  typeField: "#node-input-anticipatorType"
186
- }).typedInput("type", node.anticipatorType || "num").typedInput("value", node.anticipator);
186
+ });
187
187
 
188
188
  $("#node-input-ignoreAnticipatorCycles").typedInput({
189
189
  default: "num",
190
190
  types: ["num", "msg", "flow", "global"],
191
191
  typeField: "#node-input-ignoreAnticipatorCyclesType"
192
- }).typedInput("type", node.ignoreAnticipatorCyclesType || "num").typedInput("value", node.ignoreAnticipatorCycles);
192
+ });
193
193
 
194
194
  $("#node-input-isHeating").typedInput({
195
195
  default: "bool",
196
196
  types: ["bool", "msg", "flow", "global"],
197
197
  typeField: "#node-input-isHeatingType"
198
- }).typedInput("type", node.isHeatingType || "bool").typedInput("value", node.isHeating);
198
+ });
199
199
 
200
200
  function toggleFields() {
201
- const algorithm = $algorithm.val();
201
+ const type = $("#node-input-algorithm").typedInput("type");
202
+ if (type !== "dropdown") {
203
+ // Dynamic source — hide all, resolved at runtime
204
+ $singleFields.hide();
205
+ $splitFields.hide();
206
+ $specifiedFields.hide();
207
+ return;
208
+ }
209
+ const algorithm = $("#node-input-algorithm").typedInput("value");
202
210
  if (algorithm === "single") {
203
211
  $singleFields.show();
204
212
  $splitFields.hide();
@@ -287,6 +287,13 @@ module.exports = function(RED) {
287
287
  // ----------------------------------------------------------------
288
288
  // 6. Startup suppression
289
289
  // ----------------------------------------------------------------
290
+ // Prevent call state from accumulating during startup.
291
+ // Without this, above/below can latch ON while output is
292
+ // suppressed, then emit a false call the moment startup ends.
293
+ if (!node.startupComplete) {
294
+ above = false;
295
+ below = false;
296
+ }
290
297
  const outputAbove = node.startupComplete ? above : false;
291
298
  const outputBelow = node.startupComplete ? below : false;
292
299
 
@@ -324,7 +331,31 @@ module.exports = function(RED) {
324
331
  ? `<${onThreshold.toFixed(1)}`
325
332
  : `>${onThreshold.toFixed(1)}`;
326
333
  const suffix = !node.startupComplete ? " [startup]" : "";
327
- const text = `${input.toFixed(1)}° ${threshold} [${mode}] call:${call}${suffix}`;
334
+ let thresholdText, hysteresisText;
335
+ if (node.isHeating) {
336
+ thresholdText = `<${onThreshold.toFixed(1)}`;
337
+ if (outputBelow && input > onThreshold && input <= offThreshold) {
338
+ hysteresisText = ` (holding, off at >${offThreshold.toFixed(1)})`;
339
+ } else if (outputBelow && input < onThreshold) {
340
+ hysteresisText = " (on)";
341
+ } else if (!outputBelow && input > offThreshold) {
342
+ hysteresisText = " (off)";
343
+ } else {
344
+ hysteresisText = "";
345
+ }
346
+ } else {
347
+ thresholdText = `>${onThreshold.toFixed(1)}`;
348
+ if (outputAbove && input < onThreshold && input >= offThreshold) {
349
+ hysteresisText = ` (holding, off at <${offThreshold.toFixed(1)})`;
350
+ } else if (outputAbove && input > onThreshold) {
351
+ hysteresisText = " (on)";
352
+ } else if (!outputAbove && input < offThreshold) {
353
+ hysteresisText = " (off)";
354
+ } else {
355
+ hysteresisText = "";
356
+ }
357
+ }
358
+ const text = `${input.toFixed(1)}° ${thresholdText} [${mode}] call:${call}${hysteresisText}${suffix}`;
328
359
 
329
360
  if (outputAbove === lastAbove && outputBelow === lastBelow) {
330
361
  utils.setStatusUnchanged(node, text);
@@ -38,7 +38,7 @@ module.exports = function(RED) {
38
38
 
39
39
  const payloadPreview = input !== null ? (typeof input === "number" ? input.toFixed(2) : JSON.stringify(input).slice(0, 20)) : "none";
40
40
 
41
- utils.setStatusOK(node, `in: ${payloadPreview} unit: ${node.unit !== "" ? node.unit : "none"}`);
41
+ utils.setStatusOK(node, `${node.unit !== "" ? node.unit : "none"}`);
42
42
 
43
43
  msg.units = node.unit;
44
44
  send(msg);
package/nodes/utils.js CHANGED
@@ -93,14 +93,24 @@ module.exports = function(RED) {
93
93
  let value = state.defaultValue;
94
94
  let priority = 'default';
95
95
 
96
+ // Check priorities 1-16 first (highest)
96
97
  for (let i = 1; i <= 16; i++) {
97
98
  // Check strictly for undefined/null, allow 0 or false
98
99
  if (state.priority[i] !== undefined && state.priority[i] !== null) {
99
100
  value = state.priority[i];
100
101
  priority = String(i);
101
- break;
102
+ return { value, priority };
102
103
  }
103
104
  }
105
+
106
+ // Fall through to fallback slot if no priority is set
107
+ if (state.fallback !== undefined && state.fallback !== null) {
108
+ value = state.fallback;
109
+ priority = 'fallback';
110
+ return { value, priority };
111
+ }
112
+
113
+ // Finally fall back to default
104
114
  return { value, priority };
105
115
  }
106
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [
6
6
  "node-red",