@bldgblocks/node-red-contrib-control 0.2.2 → 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.
- package/nodes/alarm-collector.html +2 -2
- package/nodes/alarm-collector.js +33 -50
- package/nodes/alarm-service.js +1 -1
- package/nodes/boolean-to-number-block.html +13 -4
- package/nodes/boolean-to-number-block.js +6 -2
- package/nodes/call-status-block.js +29 -3
- package/nodes/changeover-block.html +31 -25
- package/nodes/changeover-block.js +26 -7
- package/nodes/comment-block.html +48 -9
- package/nodes/comment-block.js +65 -6
- package/nodes/contextual-label-block.js +3 -2
- package/nodes/convert-block.js +1 -1
- package/nodes/edge-block.html +6 -3
- package/nodes/edge-block.js +4 -3
- package/nodes/enum-switch-block.html +1 -1
- package/nodes/global-setter.html +18 -11
- package/nodes/global-setter.js +62 -45
- package/nodes/hysteresis-block.js +1 -1
- package/nodes/interpolate-block.js +2 -2
- package/nodes/negate-block.js +2 -2
- package/nodes/network-point-register.js +11 -2
- package/nodes/network-service-write.js +7 -11
- package/nodes/on-change-block.js +1 -8
- package/nodes/priority-block.js +6 -6
- package/nodes/rate-limit-block.js +6 -6
- package/nodes/round-block.js +1 -1
- package/nodes/scale-range-block.js +1 -1
- package/nodes/tstat-block.html +21 -13
- package/nodes/tstat-block.js +32 -1
- package/nodes/units-block.js +1 -1
- package/nodes/utils.js +11 -1
- package/package.json +1 -1
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
priority: { value: "high" },
|
|
132
132
|
topic: { value: "Alarms_Default" },
|
|
133
133
|
title: { value: "Alarm" },
|
|
134
|
-
message: { value: "Condition
|
|
134
|
+
message: { value: "Condition active" },
|
|
135
135
|
messageType: { value: "str" },
|
|
136
136
|
tags: { value: "" },
|
|
137
137
|
units: { value: "°F" }
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"msg"
|
|
207
207
|
],
|
|
208
208
|
typeField: "#node-input-messageType"
|
|
209
|
-
}).typedInput("type", node.messageType || "str").typedInput("value", node.message || "Condition
|
|
209
|
+
}).typedInput("type", node.messageType || "str").typedInput("value", node.message || "Condition active");
|
|
210
210
|
|
|
211
211
|
// Show/hide sections based on inputMode
|
|
212
212
|
const updateDisplay = () => {
|
package/nodes/alarm-collector.js
CHANGED
|
@@ -19,7 +19,7 @@ module.exports = function(RED) {
|
|
|
19
19
|
node.priority = config.priority || "normal";
|
|
20
20
|
node.topic = config.topic || "Alarms_Default";
|
|
21
21
|
node.title = config.title || "Alarm";
|
|
22
|
-
node.message = config.message || "Condition
|
|
22
|
+
node.message = config.message || "Condition active";
|
|
23
23
|
node.messageType = config.messageType || "str";
|
|
24
24
|
node.tags = config.tags || "";
|
|
25
25
|
node.units = config.units || "";
|
|
@@ -66,84 +66,65 @@ module.exports = function(RED) {
|
|
|
66
66
|
// Helper: Evaluate alarm condition and emit event if state changed
|
|
67
67
|
// ====================================================================
|
|
68
68
|
function evaluateAndEmit(inputValue) {
|
|
69
|
-
// Evaluate alarm condition based on input mode
|
|
70
69
|
let conditionNowMet = false;
|
|
71
70
|
let numericValue = null;
|
|
72
71
|
|
|
73
72
|
if (node.inputMode === "boolean") {
|
|
74
|
-
// Boolean mode: compare directly with alarmWhenTrue
|
|
75
73
|
conditionNowMet = (inputValue === node.alarmWhenTrue);
|
|
76
74
|
node.currentValue = inputValue;
|
|
77
75
|
} else {
|
|
78
|
-
// Value mode: parse as numeric and check thresholds
|
|
79
76
|
numericValue = inputValue;
|
|
80
77
|
if (typeof inputValue === 'object' && inputValue !== null && inputValue.value !== undefined) {
|
|
81
78
|
numericValue = inputValue.value;
|
|
82
79
|
}
|
|
83
80
|
numericValue = parseFloat(numericValue);
|
|
84
|
-
|
|
81
|
+
|
|
85
82
|
if (isNaN(numericValue)) {
|
|
86
83
|
utils.setStatusError(node, "Invalid numeric input");
|
|
87
84
|
return;
|
|
88
85
|
}
|
|
89
|
-
|
|
86
|
+
|
|
90
87
|
node.currentValue = numericValue;
|
|
91
88
|
|
|
92
|
-
//
|
|
89
|
+
// Schmitt-trigger thresholds: when alarm is already active, use
|
|
90
|
+
// the magnitude-adjusted band so the value must move decisively
|
|
91
|
+
// past the threshold before the alarm will consider clearing.
|
|
92
|
+
const effectiveHigh = node.alarmState
|
|
93
|
+
? (node.highThreshold - node.hysteresisMagnitude)
|
|
94
|
+
: node.highThreshold;
|
|
95
|
+
const effectiveLow = node.alarmState
|
|
96
|
+
? (node.lowThreshold + node.hysteresisMagnitude)
|
|
97
|
+
: node.lowThreshold;
|
|
98
|
+
|
|
93
99
|
if (node.compareMode === "either") {
|
|
94
|
-
conditionNowMet = (numericValue >
|
|
100
|
+
conditionNowMet = (numericValue > effectiveHigh) || (numericValue < effectiveLow);
|
|
95
101
|
} else if (node.compareMode === "high-only") {
|
|
96
|
-
conditionNowMet =
|
|
102
|
+
conditionNowMet = numericValue > effectiveHigh;
|
|
97
103
|
} else if (node.compareMode === "low-only") {
|
|
98
|
-
conditionNowMet =
|
|
104
|
+
conditionNowMet = numericValue < effectiveLow;
|
|
99
105
|
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (node.hysteresisTimer) clearTimeout(node.hysteresisTimer);
|
|
108
|
+
// Single debounce timer: when the condition transitions, wait
|
|
109
|
+
// hysteresisTime before committing the change to alarmState.
|
|
110
|
+
// This filters noise in both the activation and clearing directions.
|
|
111
|
+
if (conditionNowMet !== node.conditionMet) {
|
|
112
|
+
node.conditionMet = conditionNowMet;
|
|
108
113
|
|
|
109
|
-
node.hysteresisTimer = setTimeout(() => {
|
|
110
|
-
if (node.conditionMet && node.alarmState === false) {
|
|
111
|
-
// Condition stayed true for hysteresisTime ms
|
|
112
|
-
node.alarmState = true;
|
|
113
|
-
emitAlarmEvent("false → true");
|
|
114
|
-
}
|
|
115
|
-
node.hysteresisTimer = null;
|
|
116
|
-
}, node.hysteresisTime);
|
|
117
|
-
|
|
118
|
-
} else if (!conditionNowMet && node.conditionMet) {
|
|
119
|
-
// Condition just became false - cancel pending timer
|
|
120
|
-
node.conditionMet = false;
|
|
121
|
-
|
|
122
114
|
if (node.hysteresisTimer) {
|
|
123
115
|
clearTimeout(node.hysteresisTimer);
|
|
124
116
|
node.hysteresisTimer = null;
|
|
125
117
|
}
|
|
126
118
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
shouldClear = false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (node.compareMode === "either" || node.compareMode === "low-only") {
|
|
137
|
-
const clearThreshold = node.lowThreshold + node.hysteresisMagnitude;
|
|
138
|
-
if (numericValue < clearThreshold) {
|
|
139
|
-
shouldClear = false;
|
|
119
|
+
// Start timer only when condition disagrees with alarm state
|
|
120
|
+
if (conditionNowMet !== node.alarmState) {
|
|
121
|
+
node.hysteresisTimer = setTimeout(() => {
|
|
122
|
+
if (node.conditionMet !== node.alarmState) {
|
|
123
|
+
node.alarmState = node.conditionMet;
|
|
124
|
+
emitAlarmEvent(node.alarmState ? "false → true" : "true → false");
|
|
140
125
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (shouldClear && node.alarmState === true) {
|
|
145
|
-
node.alarmState = false;
|
|
146
|
-
emitAlarmEvent("true → false");
|
|
126
|
+
node.hysteresisTimer = null;
|
|
127
|
+
}, node.hysteresisTime);
|
|
147
128
|
}
|
|
148
129
|
}
|
|
149
130
|
|
|
@@ -155,9 +136,11 @@ module.exports = function(RED) {
|
|
|
155
136
|
statusText = `${numericValue.toFixed(2)} ${node.units}`;
|
|
156
137
|
}
|
|
157
138
|
|
|
158
|
-
if (node.alarmState) {
|
|
139
|
+
if (node.alarmState && node.hysteresisTimer) {
|
|
140
|
+
utils.setStatusWarn(node, statusText + " [ALARM clearing...]");
|
|
141
|
+
} else if (node.alarmState) {
|
|
159
142
|
utils.setStatusError(node, statusText + " [ALARM]");
|
|
160
|
-
} else if (node.
|
|
143
|
+
} else if (node.hysteresisTimer) {
|
|
161
144
|
utils.setStatusWarn(node, statusText + " (hysteresis)");
|
|
162
145
|
} else {
|
|
163
146
|
utils.setStatusOK(node, statusText);
|
package/nodes/alarm-service.js
CHANGED
|
@@ -49,7 +49,7 @@ module.exports = function(RED) {
|
|
|
49
49
|
// Send alarm message with status
|
|
50
50
|
const msg = {
|
|
51
51
|
alarm: eventData,
|
|
52
|
-
status: { state: "
|
|
52
|
+
status: { state: "active", transition: eventData.transition },
|
|
53
53
|
activeAlarmCount: activeCount,
|
|
54
54
|
alarmKey: key
|
|
55
55
|
};
|
|
@@ -38,13 +38,14 @@
|
|
|
38
38
|
|
|
39
39
|
<!-- Help Section -->
|
|
40
40
|
<script type="text/markdown" data-help-name="boolean-to-number-block">
|
|
41
|
-
Converts
|
|
41
|
+
Converts between boolean and numeric representations. Booleans are converted to `0`/`1`;
|
|
42
|
+
numeric `0`/`1` values are converted back to `false`/`true`. Also handles `null`.
|
|
42
43
|
|
|
43
44
|
### Inputs
|
|
44
|
-
: input-property (boolean | null) : Value to convert, read from the configured Input Property.
|
|
45
|
+
: input-property (boolean | number | null) : Value to convert, read from the configured Input Property.
|
|
45
46
|
|
|
46
47
|
### Outputs
|
|
47
|
-
: payload (number) : Converted value
|
|
48
|
+
: payload (number | boolean) : Converted value — booleans become `0`/`1`, numbers `0`/`1` become `false`/`true`, `null` becomes `0` or `-1`.
|
|
48
49
|
|
|
49
50
|
### Properties
|
|
50
51
|
: name (string) : Display name in editor.
|
|
@@ -52,9 +53,17 @@ Converts a boolean or null input from a configured property to a numeric output.
|
|
|
52
53
|
: nullToZero (boolean) : When checked, `null` maps to `0`; when unchecked, `null` maps to `-1`.
|
|
53
54
|
|
|
54
55
|
### Details
|
|
55
|
-
|
|
56
|
+
Automatically converts in both directions (read from the configured **Input Property**, default: `msg.payload`):
|
|
57
|
+
|
|
58
|
+
**Boolean → Number:**
|
|
56
59
|
- `true` → `1`
|
|
57
60
|
- `false` → `0`
|
|
61
|
+
|
|
62
|
+
**Number → Boolean:**
|
|
63
|
+
- `1` → `true`
|
|
64
|
+
- `0` → `false`
|
|
65
|
+
|
|
66
|
+
**Null handling:**
|
|
58
67
|
- `null` → `0` (if **Null Mapping** is checked) or `-1` (if unchecked)
|
|
59
68
|
|
|
60
69
|
Output is always written to `msg.payload`. All other input message properties are passed through unchanged.
|
|
@@ -31,11 +31,15 @@ module.exports = function(RED) {
|
|
|
31
31
|
const inputDisplay = inputValue === null ? "null" : String(inputValue);
|
|
32
32
|
if (inputValue === null) {
|
|
33
33
|
msg.payload = node.nullToZero ? 0 : -1;
|
|
34
|
-
utils.setStatusChanged(node,
|
|
34
|
+
utils.setStatusChanged(node, `${inputDisplay} -> ${msg.payload}`);
|
|
35
35
|
send(msg);
|
|
36
36
|
} else if (typeof inputValue === "boolean") {
|
|
37
37
|
msg.payload = inputValue ? 1 : 0;
|
|
38
|
-
utils.setStatusChanged(node,
|
|
38
|
+
utils.setStatusChanged(node, `${inputDisplay} -> ${msg.payload}`);
|
|
39
|
+
send(msg);
|
|
40
|
+
} else if (typeof inputValue === "number" && (inputValue === 0 || inputValue === 1)) {
|
|
41
|
+
msg.payload = inputValue === 1;
|
|
42
|
+
utils.setStatusChanged(node, `${inputDisplay} -> ${msg.payload}`);
|
|
39
43
|
send(msg);
|
|
40
44
|
} else {
|
|
41
45
|
utils.setStatusError(node, "invalid input type");
|
|
@@ -267,9 +267,17 @@ module.exports = function(RED) {
|
|
|
267
267
|
}
|
|
268
268
|
} else {
|
|
269
269
|
// === Call deactivated ===
|
|
270
|
+
// Clear run-related timers and alarms — they are no longer
|
|
271
|
+
// relevant once the call is off.
|
|
270
272
|
if (node.initialStatusTimer) { clearTimeout(node.initialStatusTimer); node.initialStatusTimer = null; }
|
|
271
273
|
if (node.heartbeatTimer) { clearTimeout(node.heartbeatTimer); node.heartbeatTimer = null; }
|
|
272
274
|
if (node.statusLostTimer) { clearTimeout(node.statusLostTimer); node.statusLostTimer = null; }
|
|
275
|
+
if (node.debounceTimer) { clearTimeout(node.debounceTimer); node.debounceTimer = null; }
|
|
276
|
+
|
|
277
|
+
// Clear any "during run" alarm (e.g. status lost) — no longer
|
|
278
|
+
// meaningful now that the call itself is off.
|
|
279
|
+
node.alarm = false;
|
|
280
|
+
node.alarmMessage = "";
|
|
273
281
|
|
|
274
282
|
// Monitor that status goes inactive
|
|
275
283
|
if (node.actualState) {
|
|
@@ -318,16 +326,33 @@ module.exports = function(RED) {
|
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
// If call active and status went false → status lost alarm
|
|
321
|
-
if
|
|
329
|
+
// CRITICAL: Only alarm if we've already received at least one status=true
|
|
330
|
+
// response. And use the configured heartbeat/status timeout as the delay —
|
|
331
|
+
// a brief status dropout should NOT alarm faster than the configured
|
|
332
|
+
// tolerance window. If heartbeat was running, cancel it (we know status
|
|
333
|
+
// is false now) and let the statusLostTimer take over with the same delay.
|
|
334
|
+
if (node.requestedState && !newStatus && node.config.runLostStatus && !node.neverReceivedStatus) {
|
|
335
|
+
// Cancel heartbeat — status is explicitly false, no point monitoring freshness
|
|
336
|
+
if (node.heartbeatTimer) {
|
|
337
|
+
clearTimeout(node.heartbeatTimer);
|
|
338
|
+
node.heartbeatTimer = null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Use heartbeatTimeout as the delay if configured, otherwise statusTimeout.
|
|
342
|
+
// These are the user's configured tolerance windows for status gaps.
|
|
343
|
+
const lostDelay = node.config.heartbeatTimeout > 0
|
|
344
|
+
? node.config.heartbeatTimeout * 1000
|
|
345
|
+
: (node.config.statusTimeout > 0 ? node.config.statusTimeout * 1000 : 5000);
|
|
346
|
+
|
|
322
347
|
node.statusLostTimer = setTimeout(() => {
|
|
323
348
|
node.statusLostTimer = null;
|
|
324
|
-
if (node.requestedState && !node.actualState) {
|
|
349
|
+
if (node.requestedState && !node.actualState && !node.neverReceivedStatus) {
|
|
325
350
|
node.alarm = true;
|
|
326
351
|
node.alarmMessage = node.config.runLostStatusMessage;
|
|
327
352
|
send(buildOutput());
|
|
328
353
|
updateNodeStatus();
|
|
329
354
|
}
|
|
330
|
-
},
|
|
355
|
+
}, lostDelay);
|
|
331
356
|
}
|
|
332
357
|
|
|
333
358
|
// If call inactive and status goes false → all clear
|
|
@@ -341,6 +366,7 @@ module.exports = function(RED) {
|
|
|
341
366
|
}
|
|
342
367
|
|
|
343
368
|
// If status active without call and no clearTimer running → unexpected
|
|
369
|
+
// Skip if clearTimer is active — we're still in grace period after call deactivated
|
|
344
370
|
if (!node.requestedState && newStatus && !node.clearTimer && node.config.statusWithoutCall) {
|
|
345
371
|
node.statusLostTimer = setTimeout(() => {
|
|
346
372
|
node.statusLostTimer = null;
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
<div class="form-row">
|
|
11
11
|
<label for="node-input-algorithm" title="Control algorithm type"><i class="fa fa-cog"></i> Algorithm</label>
|
|
12
|
-
<input type="text" id="node-input-algorithm"
|
|
12
|
+
<input type="text" id="node-input-algorithm" placeholder="single">
|
|
13
13
|
<input type="hidden" id="node-input-algorithmType">
|
|
14
14
|
</div>
|
|
15
15
|
<div class="form-row single-only">
|
|
16
16
|
<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>
|
|
17
|
-
<input type="text" id="node-input-setpoint"
|
|
17
|
+
<input type="text" id="node-input-setpoint" placeholder="70">
|
|
18
18
|
<input type="hidden" id="node-input-setpointType">
|
|
19
19
|
</div>
|
|
20
20
|
<div class="form-row single-only">
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
</div>
|
|
30
30
|
<div class="form-row split-only" style="display: none;">
|
|
31
31
|
<label for="node-input-heatingSetpoint" title="Heating setpoint for split algorithm (number from num, msg, flow, or global)"><i class="fa fa-thermometer-empty"></i> Heating Setpoint</label>
|
|
32
|
-
<input type="text" id="node-input-heatingSetpoint"
|
|
32
|
+
<input type="text" id="node-input-heatingSetpoint" placeholder="68">
|
|
33
33
|
<input type="hidden" id="node-input-heatingSetpointType">
|
|
34
34
|
</div>
|
|
35
35
|
<div class="form-row split-only" style="display: none;">
|
|
36
36
|
<label for="node-input-coolingSetpoint" title="Cooling setpoint for split algorithm (number from num, msg, flow, or global)"><i class="fa fa-thermometer-full"></i> Cooling Setpoint</label>
|
|
37
|
-
<input type="text" id="node-input-coolingSetpoint"
|
|
37
|
+
<input type="text" id="node-input-coolingSetpoint" placeholder="74">
|
|
38
38
|
<input type="hidden" id="node-input-coolingSetpointType">
|
|
39
39
|
</div>
|
|
40
40
|
<div class="form-row split-only" style="display: none;">
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
<div class="form-row">
|
|
68
68
|
<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>
|
|
69
|
-
<input type="text" id="node-input-swapTime"
|
|
69
|
+
<input type="text" id="node-input-swapTime" placeholder="300">
|
|
70
70
|
<input type="hidden" id="node-input-swapTimeType">
|
|
71
71
|
</div>
|
|
72
72
|
<div class="form-row">
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
setpoint: { value: "70" },
|
|
108
108
|
setpointType: { value: "num" },
|
|
109
109
|
deadband: { value: "2" },
|
|
110
|
+
deadbandType: { value: "num" },
|
|
110
111
|
heatingSetpoint: { value: "68" },
|
|
111
112
|
heatingSetpointType: { value: "num" },
|
|
112
113
|
coolingSetpoint: { value: "74" },
|
|
@@ -116,6 +117,7 @@
|
|
|
116
117
|
heatingOn: { value: "66" },
|
|
117
118
|
heatingOnType: { value: "num" },
|
|
118
119
|
extent: { value: "1" },
|
|
120
|
+
extentType: { value: "num" },
|
|
119
121
|
swapTime: { value: "300" },
|
|
120
122
|
swapTimeType: { value: "num" },
|
|
121
123
|
minTempSetpoint: { value: "55" },
|
|
@@ -156,7 +158,7 @@
|
|
|
156
158
|
]
|
|
157
159
|
}, "msg", "flow", "global"],
|
|
158
160
|
typeField: "#node-input-operationModeType"
|
|
159
|
-
})
|
|
161
|
+
});
|
|
160
162
|
|
|
161
163
|
$("#node-input-algorithm").typedInput({
|
|
162
164
|
default: "dropdown",
|
|
@@ -169,87 +171,91 @@
|
|
|
169
171
|
]
|
|
170
172
|
}, "msg", "flow", "global"],
|
|
171
173
|
typeField: "#node-input-algorithmType"
|
|
172
|
-
})
|
|
174
|
+
});
|
|
173
175
|
|
|
174
176
|
$("#node-input-setpoint").typedInput({
|
|
175
177
|
default: "num",
|
|
176
178
|
types: ["num", "msg", "flow", "global"],
|
|
177
179
|
typeField: "#node-input-setpointType"
|
|
178
|
-
})
|
|
180
|
+
});
|
|
179
181
|
|
|
180
182
|
$("#node-input-heatingSetpoint").typedInput({
|
|
181
183
|
default: "num",
|
|
182
184
|
types: ["num", "msg", "flow", "global"],
|
|
183
185
|
typeField: "#node-input-heatingSetpointType"
|
|
184
|
-
})
|
|
186
|
+
});
|
|
185
187
|
|
|
186
188
|
$("#node-input-coolingSetpoint").typedInput({
|
|
187
189
|
default: "num",
|
|
188
190
|
types: ["num", "msg", "flow", "global"],
|
|
189
191
|
typeField: "#node-input-coolingSetpointType"
|
|
190
|
-
})
|
|
192
|
+
});
|
|
191
193
|
|
|
192
194
|
$("#node-input-heatingOn").typedInput({
|
|
193
195
|
default: "num",
|
|
194
196
|
types: ["num", "msg", "flow", "global"],
|
|
195
197
|
typeField: "#node-input-heatingOnType"
|
|
196
|
-
})
|
|
198
|
+
});
|
|
197
199
|
|
|
198
200
|
$("#node-input-coolingOn").typedInput({
|
|
199
201
|
default: "num",
|
|
200
202
|
types: ["num", "msg", "flow", "global"],
|
|
201
203
|
typeField: "#node-input-coolingOnType"
|
|
202
|
-
})
|
|
204
|
+
});
|
|
203
205
|
|
|
204
206
|
$("#node-input-swapTime").typedInput({
|
|
205
207
|
default: "num",
|
|
206
208
|
types: ["num", "msg", "flow", "global"],
|
|
207
209
|
typeField: "#node-input-swapTimeType"
|
|
208
|
-
})
|
|
210
|
+
});
|
|
209
211
|
|
|
210
212
|
$("#node-input-deadband").typedInput({
|
|
211
213
|
default: "num",
|
|
212
214
|
types: ["num", "msg", "flow", "global"],
|
|
213
215
|
typeField: "#node-input-deadbandType"
|
|
214
|
-
})
|
|
216
|
+
});
|
|
215
217
|
|
|
216
218
|
$("#node-input-extent").typedInput({
|
|
217
219
|
default: "num",
|
|
218
220
|
types: ["num", "msg", "flow", "global"],
|
|
219
221
|
typeField: "#node-input-extentType"
|
|
220
|
-
})
|
|
222
|
+
});
|
|
221
223
|
|
|
222
224
|
$("#node-input-minTempSetpoint").typedInput({
|
|
223
225
|
default: "num",
|
|
224
226
|
types: ["num", "msg", "flow", "global"],
|
|
225
227
|
typeField: "#node-input-minTempSetpointType"
|
|
226
|
-
})
|
|
228
|
+
});
|
|
227
229
|
|
|
228
230
|
$("#node-input-maxTempSetpoint").typedInput({
|
|
229
231
|
default: "num",
|
|
230
232
|
types: ["num", "msg", "flow", "global"],
|
|
231
233
|
typeField: "#node-input-maxTempSetpointType"
|
|
232
|
-
})
|
|
234
|
+
});
|
|
233
235
|
|
|
234
236
|
// Toggle fields based on algorithm
|
|
235
237
|
function toggleFields() {
|
|
236
|
-
|
|
238
|
+
const type = $("#node-input-algorithm").typedInput("type");
|
|
239
|
+
if (type !== "dropdown") {
|
|
240
|
+
// Dynamic source — hide all, resolved at runtime
|
|
241
|
+
$singleFields.hide();
|
|
242
|
+
$splitFields.hide();
|
|
243
|
+
$specifiedFields.hide();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const algorithm = $("#node-input-algorithm").typedInput("value");
|
|
247
|
+
if (algorithm === "single") {
|
|
237
248
|
$singleFields.show();
|
|
238
249
|
$splitFields.hide();
|
|
239
250
|
$specifiedFields.hide();
|
|
240
|
-
} else if (
|
|
251
|
+
} else if (algorithm === "split") {
|
|
241
252
|
$singleFields.hide();
|
|
242
253
|
$splitFields.show();
|
|
243
254
|
$specifiedFields.hide();
|
|
244
|
-
} else if (
|
|
255
|
+
} else if (algorithm === "specified") {
|
|
245
256
|
$singleFields.hide();
|
|
246
257
|
$splitFields.hide();
|
|
247
258
|
$specifiedFields.show();
|
|
248
|
-
} else {
|
|
249
|
-
$algorithm.val("single");
|
|
250
|
-
$singleFields.show();
|
|
251
|
-
$splitFields.hide();
|
|
252
|
-
$specifiedFields.hide();
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
@@ -328,6 +328,7 @@ module.exports = function(RED) {
|
|
|
328
328
|
msg.status = {
|
|
329
329
|
mode: node.currentMode,
|
|
330
330
|
operationMode: node.operationMode,
|
|
331
|
+
algorithm: node.algorithm,
|
|
331
332
|
isHeating,
|
|
332
333
|
heatingSetpoint: effectiveHeating,
|
|
333
334
|
coolingSetpoint: effectiveCooling,
|
|
@@ -352,13 +353,31 @@ module.exports = function(RED) {
|
|
|
352
353
|
|
|
353
354
|
const temp = node.lastTemperature !== null ? node.lastTemperature.toFixed(1) : "?";
|
|
354
355
|
const { heating, cooling } = getThresholds();
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
356
|
+
let thresholdText, hysteresisText;
|
|
357
|
+
if (isHeating) {
|
|
358
|
+
thresholdText = `<${cooling.toFixed(1)}`;
|
|
359
|
+
if (node.lastTemperature !== null && node.lastTemperature < cooling) {
|
|
360
|
+
hysteresisText = " (on)";
|
|
361
|
+
} else if (node.lastTemperature !== null && node.lastTemperature >= heating && node.lastTemperature < cooling) {
|
|
362
|
+
hysteresisText = ` (holding, swap at >${heating.toFixed(1)})`;
|
|
363
|
+
} else if (node.lastTemperature !== null && node.lastTemperature >= cooling) {
|
|
364
|
+
hysteresisText = " (off)";
|
|
365
|
+
} else {
|
|
366
|
+
hysteresisText = "";
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
thresholdText = `>${heating.toFixed(1)}`;
|
|
370
|
+
if (node.lastTemperature !== null && node.lastTemperature > heating) {
|
|
371
|
+
hysteresisText = " (on)";
|
|
372
|
+
} else if (node.lastTemperature !== null && node.lastTemperature <= cooling && node.lastTemperature > heating) {
|
|
373
|
+
hysteresisText = ` (holding, swap at <${cooling.toFixed(1)})`;
|
|
374
|
+
} else if (node.lastTemperature !== null && node.lastTemperature <= heating) {
|
|
375
|
+
hysteresisText = " (off)";
|
|
376
|
+
} else {
|
|
377
|
+
hysteresisText = "";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let text = `${temp}° ${thresholdText} [${node.operationMode}] ${node.currentMode}${hysteresisText}`;
|
|
362
381
|
|
|
363
382
|
if (pendingMode && conditionStartTime) {
|
|
364
383
|
const remaining = Math.max(0, node.swapTime - (now - conditionStartTime));
|
package/nodes/comment-block.html
CHANGED
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
<select id="node-input-statusDisplay">
|
|
13
13
|
<option value="default">Comment to status (Default)</option>
|
|
14
14
|
<option value="name">Name to status</option>
|
|
15
|
+
<option value="property">Message property to status</option>
|
|
15
16
|
<option value="none">No status</option>
|
|
16
17
|
</select>
|
|
17
18
|
</div>
|
|
19
|
+
<div class="form-row" id="row-statusProperty" style="display:none;">
|
|
20
|
+
<label for="node-input-statusProperty" title="Message property or JSONata expression to display in status"><i class="fa fa-code"></i> Property</label>
|
|
21
|
+
<input type="text" id="node-input-statusProperty" placeholder="payload" style="width:70%;">
|
|
22
|
+
<input type="hidden" id="node-input-statusPropertyType">
|
|
23
|
+
</div>
|
|
18
24
|
</script>
|
|
19
25
|
|
|
20
26
|
<script type="text/javascript">
|
|
@@ -24,7 +30,9 @@
|
|
|
24
30
|
defaults: {
|
|
25
31
|
name: { value: "comment" },
|
|
26
32
|
comment: { value: "No comment", validate: function(v) { return v.length < 100 } },
|
|
27
|
-
statusDisplay: { value: "default" }
|
|
33
|
+
statusDisplay: { value: "default" },
|
|
34
|
+
statusProperty: { value: "payload" },
|
|
35
|
+
statusPropertyType: { value: "msg" }
|
|
28
36
|
},
|
|
29
37
|
inputs: 1,
|
|
30
38
|
outputs: 1,
|
|
@@ -35,9 +43,31 @@
|
|
|
35
43
|
label: function() {
|
|
36
44
|
return this.name || this.comment || "comment";
|
|
37
45
|
},
|
|
46
|
+
oneditprepare: function() {
|
|
47
|
+
var node = this;
|
|
48
|
+
|
|
49
|
+
$("#node-input-statusProperty").typedInput({
|
|
50
|
+
default: "msg",
|
|
51
|
+
types: ["msg", "jsonata"],
|
|
52
|
+
typeField: "#node-input-statusPropertyType"
|
|
53
|
+
}).typedInput("type", node.statusPropertyType || "msg")
|
|
54
|
+
.typedInput("value", node.statusProperty || "payload");
|
|
55
|
+
|
|
56
|
+
function togglePropertyRow() {
|
|
57
|
+
if ($("#node-input-statusDisplay").val() === "property") {
|
|
58
|
+
$("#row-statusProperty").show();
|
|
59
|
+
} else {
|
|
60
|
+
$("#row-statusProperty").hide();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$("#node-input-statusDisplay").on("change", togglePropertyRow);
|
|
65
|
+
togglePropertyRow();
|
|
66
|
+
},
|
|
38
67
|
oneditsave: function() {
|
|
39
68
|
// Update status immediately
|
|
40
69
|
const node = RED.nodes.getNode(this.id);
|
|
70
|
+
if (!node) return;
|
|
41
71
|
let status = {};
|
|
42
72
|
switch (node.statusDisplay) {
|
|
43
73
|
case "default":
|
|
@@ -46,18 +76,20 @@
|
|
|
46
76
|
case "name":
|
|
47
77
|
status = { fill: "green", shape: "dot", text: node.name || "comment" };
|
|
48
78
|
break;
|
|
79
|
+
case "property":
|
|
80
|
+
status = { fill: "green", shape: "dot", text: "waiting for input" };
|
|
81
|
+
break;
|
|
49
82
|
case "none":
|
|
50
83
|
break;
|
|
51
84
|
default:
|
|
52
85
|
}
|
|
53
|
-
|
|
54
86
|
node.status(status);
|
|
55
87
|
}
|
|
56
88
|
});
|
|
57
89
|
</script>
|
|
58
90
|
|
|
59
91
|
<script type="text/markdown" data-help-name="comment-block">
|
|
60
|
-
Displays a configurable comment, node name, or no status persistently and on input.
|
|
92
|
+
Displays a configurable comment, node name, message property, or no status persistently and on input.
|
|
61
93
|
|
|
62
94
|
### Inputs
|
|
63
95
|
: payload (any) : Passthrough.
|
|
@@ -66,16 +98,23 @@ Displays a configurable comment, node name, or no status persistently and on inp
|
|
|
66
98
|
: payload (any) : Passthrough.
|
|
67
99
|
|
|
68
100
|
### Details
|
|
69
|
-
Displays a status (comment, node name, or none) on node creation, editor saves, and input messages.
|
|
101
|
+
Displays a status (comment, node name, message property, or none) on node creation, editor saves, and input messages.
|
|
102
|
+
|
|
103
|
+
Wire this node inline to observe values flowing through without interrupting the message chain.
|
|
104
|
+
|
|
105
|
+
**Status Display modes:**
|
|
106
|
+
- **Comment to status (Default)**: Shows the configured comment text.
|
|
107
|
+
- **Name to status**: Shows the node's name.
|
|
108
|
+
- **Message property to status**: Evaluates a `msg` property path (e.g., `payload`, `value`, `activePriority`) or a JSONata expression on each input and displays the result.
|
|
109
|
+
- **No status**: Hides the status indicator.
|
|
70
110
|
|
|
71
|
-
|
|
111
|
+
When using **Message property** mode, the status updates on every input message. If the property is not found, a yellow warning is shown.
|
|
72
112
|
|
|
73
113
|
### Status
|
|
74
|
-
- Green (dot):
|
|
75
|
-
- Blue (dot):
|
|
76
|
-
-
|
|
114
|
+
- Green (dot): Static display (comment/name)
|
|
115
|
+
- Blue (dot): Property value displayed
|
|
116
|
+
- Yellow (ring): Property not found
|
|
77
117
|
- Red (ring): Error
|
|
78
|
-
- Yellow (ring): Warning
|
|
79
118
|
|
|
80
119
|
### References
|
|
81
120
|
- [Node-RED Documentation](https://nodered.org/docs/)
|