@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.
@@ -0,0 +1,233 @@
1
+ module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
4
+ function RateOfChangeNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // Initialize runtime state
9
+ node.runtime = {
10
+ maxSamples: parseInt(config.sampleSize),
11
+ samples: [], // Array of {timestamp: Date, value: number}
12
+ units: config.units || "minutes", // minutes, seconds, hours
13
+ lastRate: null
14
+ };
15
+
16
+ // Evaluate typed-input properties
17
+ try {
18
+ node.runtime.minValid = parseFloat(RED.util.evaluateNodeProperty( config.minValid, config.minValidType, node ));
19
+ node.runtime.maxValid = parseFloat(RED.util.evaluateNodeProperty( config.maxValid, config.maxValidType, node ));
20
+ } catch (err) {
21
+ node.error(`Error evaluating properties: ${err.message}`);
22
+ }
23
+
24
+ node.on("input", function(msg, send, done) {
25
+ send = send || function() { node.send.apply(node, arguments); };
26
+
27
+ // Guard against invalid msg
28
+ if (!msg) {
29
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
30
+ if (done) done();
31
+ return;
32
+ }
33
+
34
+ // Update typed-input properties if needed
35
+ try {
36
+ if (utils.requiresEvaluation(config.minValidType)) {
37
+ node.runtime.minValid = parseFloat(RED.util.evaluateNodeProperty( config.minValid, config.minValidType, node, msg ));
38
+ }
39
+ if (utils.requiresEvaluation(config.maxValidType)) {
40
+ node.runtime.maxValid = parseFloat(RED.util.evaluateNodeProperty( config.maxValid, config.maxValidType, node, msg ));
41
+ }
42
+ } catch (err) {
43
+ node.error(`Error evaluating properties: ${err.message}`);
44
+ if (done) done();
45
+ return;
46
+ }
47
+
48
+ // Acceptable fallbacks
49
+ if (isNaN(node.runtime.maxSamples) || node.runtime.maxSamples < 2) {
50
+ node.runtime.maxSamples = 10;
51
+ node.status({ fill: "red", shape: "ring", text: "invalid sample size, using 10" });
52
+ }
53
+
54
+ // Validate values
55
+ if (isNaN(node.runtime.maxValid) || isNaN(node.runtime.minValid) || node.runtime.maxValid <= node.runtime.minValid ) {
56
+ node.status({ fill: "red", shape: "ring", text: `invalid evaluated values ${node.runtime.minValid}, ${node.runtime.maxValid}` });
57
+ if (done) done();
58
+ return;
59
+ }
60
+
61
+ // Handle configuration messages
62
+ if (msg.hasOwnProperty("context")) {
63
+ if (!msg.hasOwnProperty("payload")) {
64
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
65
+ if (done) done();
66
+ return;
67
+ }
68
+
69
+ switch (msg.context) {
70
+ case "reset":
71
+ if (typeof msg.payload !== "boolean") {
72
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
73
+ if (done) done();
74
+ return;
75
+ }
76
+ if (msg.payload === true) {
77
+ node.runtime.samples = [];
78
+ node.runtime.lastRate = null;
79
+ node.status({ fill: "green", shape: "dot", text: "state reset" });
80
+ }
81
+ break;
82
+
83
+ case "sampleSize":
84
+ let newMaxSamples = parseInt(msg.payload);
85
+ if (isNaN(newMaxSamples) || newMaxSamples < 2) {
86
+ node.status({ fill: "red", shape: "ring", text: "sample size must be at least 2" });
87
+ if (done) done();
88
+ return;
89
+ }
90
+ node.runtime.maxSamples = newMaxSamples;
91
+ // Trim samples if new window is smaller
92
+ if (node.runtime.samples.length > newMaxSamples) {
93
+ node.runtime.samples = node.runtime.samples.slice(-newMaxSamples);
94
+ }
95
+ node.status({ fill: "green", shape: "dot", text: `samples: ${newMaxSamples}` });
96
+ break;
97
+
98
+ case "units":
99
+ const validUnits = ["seconds", "minutes", "hours"];
100
+ if (typeof msg.payload === "string" && validUnits.includes(msg.payload.toLowerCase())) {
101
+ node.runtime.units = msg.payload.toLowerCase();
102
+ node.status({ fill: "green", shape: "dot", text: `units: ${msg.payload}` });
103
+ } else {
104
+ node.status({ fill: "red", shape: "ring", text: "invalid units" });
105
+ }
106
+ break;
107
+
108
+ default:
109
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
110
+ break;
111
+ }
112
+ if (done) done();
113
+ return;
114
+ }
115
+
116
+ // Check for missing payload
117
+ if (!msg.hasOwnProperty("payload")) {
118
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
119
+ if (done) done();
120
+ return;
121
+ }
122
+
123
+ // Process input
124
+ const inputValue = parseFloat(msg.payload);
125
+ const timestamp = msg.timestamp ? new Date(msg.timestamp) : new Date();
126
+
127
+ if (isNaN(inputValue) || inputValue < node.runtime.minValid || inputValue > node.runtime.maxValid) {
128
+ node.status({ fill: "yellow", shape: "ring", text: "out of range" });
129
+ if (done) done();
130
+ return;
131
+ }
132
+
133
+ // Add new sample
134
+ node.runtime.samples.push({ timestamp: timestamp, value: inputValue });
135
+
136
+ // Maintain sample window
137
+ if (node.runtime.samples.length > node.runtime.maxSamples + 1) {
138
+ node.runtime.samples = node.runtime.samples.slice(-node.runtime.maxSamples);
139
+ } else if (node.runtime.samples.length > node.runtime.maxSamples) {
140
+ node.runtime.samples.shift();
141
+ }
142
+
143
+ // Calculate rate of change (temperature per time unit)
144
+ let rate = null;
145
+ if (node.runtime.samples.length >= node.runtime.maxSamples) { // Need at least 3 points for good regression
146
+ const n = node.runtime.samples.length;
147
+ let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
148
+
149
+ // Convert timestamps to relative time in the selected units
150
+ const baseTime = node.runtime.samples[0].timestamp;
151
+ let timeScale; // Conversion factor from ms to selected units
152
+
153
+ switch (node.runtime.units) {
154
+ case "seconds":
155
+ timeScale = 1000; // ms to seconds
156
+ break;
157
+ case "minutes":
158
+ timeScale = 1000 * 60; // ms to minutes
159
+ break;
160
+ case "hours":
161
+ timeScale = 1000 * 60 * 60; // ms to hours
162
+ break;
163
+ default:
164
+ timeScale = 1000 * 60; // default to minutes
165
+ }
166
+
167
+ // Calculate regression sums
168
+ node.runtime.samples.forEach((sample, i) => {
169
+ const x = (sample.timestamp - baseTime) / timeScale; // time in selected units
170
+ const y = sample.value;
171
+
172
+ sumX += x;
173
+ sumY += y;
174
+ sumXY += x * y;
175
+ sumXX += x * x;
176
+ });
177
+
178
+ // Calculate slope (rate of change) using linear regression formula
179
+ const denominator = n * sumXX - sumX * sumX;
180
+
181
+ // Avoid division by zero - use original endpoint method if regression fails
182
+ if (Math.abs(denominator) > 1e-10) { // Small tolerance for floating point
183
+ rate = (n * sumXY - sumX * sumY) / denominator;
184
+ } else {
185
+ // Fallback to original endpoint method if regression is unstable
186
+ const firstSample = node.runtime.samples[0];
187
+ const lastSample = node.runtime.samples[node.runtime.samples.length - 1];
188
+ const timeDiff = (lastSample.timestamp - firstSample.timestamp) / timeScale;
189
+ rate = timeDiff > 0 ? (lastSample.value - firstSample.value) / timeDiff : 0;
190
+ }
191
+ }
192
+
193
+ const isUnchanged = rate === node.runtime.lastRate;
194
+
195
+ // Send new message
196
+ const unitsDisplay = {
197
+ seconds: "/sec",
198
+ minutes: "/min",
199
+ hours: "/hr"
200
+ };
201
+
202
+ node.status({
203
+ fill: "blue",
204
+ shape: isUnchanged ? "ring" : "dot",
205
+ text: `rate: ${rate !== null ? rate.toFixed(2) : "not ready"} ${unitsDisplay[node.runtime.units] || "/min"}`
206
+ });
207
+
208
+ node.runtime.lastRate = rate;
209
+
210
+ // Enhanced output with metadata
211
+ const outputMsg = {
212
+ payload: rate,
213
+ samples: node.runtime.samples.length,
214
+ units: `${unitsDisplay[node.runtime.units] || "/min"}`,
215
+ currentValue: inputValue,
216
+ timeSpan: node.runtime.samples.length >= 2 ?
217
+ (node.runtime.samples[node.runtime.samples.length - 1].timestamp - node.runtime.samples[0].timestamp) / 1000 : 0
218
+ };
219
+
220
+ if (node.runtime.samples.length >= node.runtime.maxSamples) {
221
+ send(outputMsg);
222
+ }
223
+
224
+ if (done) done();
225
+ });
226
+
227
+ node.on("close", function(done) {
228
+ done();
229
+ });
230
+ }
231
+
232
+ RED.nodes.registerType("rate-of-change-block", RateOfChangeNode);
233
+ };
@@ -0,0 +1,112 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="string-builder-block">
3
+ <div class="form-row">
4
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
5
+ <input type="text" id="node-input-name" placeholder="Name">
6
+ </div>
7
+ <div class="form-row">
8
+ <label for="node-input-in1"><i class="fa fa-bars"></i> in1</label>
9
+ <input type="text" id="node-input-in1" placeholder="">
10
+ <input type="hidden" id="node-input-in1Type">
11
+ </div>
12
+ <div class="form-row">
13
+ <label for="node-input-in2"><i class="fa fa-bars"></i> in2</label>
14
+ <input type="text" id="node-input-in2" placeholder="">
15
+ <input type="hidden" id="node-input-in2Type">
16
+ </div>
17
+ <div class="form-row">
18
+ <label for="node-input-in3"><i class="fa fa-bars"></i> in3</label>
19
+ <input type="text" id="node-input-in3" placeholder="">
20
+ <input type="hidden" id="node-input-in3Type">
21
+ </div>
22
+ <div class="form-row">
23
+ <label for="node-input-in4"><i class="fa fa-bars"></i> in4</label>
24
+ <input type="text" id="node-input-in4" placeholder="">
25
+ <input type="hidden" id="node-input-in4Type">
26
+ </div>
27
+ </script>
28
+
29
+
30
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
31
+ <script type="text/javascript">
32
+ RED.nodes.registerType("string-builder-block", {
33
+ category: "control",
34
+ color: "#301934",
35
+ defaults: {
36
+ name: { value: "" },
37
+ in1: { value: "", required: false },
38
+ in1Type: { value: "str" },
39
+ in2: { value: "", required: false },
40
+ in2Type: { value: "str" },
41
+ in3: { value: "", required: false },
42
+ in3Type: { value: "str" },
43
+ in4: { value: "", required: false },
44
+ in4Type: { value: "str" },
45
+ },
46
+ inputs: 1,
47
+ outputs: 1,
48
+ inputLabels: ["input"],
49
+ outputLabels: ["output"],
50
+ icon: "font-awesome/fa-bars",
51
+ paletteLabel: "string builder",
52
+ label: function() {
53
+ return this.name || "string builder";
54
+ },
55
+ oneditprepare: function() {
56
+ const node = this;
57
+
58
+ try {
59
+ // Initialize typed inputs
60
+ $("#node-input-in1").typedInput({
61
+ default: "str",
62
+ types: ["str", "msg", "flow", "global"],
63
+ typeField: "#node-input-in1Type"
64
+ }).typedInput("type", node.in1Type || "str").typedInput("value", node.in1);
65
+
66
+ $("#node-input-in2").typedInput({
67
+ default: "str",
68
+ types: ["str", "msg", "flow", "global"],
69
+ typeField: "#node-input-in2Type"
70
+ }).typedInput("type", node.in2Type || "str").typedInput("value", node.in2);
71
+
72
+ $("#node-input-in3").typedInput({
73
+ default: "str",
74
+ types: ["str", "msg", "flow", "global"],
75
+ typeField: "#node-input-in3Type"
76
+ }).typedInput("type", node.in3Type || "str").typedInput("value", node.in3);
77
+
78
+ $("#node-input-in4").typedInput({
79
+ default: "str",
80
+ types: ["str", "msg", "flow", "global"],
81
+ typeField: "#node-input-in4Type"
82
+ }).typedInput("type", node.in4Type || "str").typedInput("value", node.in4);
83
+
84
+ } catch (err) {
85
+ console.error("Error in oneditprepare:", err);
86
+ }
87
+ }
88
+ });
89
+ </script>
90
+
91
+ <!-- Help Section -->
92
+ <script type="text/markdown" data-help-name="string-builder-block">
93
+ Concatenates multiple string inputs into a single output string on any input.
94
+
95
+ ### Inputs
96
+ : payload (number) : Input value to evaluate
97
+ : context (string) : Configure `inX` string inputs via context variables
98
+
99
+ ### Outputs
100
+ : output (string) : Each string input concatenated in order
101
+
102
+ ### Status
103
+ - Green (dot): Configuration update
104
+ - Blue (dot): State changed
105
+ - Blue (ring): State unchanged
106
+ - Red (ring): Error
107
+ - Yellow (ring): Warning
108
+
109
+ ### References
110
+ - [Node-RED Documentation](https://nodered.org/docs/)
111
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
112
+ </script>
@@ -0,0 +1,89 @@
1
+ module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
4
+ function StringBuilderBlockNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+ node.name = config.name;
8
+
9
+ // Evaluate typed-input properties
10
+ try {
11
+ node.in1 = RED.util.evaluateNodeProperty( config.in1, config.in1Type, node );
12
+ node.in2 = RED.util.evaluateNodeProperty( config.in2, config.in2Type, node );
13
+ node.in3 = RED.util.evaluateNodeProperty( config.in3, config.in3Type, node );
14
+ node.in4 = RED.util.evaluateNodeProperty( config.in4, config.in4Type, node );
15
+ } catch (err) {
16
+ node.error(`Error evaluating properties: ${err.message}`);
17
+ }
18
+
19
+ node.on("input", function(msg, send, done) {
20
+ send = send || function() { node.send.apply(node, arguments); };
21
+
22
+ if (!msg) {
23
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
24
+ if (done) done();
25
+ return;
26
+ }
27
+
28
+ // Update typed-input properties if needed
29
+ try {
30
+ if (utils.requiresEvaluation(config.in1Type)) {
31
+ node.in1 = RED.util.evaluateNodeProperty( config.in1, config.in1Type, node, msg );
32
+ }
33
+ if (utils.requiresEvaluation(config.in2Type)) {
34
+ node.in2 = RED.util.evaluateNodeProperty( config.in2, config.in2Type, node, msg );
35
+ }
36
+ if (utils.requiresEvaluation(config.in3Type)) {
37
+ node.in3 = RED.util.evaluateNodeProperty( config.in3, config.in3Type, node, msg );
38
+ }
39
+ if (utils.requiresEvaluation(config.in4Type)) {
40
+ node.in4 = RED.util.evaluateNodeProperty( config.in4, config.in4Type, node, msg );
41
+ }
42
+ } catch (err) {
43
+ node.error(`Error evaluating properties: ${err.message}`);
44
+ if (done) done();
45
+ return;
46
+ }
47
+
48
+ // Check required properties
49
+ if (msg.hasOwnProperty("context")) {
50
+
51
+ if (!msg.hasOwnProperty("payload")) {
52
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
53
+ if (done) done();
54
+ return;
55
+ }
56
+
57
+ // Process input slot
58
+ if (msg.context.startsWith("in")) {
59
+ let index = parseInt(msg.context.slice(2), 10);
60
+ if (!isNaN(index) && index >= 1 && index <= 4) {
61
+ if (config[`in${index}Type`] === "str") {
62
+ node[`in${index}`] = msg.payload;
63
+ } else {
64
+ node.status({ fill: "red", shape: "ring", text: `Field type is ${config[`in${index}Type`]}` });
65
+ if (done) done();
66
+ return;
67
+ }
68
+ } else {
69
+ node.status({ fill: "red", shape: "ring", text: `invalid input index ${index || "NaN"}` });
70
+ if (done) done();
71
+ return;
72
+ }
73
+ }
74
+ }
75
+
76
+ const output = { payload: `${node.in1}${node.in2}${node.in3}${node.in4}` };
77
+ node.status({ fill: "blue", shape: "dot", text: `${ output.payload }` });
78
+ send(output);
79
+
80
+ if (done) done();
81
+ });
82
+
83
+ node.on("close", function(done) {
84
+ done();
85
+ });
86
+ }
87
+
88
+ RED.nodes.registerType("string-builder-block", StringBuilderBlockNode);
89
+ };
@@ -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`.