@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,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
  });
@@ -10,10 +10,7 @@ module.exports = function(RED) {
10
10
  // Initialize runtime state
11
11
  node.runtime = {
12
12
  name: config.name,
13
- algorithm: config.algorithm,
14
- operationMode: config.operationMode,
15
13
  initWindow: parseFloat(config.initWindow),
16
- currentMode: (config.operationMode === "cool" ? "cooling" : "heating"),
17
14
  lastTemperature: null,
18
15
  lastModeChange: 0
19
16
  };
@@ -27,7 +24,10 @@ module.exports = function(RED) {
27
24
  node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node ));
28
25
  node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node ));
29
26
  node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node ));
30
- node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, 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";
31
31
  } catch (err) {
32
32
  node.error(`Error evaluating properties: ${err.message}`);
33
33
  if (done) done();
@@ -74,7 +74,14 @@ module.exports = function(RED) {
74
74
  }
75
75
  if (utils.requiresEvaluation(config.maxTempSetpointType)) {
76
76
  node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node, msg ));
77
- }
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
+ }
78
85
  } catch (err) {
79
86
  node.error(`Error evaluating properties: ${err.message}`);
80
87
  if (done) done();
@@ -277,9 +284,12 @@ module.exports = function(RED) {
277
284
  if (node.runtime.algorithm === "single") {
278
285
  heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
279
286
  coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
280
- } else {
287
+ } else if (node.runtime.algorithm === "split") {
281
288
  heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
282
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;
283
293
  }
284
294
 
285
295
  if (temp < heatingThreshold) {
@@ -311,9 +321,12 @@ module.exports = function(RED) {
311
321
  if (node.runtime.algorithm === "single") {
312
322
  heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
313
323
  coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
314
- } else {
324
+ } else if (node.runtime.algorithm === "split") {
315
325
  heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
316
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;
317
330
  }
318
331
 
319
332
  let desiredMode = node.runtime.currentMode;
@@ -352,9 +365,12 @@ module.exports = function(RED) {
352
365
  if (node.runtime.algorithm === "single") {
353
366
  effectiveHeatingSetpoint = node.runtime.setpoint - node.runtime.deadband / 2;
354
367
  effectiveCoolingSetpoint = node.runtime.setpoint + node.runtime.deadband / 2;
355
- } else {
368
+ } else if (node.runtime.algorithm === "split") {
356
369
  effectiveHeatingSetpoint = node.runtime.heatingSetpoint;
357
370
  effectiveCoolingSetpoint = node.runtime.coolingSetpoint;
371
+ } else if (node.runtime.algorithm === "specified") {
372
+ effectiveHeatingSetpoint = node.runtime.heatingOn;
373
+ effectiveCoolingSetpoint = node.runtime.coolingOn;
358
374
  }
359
375
 
360
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
  });
@@ -84,7 +84,7 @@ module.exports = function(RED) {
84
84
 
85
85
  // Add validation to ensure numbers
86
86
  if (isNaN(upperTurnOn) || isNaN(upperTurnOff) || isNaN(lowerTurnOn) || isNaN(lowerTurnOff)) {
87
- node.status({ fill: "red", shape: "ring", text: "invalid boundary calculation" });
87
+ node.status({ fill: "red", shape: "ring", text: "invalid limits calculation" });
88
88
  if (done) done();
89
89
  return;
90
90
  }
@@ -0,0 +1,55 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="latch-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
+ </script>
8
+
9
+ <!-- JavaScript Section -->
10
+ <script type="text/javascript">
11
+ RED.nodes.registerType("latch-block", {
12
+ category: "control",
13
+ color: "#301934",
14
+ defaults: {
15
+ name: { value: "" },
16
+ state: { value: false }
17
+ },
18
+ inputs: 1,
19
+ outputs: 1,
20
+ inputLabels: ["input"],
21
+ outputLabels: ["output"],
22
+ icon: "font-awesome/fa-toggle-on",
23
+ paletteLabel: "latch",
24
+ label: function() {
25
+ return this.name || "latch";
26
+ }
27
+ });
28
+ </script>
29
+
30
+ <!-- Help Section -->
31
+ <script type="text/markdown" data-help-name="latch-block">
32
+ Latch output based on control messages.
33
+
34
+ ### Inputs
35
+ : context (string) : Configuration commands (`set`, `reset`).
36
+ : payload (boolean) : `true` to set latch, `false` to reset latch when paired with appropriate `context`.
37
+
38
+ ### Outputs
39
+ : output (msg) : `msg.payload` `true` or `false` based on latch state.
40
+
41
+ ### Details
42
+ Set or reset the latch state based on input messages. `msg.context` = `"set"` with `msg.payload` = `true`
43
+ sets the latch `true`, while `msg.context` = `"reset"` with `msg.payload` = `true` resets it to `false`.
44
+
45
+ ### Status
46
+ - Green (dot): Configuration update
47
+ - Blue (dot): State changed
48
+ - Blue (ring): State unchanged
49
+ - Red (ring): Error
50
+ - Yellow (ring): Warning
51
+
52
+ ### References
53
+ - [Node-RED Documentation](https://nodered.org/docs/)
54
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
55
+ </script>
@@ -0,0 +1,77 @@
1
+ module.exports = function(RED) {
2
+ function LatchBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+
5
+ const node = this;
6
+
7
+ // Initialize state from config
8
+ node.state = config.state;
9
+
10
+ // Set initial status
11
+ node.status({
12
+ fill: "green",
13
+ shape: "dot",
14
+ text: `state: ${node.state}`
15
+ });
16
+
17
+ node.on("input", function(msg, send, done) {
18
+ send = send || function() { node.send.apply(node, arguments); };
19
+
20
+ // Guard against invalid message
21
+ if (!msg) {
22
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
23
+ if (done) done();
24
+ return;
25
+ }
26
+
27
+ // Validate context
28
+ if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
29
+ node.status({ fill: "red", shape: "ring", text: "missing or invalid context" });
30
+ if (done) done();
31
+ return;
32
+ }
33
+
34
+ // Handle context commands
35
+ switch (msg.context) {
36
+ case "set":
37
+ if (node.state) {
38
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
39
+ } else {
40
+ if (msg.payload) {
41
+ node.state = true;
42
+ node.status({ fill: "blue", shape: "dot", text: `state: ${node.state}` });
43
+ } else {
44
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
45
+ }
46
+ }
47
+ // Output latch value regardless
48
+ send({ payload: node.state });
49
+ break;
50
+ case "reset":
51
+ if (node.state === false) {
52
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
53
+ } else {
54
+ if (msg.payload) {
55
+ node.state = false;
56
+ node.status({ fill: "blue", shape: "dot", text: `state: ${node.state}` });
57
+ } else {
58
+ node.status({ fill: "blue", shape: "ring", text: `state: ${node.state}` });
59
+ }
60
+ }
61
+ send({ payload: node.state });
62
+ break;
63
+ default:
64
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
65
+ if (done) done("Unknown context");
66
+ return;
67
+ }
68
+ if (done) done();
69
+ });
70
+
71
+ node.on("close", function(done) {
72
+ done();
73
+ });
74
+ }
75
+
76
+ RED.nodes.registerType("latch-block", LatchBlockNode);
77
+ };
@@ -64,7 +64,6 @@ differs from the last output value. When `period > 0`, outputs the first message
64
64
  Supports complex payloads (objects, arrays) via deep comparison.
65
65
  Configuration
66
66
  - `msg.context = "period"` Sets period (ms), no output.
67
- - `msg.context = "status"` Outputs `{ period, periodType }`.
68
67
 
69
68
  ### Status
70
69
  - Green (dot): Configuration update
@@ -70,16 +70,6 @@ module.exports = function(RED) {
70
70
  if (done) done();
71
71
  return;
72
72
  }
73
- if (msg.context === "status") {
74
- send({
75
- payload: {
76
- period: node.runtime.period,
77
- periodType: node.runtime.periodType
78
- }
79
- });
80
- if (done) done();
81
- return;
82
- }
83
73
  // Ignore unknown context
84
74
  }
85
75
 
@@ -115,7 +105,7 @@ module.exports = function(RED) {
115
105
  node.status({
116
106
  fill: "blue",
117
107
  shape: "ring",
118
- text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)}`
108
+ text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)} |`
119
109
  });
120
110
  if (done) done();
121
111
  return;
@@ -139,7 +129,7 @@ module.exports = function(RED) {
139
129
  node.status({
140
130
  fill: "blue",
141
131
  shape: "ring",
142
- text: `Filter period expired`
132
+ text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)}` // remove ' |' to indicate end of filter period
143
133
  });
144
134
  }, node.runtime.period);
145
135
  }