@bldgblocks/node-red-contrib-control 0.1.26 → 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,10 +5,8 @@
5
5
  </div>
6
6
  <div class="form-row">
7
7
  <label for="node-input-algorithm" title="Control algorithm type"><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 Setpoints</option>
11
- </select>
8
+ <input type="text" id="node-input-algorithm" class="node-input-typed" placeholder="single">
9
+ <input type="hidden" id="node-input-algorithmType">
12
10
  </div>
13
11
  <div class="form-row single-only">
14
12
  <label for="node-input-setpoint" title="Target temperature setpoint (number from num, msg, flow, or global)"><i class="fa fa-thermometer-half"></i> Setpoint</label>
@@ -50,6 +48,18 @@
50
48
  Invalid
51
49
  </div>
52
50
  </div>
51
+
52
+ <div class="form-row specified-only" style="display: none;">
53
+ <label for="node-input-coolingOn" title="Temperature to turn cooling on (number from num, msg, flow, or global)"><i class="fa fa-snowflake-o"></i> Cooling On</label>
54
+ <input type="text" id="node-input-coolingOn" placeholder="74">
55
+ <input type="hidden" id="node-input-coolingOnType">
56
+ </div>
57
+ <div class="form-row specified-only" style="display: none;">
58
+ <label for="node-input-heatingOn" title="Temperature to turn heating on (number from num, msg, flow, or global)"><i class="fa fa-fire"></i> Heating On</label>
59
+ <input type="text" id="node-input-heatingOn" placeholder="66">
60
+ <input type="hidden" id="node-input-heatingOnType">
61
+ </div>
62
+
53
63
  <div class="form-row">
54
64
  <label for="node-input-swapTime" title="Minimum time before mode change (seconds, minimum 60, from num, msg, flow, or global)"><i class="fa fa-clock-o"></i> Swap Time</label>
55
65
  <input type="text" id="node-input-swapTime" class="node-input-typed" placeholder="300">
@@ -76,11 +86,8 @@
76
86
  </div>
77
87
  <div class="form-row">
78
88
  <label for="node-input-operationMode" title="Operation mode"><i class="fa fa-cogs"></i> Operation Mode</label>
79
- <select id="node-input-operationMode">
80
- <option value="auto">Auto</option>
81
- <option value="heat">Heat</option>
82
- <option value="cool">Cool</option>
83
- </select>
89
+ <input type="text" id="node-input-operationMode" placeholder="auto">
90
+ <input type="hidden" id="node-input-operationModeType">
84
91
  </div>
85
92
  </script>
86
93
 
@@ -91,6 +98,7 @@
91
98
  defaults: {
92
99
  name: { value: "" },
93
100
  algorithm: { value: "single" },
101
+ algorithmType: { value: "dropdown" },
94
102
  setpoint: { value: "70" },
95
103
  setpointType: { value: "num" },
96
104
  deadband: { value: "2" },
@@ -98,6 +106,10 @@
98
106
  heatingSetpointType: { value: "num" },
99
107
  coolingSetpoint: { value: "74" },
100
108
  coolingSetpointType: { value: "num" },
109
+ coolingOn: { value: "74" },
110
+ coolingOnType: { value: "num" },
111
+ heatingOn: { value: "66" },
112
+ heatingOnType: { value: "num" },
101
113
  extent: { value: "1" },
102
114
  swapTime: { value: "300" },
103
115
  swapTimeType: { value: "num" },
@@ -106,7 +118,8 @@
106
118
  maxTempSetpoint: { value: "90" },
107
119
  maxTempSetpointType: { value: "num"},
108
120
  initWindow: { value: "10" },
109
- operationMode: { value: "auto" }
121
+ operationMode: { value: "auto" },
122
+ operationModeType: { value: "dropdown" },
110
123
  },
111
124
  inputs: 1,
112
125
  outputs: 1,
@@ -122,9 +135,37 @@
122
135
  const $algorithm = $("#node-input-algorithm");
123
136
  const $singleFields = $(".single-only");
124
137
  const $splitFields = $(".split-only");
138
+ const $specifiedFields = $(".specified-only");
125
139
 
126
140
  try {
127
141
  // Initialize typed inputs
142
+
143
+ $("#node-input-operationMode").typedInput({
144
+ default: "dropdown",
145
+ types: [{
146
+ value: "dropdown",
147
+ options: [
148
+ { value: "auto", label: "Auto"},
149
+ { value: "heat", label: "Heat"},
150
+ { value: "cool", label: "Cool"},
151
+ ]
152
+ }, "msg", "flow", "global"],
153
+ typeField: "#node-input-operationModeType"
154
+ }).typedInput("type", node.operationModeType).typedInput("value", node.operationMode);
155
+
156
+ $("#node-input-algorithm").typedInput({
157
+ default: "dropdown",
158
+ types: [{
159
+ value: "dropdown",
160
+ options: [
161
+ { value: "single", label: "Single"},
162
+ { value: "split", label: "Split"},
163
+ { value: "specified", label: "Specified"},
164
+ ]
165
+ }, "msg", "flow", "global"],
166
+ typeField: "#node-input-algorithmType"
167
+ }).typedInput("type", node.algorithmType).typedInput("value", node.algorithm);
168
+
128
169
  $("#node-input-setpoint").typedInput({
129
170
  default: "num",
130
171
  types: ["num", "msg", "flow", "global"],
@@ -143,6 +184,18 @@
143
184
  typeField: "#node-input-coolingSetpointType"
144
185
  }).typedInput("type", node.coolingSetpointType || "num").typedInput("value", node.coolingSetpoint);
145
186
 
187
+ $("#node-input-heatingOn").typedInput({
188
+ default: "num",
189
+ types: ["num", "msg", "flow", "global"],
190
+ typeField: "#node-input-heatingOnType"
191
+ }).typedInput("type", node.heatingOnType || "num").typedInput("value", node.heatingOn);
192
+
193
+ $("#node-input-coolingOn").typedInput({
194
+ default: "num",
195
+ types: ["num", "msg", "flow", "global"],
196
+ typeField: "#node-input-coolingOnType"
197
+ }).typedInput("type", node.coolingOnType || "num").typedInput("value", node.coolingOn);
198
+
146
199
  $("#node-input-swapTime").typedInput({
147
200
  default: "num",
148
201
  types: ["num", "msg", "flow", "global"],
@@ -178,9 +231,20 @@
178
231
  if ($algorithm.val() === "single") {
179
232
  $singleFields.show();
180
233
  $splitFields.hide();
181
- } else {
234
+ $specifiedFields.hide();
235
+ } else if ($algorithm.val() === "split") {
182
236
  $singleFields.hide();
183
237
  $splitFields.show();
238
+ $specifiedFields.hide();
239
+ } else if ($algorithm.val() === "specified") {
240
+ $singleFields.hide();
241
+ $splitFields.hide();
242
+ $specifiedFields.show();
243
+ } else {
244
+ $algorithm.val("single");
245
+ $singleFields.show();
246
+ $splitFields.hide();
247
+ $specifiedFields.hide();
184
248
  }
185
249
  }
186
250
 
@@ -188,7 +252,7 @@
188
252
  toggleFields();
189
253
 
190
254
  } catch (err) {
191
- console.error("Error in changeover-block oneditprepare:", err);
255
+ console.error("Error in oneditprepare:", err);
192
256
  }
193
257
  }
194
258
  });
@@ -1,3 +1,5 @@
1
+ //const { parse } = require('echarts/types/src/export/api/time.js');
2
+
1
3
  module.exports = function(RED) {
2
4
  const utils = require('./utils')(RED);
3
5
 
@@ -8,28 +10,24 @@ module.exports = function(RED) {
8
10
  // Initialize runtime state
9
11
  node.runtime = {
10
12
  name: config.name,
11
- algorithm: config.algorithm,
12
- operationMode: config.operationMode,
13
13
  initWindow: parseFloat(config.initWindow),
14
- currentMode: (config.operationMode === "cool" ? "cooling" : "heating"),
15
14
  lastTemperature: null,
16
15
  lastModeChange: 0
17
16
  };
18
17
 
19
- const typedProperties = ['setpoint', 'heatingSetpoint', 'coolingSetpoint', 'swapTime', 'deadband',
20
- 'extent', 'minTempSetpoint', 'maxTempSetpoint'];
21
-
22
18
  // Evaluate typed-input properties
23
- try {
24
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
25
- node.runtime.setpoint = parseFloat(evaluatedValues.setpoint);
26
- node.runtime.heatingSetpoint = parseFloat(evaluatedValues.heatingSetpoint);
27
- node.runtime.coolingSetpoint = parseFloat(evaluatedValues.coolingSetpoint);
28
- node.runtime.swapTime = parseFloat(evaluatedValues.swapTime);
29
- node.runtime.deadband = parseFloat(evaluatedValues.deadband);
30
- node.runtime.extent = parseFloat(evaluatedValues.extent);
31
- node.runtime.minTempSetpoint = parseFloat(evaluatedValues.minTempSetpoint);
32
- node.runtime.maxTempSetpoint = parseFloat(evaluatedValues.maxTempSetpoint);
19
+ try {
20
+ node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node ));
21
+ node.runtime.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node ));
22
+ node.runtime.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node ));
23
+ node.runtime.swapTime = parseFloat(RED.util.evaluateNodeProperty( config.swapTime, config.swapTimeType, node ));
24
+ node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node ));
25
+ node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node ));
26
+ node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node ));
27
+ node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node ));
28
+ node.runtime.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node );
29
+ node.runtime.operationMode = RED.util.evaluateNodeProperty( config.operationMode, config.operationModeType, node );
30
+ node.runtime.currentMode = node.runtime.operationMode === "cool" ? "cooling" : "heating";
33
31
  } catch (err) {
34
32
  node.error(`Error evaluating properties: ${err.message}`);
35
33
  if (done) done();
@@ -53,15 +51,37 @@ module.exports = function(RED) {
53
51
 
54
52
  // Update typed-input properties if needed
55
53
  try {
56
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
57
- node.runtime.setpoint = parseFloat(evaluatedValues.setpoint);
58
- node.runtime.heatingSetpoint = parseFloat(evaluatedValues.heatingSetpoint);
59
- node.runtime.coolingSetpoint = parseFloat(evaluatedValues.coolingSetpoint);
60
- node.runtime.swapTime = parseFloat(evaluatedValues.swapTime);
61
- node.runtime.deadband = parseFloat(evaluatedValues.deadband);
62
- node.runtime.extent = parseFloat(evaluatedValues.extent);
63
- node.runtime.minTempSetpoint = parseFloat(evaluatedValues.minTempSetpoint);
64
- node.runtime.maxTempSetpoint = parseFloat(evaluatedValues.maxTempSetpoint);
54
+ if (utils.requiresEvaluation(config.setpointType)) {
55
+ node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node, msg ));
56
+ }
57
+ if (utils.requiresEvaluation(config.heatingSetpointType)) {
58
+ node.runtime.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node, msg ));
59
+ }
60
+ if (utils.requiresEvaluation(config.coolingSetpointType)) {
61
+ node.runtime.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node, msg ));
62
+ }
63
+ if (utils.requiresEvaluation(config.swapTimeType)) {
64
+ node.runtime.swapTime = parseFloat(RED.util.evaluateNodeProperty( config.swapTime, config.swapTimeType, node, msg ));
65
+ }
66
+ if (utils.requiresEvaluation(config.deadbandType)) {
67
+ node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node, msg ));
68
+ }
69
+ if (utils.requiresEvaluation(config.extentType)) {
70
+ node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node, msg ));
71
+ }
72
+ if (utils.requiresEvaluation(config.minTempSetpointType)) {
73
+ node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node, msg ));
74
+ }
75
+ if (utils.requiresEvaluation(config.maxTempSetpointType)) {
76
+ node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node, msg ));
77
+ }
78
+ if (utils.requiresEvaluation(config.algorithmType)) {
79
+ node.runtime.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node, msg );
80
+ }
81
+ if (utils.requiresEvaluation(config.operationModeType)) {
82
+ node.runtime.operationMode = RED.util.evaluateNodeProperty( config.operationMode, config.operationModeType, node, msg );
83
+ node.runtime.currentMode = node.runtime.operationMode === "cool" ? "cooling" : "heating";
84
+ }
65
85
  } catch (err) {
66
86
  node.error(`Error evaluating properties: ${err.message}`);
67
87
  if (done) done();
@@ -264,9 +284,12 @@ module.exports = function(RED) {
264
284
  if (node.runtime.algorithm === "single") {
265
285
  heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
266
286
  coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
267
- } else {
287
+ } else if (node.runtime.algorithm === "split") {
268
288
  heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
269
289
  coolingThreshold = node.runtime.coolingSetpoint + node.runtime.extent;
290
+ } else if (node.runtime.algorithm === "specified") {
291
+ heatingThreshold = node.runtime.heatingOn - node.runtime.extent;
292
+ coolingThreshold = node.runtime.coolingOn + node.runtime.extent;
270
293
  }
271
294
 
272
295
  if (temp < heatingThreshold) {
@@ -298,9 +321,12 @@ module.exports = function(RED) {
298
321
  if (node.runtime.algorithm === "single") {
299
322
  heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
300
323
  coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
301
- } else {
324
+ } else if (node.runtime.algorithm === "split") {
302
325
  heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
303
326
  coolingThreshold = node.runtime.coolingSetpoint + node.runtime.extent;
327
+ } else if (node.runtime.algorithm === "specified") {
328
+ heatingThreshold = node.runtime.heatingOn - node.runtime.extent;
329
+ coolingThreshold = node.runtime.coolingOn + node.runtime.extent;
304
330
  }
305
331
 
306
332
  let desiredMode = node.runtime.currentMode;
@@ -339,9 +365,12 @@ module.exports = function(RED) {
339
365
  if (node.runtime.algorithm === "single") {
340
366
  effectiveHeatingSetpoint = node.runtime.setpoint - node.runtime.deadband / 2;
341
367
  effectiveCoolingSetpoint = node.runtime.setpoint + node.runtime.deadband / 2;
342
- } else {
368
+ } else if (node.runtime.algorithm === "split") {
343
369
  effectiveHeatingSetpoint = node.runtime.heatingSetpoint;
344
370
  effectiveCoolingSetpoint = node.runtime.coolingSetpoint;
371
+ } else if (node.runtime.algorithm === "specified") {
372
+ effectiveHeatingSetpoint = node.runtime.heatingOn;
373
+ effectiveCoolingSetpoint = node.runtime.coolingOn;
345
374
  }
346
375
 
347
376
  return [
@@ -70,7 +70,7 @@
70
70
  }).typedInput("type", node.delayOffType || "num").typedInput("value", node.delayOff);
71
71
 
72
72
  } catch (err) {
73
- console.error("Error in hysteresis-block oneditprepare:", err);
73
+ console.error("Error in oneditprepare:", err);
74
74
  }
75
75
  }
76
76
  });
@@ -11,13 +11,10 @@ module.exports = function(RED) {
11
11
  desired: false
12
12
  };
13
13
 
14
- const typedProperties = ['delayOn', 'delayOff'];
15
-
16
14
  // Evaluate typed-input properties
17
15
  try {
18
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
19
- node.runtime.delayOn = (parseFloat(evaluatedValues.delayOn)) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
20
- node.runtime.delayOff = (parseFloat(evaluatedValues.delayOff)) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
16
+ node.runtime.delayOn = parseFloat(RED.util.evaluateNodeProperty( config.delayOn, config.delayOnType, node )) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
17
+ node.runtime.delayOff = parseFloat(RED.util.evaluateNodeProperty( config.delayOff, config.delayOffType, node )) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
21
18
  } catch (err) {
22
19
  node.error(`Error evaluating properties: ${err.message}`);
23
20
  }
@@ -33,9 +30,12 @@ module.exports = function(RED) {
33
30
 
34
31
  // Update typed-input properties if needed
35
32
  try {
36
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
37
- node.runtime.delayOn = parseFloat(evaluatedValues.delayOn) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
38
- node.runtime.delayOff = parseFloat(evaluatedValues.delayOff) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
33
+ if (utils.requiresEvaluation(config.delayOnType)) {
34
+ node.runtime.delayOn = parseFloat(RED.util.evaluateNodeProperty( config.delayOn, config.delayOnType, node, msg )) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
35
+ }
36
+ if (utils.requiresEvaluation(config.delayOffType)) {
37
+ node.runtime.delayOff = parseFloat(RED.util.evaluateNodeProperty( config.delayOff, config.delayOffType, node, msg )) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
38
+ }
39
39
  } catch (err) {
40
40
  node.error(`Error evaluating properties: ${err.message}`);
41
41
  if (done) done();
@@ -0,0 +1,157 @@
1
+ <script type="text/html" data-template-name="enum-switch-block">
2
+ <div class="form-row">
3
+ <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Name">
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-input-property" title="Property to evaluate"><i class="fa fa-code"></i> Property</label>
8
+ <input type="text" id="node-input-property" placeholder="property">
9
+ <input type="hidden" id="node-input-propertyType">
10
+ <input type="hidden" id="node-input-outputs">
11
+ </div>
12
+ <div class="form-row">
13
+ <label><i class="fa fa-cogs"></i> Rules</label>
14
+ <div id="node-input-rules-container" style="border: 1px solid #999; padding: 5px; margin: 5px 0; max-height: 200px; overflow-y: auto;">
15
+ <input type="hidden" id="node-input-rules">
16
+ <div class="rule-template" style="display: none;">
17
+ <div class="form-row rule-item" style="margin-bottom: 5px; padding: 5px; border-bottom: 1px solid #eee; display: flex; align-items: center;">
18
+ <input type="text" class="node-input-rule-value" placeholder="value to match" style="flex: 1; margin-right: 5px;">
19
+ <button class="red-ui-button delete-rule" style="width: auto; padding: 0 8px;">
20
+ <i class="fa fa-times"></i>
21
+ </button>
22
+ </div>
23
+ </div>
24
+ <div id="node-input-rules-list"></div>
25
+ <button id="node-input-add-rule" class="red-ui-button" style="width: 100%; margin-top: 5px;">
26
+ <i class="fa fa-plus"></i> Add Rule
27
+ </button>
28
+ </div>
29
+ </div>
30
+ </script>
31
+
32
+ <script type="text/javascript">
33
+ RED.nodes.registerType("enum-switch-block", {
34
+ category: "control",
35
+ color: "#301934",
36
+ defaults: {
37
+ name: { value: "" },
38
+ property: { value: "payload" },
39
+ propertyType: { value: "msg" },
40
+ rules: { value: "[]" },
41
+ outputs: { value: 1 }
42
+ },
43
+ inputs: 1,
44
+ outputs: 1,
45
+ inputLabels: ["input"],
46
+ outputLabels: function(index) {
47
+ try {
48
+ const rules = JSON.parse(this.rules || "[]");
49
+ if (rules[index]) {
50
+ return rules[index].value || `rule ${index + 1}`;
51
+ }
52
+ return "";
53
+ } catch (e) {
54
+ return "";
55
+ }
56
+ },
57
+ icon: "font-awesome/fa-exchange",
58
+ paletteLabel: "enum switch",
59
+ label: function() {
60
+ const rules = JSON.parse(this.rules || "[]");
61
+ return this.name ? this.name : `enum switch (${rules.length} rules)`;
62
+ },
63
+ oneditprepare: function() {
64
+ const node = this;
65
+ const rulesContainer = $("#node-input-rules-list");
66
+ const template = $(".rule-template").clone().removeClass("rule-template").show();
67
+
68
+ // Initialize typed inputs
69
+ $("#node-input-property").typedInput({
70
+ default: "msg",
71
+ types: ["msg", "flow", "global"],
72
+ typeField: "#node-input-propertyType"
73
+ }).typedInput("type", node.propertyType || "msg").typedInput("value", node.property);
74
+
75
+ // Parse existing rules
76
+ let rules = [];
77
+ try {
78
+ rules = JSON.parse(node.rules || "[]");
79
+ } catch (e) {
80
+ rules = [];
81
+ }
82
+
83
+ // Render existing rules
84
+ rules.forEach(function(rule, index) {
85
+ const ruleItem = template.clone();
86
+ ruleItem.find(".node-input-rule-value").val(rule.value || "");
87
+ rulesContainer.append(ruleItem);
88
+ });
89
+
90
+ // Add new rule button click
91
+ $("#node-input-add-rule").on("click", function() {
92
+ const ruleItem = template.clone();
93
+ rulesContainer.append(ruleItem);
94
+ });
95
+
96
+ // Delete rule button click
97
+ rulesContainer.on("click", ".delete-rule", function() {
98
+ $(this).closest(".rule-item").remove();
99
+ });
100
+ },
101
+
102
+ oneditsave: function() {
103
+ const rules = [];
104
+ $("#node-input-rules-list .rule-item").each(function() {
105
+ const value = $(this).find(".node-input-rule-value").val().trim();
106
+ if (value) {
107
+ rules.push({ value: value });
108
+ }
109
+ });
110
+
111
+ $("#node-input-rules").val(JSON.stringify(rules));
112
+ $("#node-input-outputs").val(rules.length);
113
+ },
114
+ oneditresize: function(size) {
115
+ $("#node-input-rules-container").height(size.height - 200);
116
+ }
117
+ });
118
+ </script>
119
+
120
+
121
+ <!-- Help Section -->
122
+ <script type="text/markdown" data-help-name="enum-switch-block">
123
+ Route input to the appropriate output based on switch values.
124
+
125
+ ### Inputs
126
+ : payload (string) : `string` for comparison.
127
+
128
+ ### Outputs
129
+ : payload (boolean) : Boolean output based on matching switch value.
130
+
131
+ ### Properties
132
+ : slots (integer) : Number of input slots (≥ 2).
133
+
134
+ ### Details
135
+ Similar to the NodeRED switch node, but single purpose, with the important difference:
136
+ 1. Outputting boolean based on matching string input.
137
+ 2. Updating all outputs `true`/`false` on each input message. (updating false being the key difference)
138
+
139
+ NodeRED `switch` node only outputs on matching cases, and does not update non-matching outputs unless you create opposing rules for each case,
140
+ which can be messy. Use case is to drive multiple boolean outputs based on a single string input, e.g., controlling visibility of multiple UI elements,
141
+ or routing logic based on a given mode.
142
+
143
+ Supports string, number, and boolean comparisons. Input type is inferred from the incoming message payload type.
144
+
145
+ Preserves original message structure, only modifying the payload to `true` or `false`.
146
+
147
+ ### Status
148
+ - Green (dot): Configuration update
149
+ - Blue (dot): State changed
150
+ - Blue (ring): State unchanged
151
+ - Red (ring): Error
152
+ - Yellow (ring): Warning
153
+
154
+ ### References
155
+ - [Node-RED Documentation](https://nodered.org/docs/)
156
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
157
+ </script>
@@ -0,0 +1,101 @@
1
+ module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
4
+ function EnumSwitchBlockNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // Parse rules from config
9
+ let rules = [];
10
+ try {
11
+ rules = JSON.parse(config.rules || "[]");
12
+ } catch (e) {
13
+ node.error("Invalid rules configuration");
14
+ rules = [];
15
+ }
16
+
17
+ node.on("input", function(msg, send, done) {
18
+ send = send || function() { node.send.apply(node, arguments); };
19
+
20
+ // Guard against invalid msg
21
+ if (!msg) {
22
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
23
+ if (done) done();
24
+ return;
25
+ }
26
+
27
+ let matchAgainst;
28
+
29
+ // Evaluate typed-input properties
30
+ try {
31
+ matchAgainst = RED.util.evaluateNodeProperty( config.property, config.propertyType, node, msg );
32
+
33
+ if (matchAgainst === undefined) {
34
+ node.status({ fill: "red", shape: "ring", text: "property evaluation failed" });
35
+ if (done) done();
36
+ return;
37
+ }
38
+ } catch (err) {
39
+ node.status({ fill: "red", shape: "ring", text: `Error: ${err.message}` });
40
+ if (done) done(err);
41
+ return;
42
+ }
43
+
44
+ const outputs = [];
45
+ let matched = false;
46
+
47
+ // Evaluate each rule and set outputs
48
+ for (let i = 0; i < rules.length; i++) {
49
+ const rule = rules[i];
50
+ let match = false;
51
+
52
+ // Handle different types for comparison
53
+ if (matchAgainst === null || matchAgainst === undefined) {
54
+ match = (rule.value === null || rule.value === undefined || rule.value === "");
55
+ } else if (typeof matchAgainst === 'string' && typeof rule.value === 'string') {
56
+ match = matchAgainst === rule.value;
57
+ } else if (typeof matchAgainst === 'number') {
58
+ const numericRuleValue = parseFloat(rule.value);
59
+ match = !isNaN(numericRuleValue) && matchAgainst === numericRuleValue;
60
+ } else if (typeof matchAgainst === 'boolean') {
61
+ const boolRuleValue = rule.value.toLowerCase() === 'true';
62
+ match = matchAgainst === boolRuleValue;
63
+ } else {
64
+ match = String(matchAgainst) === String(rule.value);
65
+ }
66
+
67
+ outputs[i] = match;
68
+
69
+ if (match) {
70
+ matched = true;
71
+ node.status({ fill: "blue", shape: "dot", text: `Matched: ${rule.value}` });
72
+ }
73
+ }
74
+
75
+ // Send output messages (all outputs as booleans)
76
+ const messages = outputs.map(isMatch => {
77
+ return {
78
+ ...msg,
79
+ payload: isMatch,
80
+ topic: msg.topic
81
+ };
82
+ });
83
+
84
+ send(messages);
85
+
86
+ if (!matched && rules.length > 0) {
87
+ node.status({ fill: "blue", shape: "ring", text: "No match" });
88
+ } else if (rules.length === 0) {
89
+ node.status({ fill: "yellow", shape: "ring", text: "No rules configured" });
90
+ }
91
+
92
+ if (done) done();
93
+ });
94
+
95
+ node.on("close", function(done) {
96
+ if (done) done();
97
+ });
98
+ }
99
+
100
+ RED.nodes.registerType("enum-switch-block", EnumSwitchBlockNode);
101
+ };
@@ -94,7 +94,7 @@ Hysteresis controller with separate turn-on limits and turn-off differentials.
94
94
 
95
95
  ### Inputs
96
96
  : payload (number) : Input value to evaluate
97
- : context (string) : Configure `upperLimit`, `lowerLimit`, `upperLimitThreshold`, `lowerLimitThreshold`
97
+ : context (string) : Configure `upperLimitThreshold`, `lowerLimitThreshold`
98
98
 
99
99
  ### Outputs
100
100
  : above (boolean) : Input > upperLimit