@bldgblocks/node-red-contrib-control 0.1.33 → 0.1.36
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/accumulate-block.html +18 -8
- package/nodes/accumulate-block.js +39 -44
- package/nodes/add-block.html +1 -1
- package/nodes/add-block.js +18 -11
- package/nodes/alarm-collector.html +260 -0
- package/nodes/alarm-collector.js +292 -0
- package/nodes/alarm-config.html +129 -0
- package/nodes/alarm-config.js +126 -0
- package/nodes/alarm-service.html +96 -0
- package/nodes/alarm-service.js +142 -0
- package/nodes/analog-switch-block.js +25 -36
- package/nodes/and-block.js +44 -15
- package/nodes/average-block.js +46 -41
- package/nodes/boolean-switch-block.js +10 -28
- package/nodes/boolean-to-number-block.html +18 -5
- package/nodes/boolean-to-number-block.js +24 -16
- package/nodes/cache-block.js +24 -37
- package/nodes/call-status-block.html +91 -32
- package/nodes/call-status-block.js +398 -115
- package/nodes/changeover-block.html +5 -0
- package/nodes/changeover-block.js +167 -162
- package/nodes/comment-block.html +1 -1
- package/nodes/comment-block.js +14 -9
- package/nodes/compare-block.html +14 -4
- package/nodes/compare-block.js +23 -18
- package/nodes/contextual-label-block.html +5 -0
- package/nodes/contextual-label-block.js +6 -16
- package/nodes/convert-block.html +25 -39
- package/nodes/convert-block.js +31 -16
- package/nodes/count-block.html +11 -5
- package/nodes/count-block.js +34 -32
- package/nodes/delay-block.js +58 -53
- package/nodes/divide-block.js +43 -45
- package/nodes/edge-block.html +17 -10
- package/nodes/edge-block.js +43 -41
- package/nodes/enum-switch-block.js +6 -6
- package/nodes/frequency-block.html +6 -1
- package/nodes/frequency-block.js +64 -74
- package/nodes/global-getter.html +51 -15
- package/nodes/global-getter.js +74 -67
- package/nodes/global-setter.html +1 -1
- package/nodes/global-setter.js +168 -188
- package/nodes/history-buffer.html +96 -0
- package/nodes/history-buffer.js +461 -0
- package/nodes/history-collector.html +29 -1
- package/nodes/history-collector.js +37 -16
- package/nodes/history-config.html +13 -1
- package/nodes/history-service.html +84 -0
- package/nodes/history-service.js +52 -0
- package/nodes/hysteresis-block.html +5 -0
- package/nodes/hysteresis-block.js +13 -16
- package/nodes/interpolate-block.html +20 -2
- package/nodes/interpolate-block.js +39 -50
- package/nodes/join.html +78 -0
- package/nodes/join.js +78 -0
- package/nodes/latch-block.js +12 -14
- package/nodes/load-sequence-block.js +102 -110
- package/nodes/max-block.js +26 -26
- package/nodes/memory-block.js +57 -58
- package/nodes/min-block.js +26 -25
- package/nodes/minmax-block.js +35 -34
- package/nodes/modulo-block.js +45 -43
- package/nodes/multiply-block.js +43 -41
- package/nodes/negate-block.html +17 -7
- package/nodes/negate-block.js +25 -19
- package/nodes/network-point-read.html +128 -0
- package/nodes/network-point-read.js +230 -0
- package/nodes/{network-register.html → network-point-register.html} +94 -7
- package/nodes/network-point-register.js +126 -0
- package/nodes/network-point-write.html +149 -0
- package/nodes/network-point-write.js +222 -0
- package/nodes/network-service-bridge.html +131 -0
- package/nodes/network-service-bridge.js +376 -0
- package/nodes/network-service-read.html +81 -0
- package/nodes/network-service-read.js +58 -0
- package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
- package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
- package/nodes/network-service-write.html +89 -0
- package/nodes/network-service-write.js +83 -0
- package/nodes/nullify-block.js +13 -15
- package/nodes/on-change-block.html +17 -9
- package/nodes/on-change-block.js +49 -46
- package/nodes/oneshot-block.html +13 -10
- package/nodes/oneshot-block.js +57 -75
- package/nodes/or-block.js +44 -15
- package/nodes/pid-block.html +54 -4
- package/nodes/pid-block.js +459 -248
- package/nodes/priority-block.js +24 -35
- package/nodes/rate-limit-block.js +70 -72
- package/nodes/rate-of-change-block.html +33 -14
- package/nodes/rate-of-change-block.js +74 -62
- package/nodes/round-block.html +14 -9
- package/nodes/round-block.js +32 -25
- package/nodes/saw-tooth-wave-block.js +49 -76
- package/nodes/scale-range-block.html +12 -6
- package/nodes/scale-range-block.js +46 -39
- package/nodes/sine-wave-block.js +49 -57
- package/nodes/string-builder-block.js +6 -6
- package/nodes/subtract-block.js +38 -34
- package/nodes/thermistor-block.js +44 -44
- package/nodes/tick-tock-block.js +32 -32
- package/nodes/time-sequence-block.js +30 -42
- package/nodes/triangle-wave-block.js +49 -69
- package/nodes/tstat-block.js +34 -44
- package/nodes/units-block.html +90 -69
- package/nodes/units-block.js +22 -30
- package/nodes/utils.js +275 -3
- package/package.json +14 -6
- package/nodes/network-read.html +0 -56
- package/nodes/network-read.js +0 -59
- package/nodes/network-register.js +0 -161
- package/nodes/network-write.html +0 -64
- package/nodes/network-write.js +0 -126
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
4
4
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
5
5
|
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
|
|
8
|
+
<input type="text" id="node-input-inputProperty" placeholder="payload">
|
|
9
|
+
</div>
|
|
6
10
|
<div class="form-row">
|
|
7
11
|
<label for="node-input-mode" title="Select accumulation mode"><i class="fa fa-cog"></i> Mode</label>
|
|
8
12
|
<select id="node-input-mode">
|
|
@@ -19,6 +23,7 @@
|
|
|
19
23
|
color: "#301934",
|
|
20
24
|
defaults: {
|
|
21
25
|
name: { value: "" },
|
|
26
|
+
inputProperty: { value: "payload" },
|
|
22
27
|
mode: { value: "true", required: true }
|
|
23
28
|
},
|
|
24
29
|
inputs: 1,
|
|
@@ -40,22 +45,27 @@
|
|
|
40
45
|
</script>
|
|
41
46
|
|
|
42
47
|
<script type="text/markdown" data-help-name="accumulate-block">
|
|
43
|
-
Counts consecutive inputs based on the selected mode, resetting on specific conditions.
|
|
48
|
+
Counts consecutive inputs from a configured property based on the selected mode, resetting on specific conditions.
|
|
44
49
|
|
|
45
50
|
### Inputs
|
|
46
|
-
:
|
|
47
|
-
:
|
|
51
|
+
: input-property (boolean, for true/false modes) : Input value to evaluate, read from the configured Input Property.
|
|
52
|
+
: context (string) : Configuration commands (`"reset"` to clear count).
|
|
48
53
|
|
|
49
54
|
### Outputs
|
|
50
55
|
: payload (number) : Current count based on mode.
|
|
51
56
|
|
|
57
|
+
### Properties
|
|
58
|
+
: name (string) : Display name in editor.
|
|
59
|
+
: inputProperty (string) : Message property to read input from (default: `payload`). Supports nested properties (e.g., `data.value`).
|
|
60
|
+
: mode (string) : Accumulation mode (`"true"`, `"false"`, `"flows"`).
|
|
61
|
+
|
|
52
62
|
### Details
|
|
53
|
-
Counts inputs according to the selected mode
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
63
|
+
Counts inputs according to the selected mode, reading from the configured **Input Property** (default: `msg.payload`):
|
|
64
|
+
- **Accumulate True**: counts consecutive `true` values (resets on `false` or explicit reset)
|
|
65
|
+
- **Accumulate False**: counts consecutive `false` values (resets on `true` or explicit reset)
|
|
66
|
+
- **Accumulate Flows**: counts all valid input messages (resets only on explicit reset)
|
|
57
67
|
|
|
58
|
-
Reset via `msg.context = "reset"` with `msg.payload = true
|
|
68
|
+
Reset via `msg.context = "reset"` with `msg.payload = true`.
|
|
59
69
|
|
|
60
70
|
Used to track sustained events or message frequency.
|
|
61
71
|
|
|
@@ -1,95 +1,90 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require("./utils")(RED);
|
|
3
|
+
|
|
2
4
|
function AccumulateBlockNode(config) {
|
|
3
5
|
RED.nodes.createNode(this, config);
|
|
4
6
|
const node = this;
|
|
5
7
|
|
|
6
8
|
// Initialize runtime state
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// Initialize state
|
|
10
|
+
node.name = config.name;
|
|
11
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
12
|
+
node.mode = config.mode;
|
|
13
|
+
node.count = 0;
|
|
14
|
+
node.lastCount = null;
|
|
13
15
|
|
|
14
16
|
// Set initial status
|
|
15
|
-
|
|
16
|
-
fill: "green",
|
|
17
|
-
shape: "dot",
|
|
18
|
-
text: `mode: ${node.runtime.mode}, name: ${node.runtime.name || node.runtime.mode + " accumulate"}`
|
|
19
|
-
});
|
|
17
|
+
utils.setStatusOK(node, `mode: ${node.mode}, name: ${node.name || node.mode + " accumulate"}`);
|
|
20
18
|
|
|
21
19
|
node.on("input", function(msg, send, done) {
|
|
22
20
|
send = send || function() { node.send.apply(node, arguments); };
|
|
23
21
|
|
|
24
22
|
// Guard against invalid msg
|
|
25
23
|
if (!msg) {
|
|
26
|
-
|
|
24
|
+
utils.setStatusError(node, "missing message");
|
|
27
25
|
node.warn("Missing message");
|
|
28
26
|
if (done) done();
|
|
29
27
|
return;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
// Handle reset command with intentional payload requirement
|
|
33
30
|
if (msg.context === "reset") {
|
|
34
31
|
if (msg.payload === true) {
|
|
35
|
-
node.
|
|
36
|
-
|
|
32
|
+
node.count = 0;
|
|
33
|
+
utils.setStatusWarn(node, "reset");
|
|
37
34
|
if (done) done();
|
|
38
35
|
return;
|
|
39
36
|
}
|
|
40
|
-
// payload !== true: treat as normal message, don't reset
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
// Process input based on mode
|
|
44
|
-
if (node.
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
if (node.mode !== "flows") {
|
|
41
|
+
// Get input value from configured property
|
|
42
|
+
let inputValue;
|
|
43
|
+
try {
|
|
44
|
+
inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
inputValue = undefined;
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
node.status({ fill: "red", shape: "ring", text: "invalid input" });
|
|
57
|
-
node.warn("Invalid input: non-boolean payload");
|
|
48
|
+
const boolVal = utils.validateBoolean(inputValue);
|
|
49
|
+
if (!boolVal.valid) {
|
|
50
|
+
utils.setStatusError(node, boolVal.error);
|
|
51
|
+
node.warn("Invalid input: non-boolean value");
|
|
58
52
|
if (done) done();
|
|
59
53
|
return;
|
|
60
54
|
}
|
|
55
|
+
inputValue = boolVal.value;
|
|
61
56
|
|
|
62
|
-
// Prevent extended time running
|
|
63
|
-
if (node.
|
|
64
|
-
node.
|
|
57
|
+
// Prevent extended time running issues
|
|
58
|
+
if (node.count > 9999) {
|
|
59
|
+
node.count = 0;
|
|
65
60
|
}
|
|
66
61
|
|
|
67
62
|
// Accumulate or reset count
|
|
68
|
-
if (node.
|
|
63
|
+
if (node.mode === "true") {
|
|
69
64
|
if (inputValue === true) {
|
|
70
|
-
node.
|
|
65
|
+
node.count++;
|
|
71
66
|
} else {
|
|
72
|
-
node.
|
|
67
|
+
node.count = 0;
|
|
73
68
|
}
|
|
74
|
-
} else if (node.
|
|
69
|
+
} else if (node.mode === "false") {
|
|
75
70
|
if (inputValue === false) {
|
|
76
|
-
node.
|
|
71
|
+
node.count++;
|
|
77
72
|
} else {
|
|
78
|
-
node.
|
|
73
|
+
node.count = 0;
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
76
|
} else {
|
|
82
77
|
// flows mode: count all valid messages
|
|
83
|
-
node.
|
|
78
|
+
node.count++;
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
// Output only if count changed
|
|
87
|
-
if (node.
|
|
88
|
-
node.
|
|
89
|
-
|
|
90
|
-
send({ payload: node.
|
|
82
|
+
if (node.lastCount !== node.count) {
|
|
83
|
+
node.lastCount = node.count;
|
|
84
|
+
utils.setStatusChanged(node, `out: ${node.count}`);
|
|
85
|
+
send({ payload: node.count });
|
|
91
86
|
} else {
|
|
92
|
-
|
|
87
|
+
utils.setStatusUnchanged(node, `out: ${node.count}`);
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
if (done) done();
|
package/nodes/add-block.html
CHANGED
package/nodes/add-block.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
|
|
2
4
|
function AddBlockNode(config) {
|
|
3
5
|
RED.nodes.createNode(this, config);
|
|
4
6
|
const node = this;
|
|
@@ -14,20 +16,20 @@ module.exports = function(RED) {
|
|
|
14
16
|
|
|
15
17
|
// Guard against invalid msg
|
|
16
18
|
if (!msg) {
|
|
17
|
-
|
|
19
|
+
utils.setStatusError(node, "invalid message");
|
|
18
20
|
if (done) done();
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
// Check for required properties
|
|
23
25
|
if (!msg.hasOwnProperty("context")) {
|
|
24
|
-
|
|
26
|
+
utils.setStatusError(node, "missing context");
|
|
25
27
|
if (done) done();
|
|
26
28
|
return;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
if (!msg.hasOwnProperty("payload")) {
|
|
30
|
-
|
|
32
|
+
utils.setStatusError(node, "missing payload");
|
|
31
33
|
if (done) done();
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
@@ -35,40 +37,40 @@ module.exports = function(RED) {
|
|
|
35
37
|
// Handle configuration messages
|
|
36
38
|
if (msg.context === "reset") {
|
|
37
39
|
if (typeof msg.payload !== "boolean") {
|
|
38
|
-
|
|
40
|
+
utils.setStatusError(node, "invalid reset");
|
|
39
41
|
if (done) done();
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
if (msg.payload === true) {
|
|
43
45
|
node.inputs = Array(node.slots).fill(0);
|
|
44
46
|
lastSum = null;
|
|
45
|
-
|
|
47
|
+
utils.setStatusOK(node, "state reset");
|
|
46
48
|
if (done) done();
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
} else if (msg.context === "slots") {
|
|
50
52
|
let newSlots = parseInt(msg.payload);
|
|
51
53
|
if (isNaN(newSlots) || newSlots < 1) {
|
|
52
|
-
|
|
54
|
+
utils.setStatusError(node, "invalid slots");
|
|
53
55
|
if (done) done();
|
|
54
56
|
return;
|
|
55
57
|
}
|
|
56
58
|
node.slots = newSlots;
|
|
57
59
|
node.inputs = Array(newSlots).fill(0);
|
|
58
60
|
lastSum = null;
|
|
59
|
-
|
|
61
|
+
utils.setStatusOK(node, `slots: ${node.slots}`);
|
|
60
62
|
if (done) done();
|
|
61
63
|
return;
|
|
62
64
|
} else if (msg.context.startsWith("in")) {
|
|
63
65
|
let slotIndex = parseInt(msg.context.slice(2)) - 1;
|
|
64
66
|
if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.slots) {
|
|
65
|
-
|
|
67
|
+
utils.setStatusError(node, `invalid input slot ${msg.context}`);
|
|
66
68
|
if (done) done();
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
69
71
|
let newValue = parseFloat(msg.payload);
|
|
70
72
|
if (isNaN(newValue)) {
|
|
71
|
-
|
|
73
|
+
utils.setStatusError(node, "invalid input");
|
|
72
74
|
if (done) done();
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
@@ -76,13 +78,18 @@ module.exports = function(RED) {
|
|
|
76
78
|
// Calculate sum
|
|
77
79
|
const sum = node.inputs.reduce((acc, val) => acc + val, 0);
|
|
78
80
|
const isUnchanged = sum === lastSum;
|
|
79
|
-
|
|
81
|
+
const statusText = `${msg.context}: ${newValue.toFixed(2)}, sum: ${sum.toFixed(2)}`;
|
|
82
|
+
if (isUnchanged) {
|
|
83
|
+
utils.setStatusUnchanged(node, statusText);
|
|
84
|
+
} else {
|
|
85
|
+
utils.setStatusChanged(node, statusText);
|
|
86
|
+
}
|
|
80
87
|
lastSum = sum;
|
|
81
88
|
send({ payload: sum });
|
|
82
89
|
if (done) done();
|
|
83
90
|
return;
|
|
84
91
|
} else {
|
|
85
|
-
|
|
92
|
+
utils.setStatusWarn(node, "unknown context");
|
|
86
93
|
if (done) done();
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="alarm-collector">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Node Name</label>
|
|
4
|
+
<input type="text" id="node-input-name" placeholder="e.g., Temperature High">
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-alarmConfig"><i class="fa fa-cog"></i> Alarm Registry</label>
|
|
9
|
+
<input type="text" id="node-input-alarmConfig">
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="form-row">
|
|
13
|
+
<label for="node-input-sourceNode"><i class="fa fa-link"></i> Data Source</label>
|
|
14
|
+
<input type="text" id="node-input-sourceNode">
|
|
15
|
+
<input type="hidden" id="node-input-sourceNodeType">
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="form-row" id="input-field-row" style="display: none;">
|
|
19
|
+
<label for="node-input-inputField"><i class="fa fa-folder-open"></i> Input Property</label>
|
|
20
|
+
<input type="text" id="node-input-inputField" placeholder="payload">
|
|
21
|
+
<input type="hidden" id="node-input-inputFieldType">
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-inputMode"><i class="fa fa-sliders"></i> Input Mode</label>
|
|
26
|
+
<select id="node-input-inputMode">
|
|
27
|
+
<option value="boolean">Boolean Trigger</option>
|
|
28
|
+
<option value="value">Numeric Threshold</option>
|
|
29
|
+
</select>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Boolean mode options (hidden by default) -->
|
|
33
|
+
<div id="boolean-options" style="display: none;">
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-input-alarmWhenTrue"><i class="fa fa-check"></i> Alarm When</label>
|
|
36
|
+
<select id="node-input-alarmWhenTrue">
|
|
37
|
+
<option value="true">True</option>
|
|
38
|
+
<option value="false">False</option>
|
|
39
|
+
</select>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Value mode options (hidden by default) -->
|
|
44
|
+
<div id="value-options" style="display: none;">
|
|
45
|
+
<div class="form-row">
|
|
46
|
+
<label for="node-input-highThreshold"><i class="fa fa-arrow-up"></i> High Threshold</label>
|
|
47
|
+
<input type="number" id="node-input-highThreshold" placeholder="85" step="0.1">
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label for="node-input-lowThreshold"><i class="fa fa-arrow-down"></i> Low Threshold</label>
|
|
51
|
+
<input type="number" id="node-input-lowThreshold" placeholder="68" step="0.1">
|
|
52
|
+
</div>
|
|
53
|
+
<div class="form-row">
|
|
54
|
+
<label for="node-input-compareMode"><i class="fa fa-code-branch"></i> Compare Mode</label>
|
|
55
|
+
<select id="node-input-compareMode">
|
|
56
|
+
<option value="either">Either (High OR Low)</option>
|
|
57
|
+
<option value="high-only">High Only</option>
|
|
58
|
+
<option value="low-only">Low Only</option>
|
|
59
|
+
</select>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="form-row">
|
|
62
|
+
<label for="node-input-hysteresisMagnitude"><i class="fa fa-arrows-h"></i> Hysteresis Magnitude</label>
|
|
63
|
+
<input type="number" id="node-input-hysteresisMagnitude" placeholder="2" step="0.1" min="0">
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Shared hysteresis time option -->
|
|
68
|
+
<div class="form-row">
|
|
69
|
+
<label for="node-input-hysteresisTime"><i class="fa fa-hourglass-start"></i> Hysteresis Time</label>
|
|
70
|
+
<input type="number" id="node-input-hysteresisTime" placeholder="500" step="50" min="0">
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Alarm Message Properties -->
|
|
74
|
+
<hr style="margin: 15px 0;">
|
|
75
|
+
|
|
76
|
+
<div class="form-row">
|
|
77
|
+
<label for="node-input-priority"><i class="fa fa-exclamation-triangle"></i> Priority</label>
|
|
78
|
+
<select id="node-input-priority">
|
|
79
|
+
<option value="urgent">Urgent</option>
|
|
80
|
+
<option value="high">High</option>
|
|
81
|
+
<option value="normal">Normal</option>
|
|
82
|
+
<option value="low">Low</option>
|
|
83
|
+
</select>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="form-row">
|
|
87
|
+
<label for="node-input-topic"><i class="fa fa-folder-open"></i> Topic</label>
|
|
88
|
+
<input type="text" id="node-input-topic" placeholder="Alarms_HVAC">
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="form-row">
|
|
92
|
+
<label for="node-input-title"><i class="fa fa-font"></i> Title</label>
|
|
93
|
+
<input type="text" id="node-input-title" placeholder="Temperature Out of Range">
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-input-message"><i class="fa fa-comment"></i> Message</label>
|
|
98
|
+
<input type="text" id="node-input-message" placeholder="Zone exceeds setpoint">
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="form-row">
|
|
102
|
+
<label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
|
|
103
|
+
<input type="text" id="node-input-tags" placeholder="hvac,temperature,building_a">
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label for="node-input-units"><i class="fa fa-thermometer"></i> Units</label>
|
|
108
|
+
<input type="text" id="node-input-units" placeholder="°F">
|
|
109
|
+
</div>
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<script type="text/javascript">
|
|
113
|
+
RED.nodes.registerType("alarm-collector", {
|
|
114
|
+
category: "bldgblocks alarms",
|
|
115
|
+
color: "#800020",
|
|
116
|
+
defaults: {
|
|
117
|
+
name: { value: "" },
|
|
118
|
+
alarmConfig: { value: null, type: "alarm-config" },
|
|
119
|
+
sourceNode: { value: "" },
|
|
120
|
+
sourceNodeType: { value: "wired" },
|
|
121
|
+
inputField: { value: "payload" },
|
|
122
|
+
inputFieldType: { value: "msg" },
|
|
123
|
+
inputMode: { value: "value", required: true },
|
|
124
|
+
alarmWhenTrue: { value: true },
|
|
125
|
+
highThreshold: { value: 85 },
|
|
126
|
+
lowThreshold: { value: 68 },
|
|
127
|
+
compareMode: { value: "either" },
|
|
128
|
+
hysteresisTime: { value: 500 },
|
|
129
|
+
hysteresisMagnitude: { value: 2 },
|
|
130
|
+
priority: { value: "high" },
|
|
131
|
+
topic: { value: "Alarms_Default" },
|
|
132
|
+
title: { value: "Alarm" },
|
|
133
|
+
message: { value: "Condition triggered" },
|
|
134
|
+
tags: { value: "" },
|
|
135
|
+
units: { value: "°F" }
|
|
136
|
+
},
|
|
137
|
+
inputs: 1,
|
|
138
|
+
outputs: 1,
|
|
139
|
+
inputLabels: ["input"],
|
|
140
|
+
outputLabels: ["alarm"],
|
|
141
|
+
icon: "font-awesome/fa-bell-o",
|
|
142
|
+
paletteLabel: "alarm collector",
|
|
143
|
+
label: function() {
|
|
144
|
+
return this.name || "alarm collector";
|
|
145
|
+
},
|
|
146
|
+
oneditprepare: function() {
|
|
147
|
+
const node = this;
|
|
148
|
+
|
|
149
|
+
// Build searchable list of global-setter nodes
|
|
150
|
+
var candidateNodes = [];
|
|
151
|
+
RED.nodes.eachNode(function(n) {
|
|
152
|
+
if (n.type === 'global-setter') {
|
|
153
|
+
let displayPath = n.path || "No Path";
|
|
154
|
+
if (displayPath.startsWith("#") && displayPath.includes(":")) {
|
|
155
|
+
displayPath = displayPath.substring(displayPath.indexOf(":") + 1);
|
|
156
|
+
}
|
|
157
|
+
candidateNodes.push({
|
|
158
|
+
value: n.id,
|
|
159
|
+
label: displayPath + (n.name ? ` (${n.name})` : ""),
|
|
160
|
+
path: displayPath
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
|
|
165
|
+
|
|
166
|
+
// Setup typedInput: "wired" mode or "setter" searchable list
|
|
167
|
+
$("#node-input-sourceNode").typedInput({
|
|
168
|
+
types: [
|
|
169
|
+
{
|
|
170
|
+
value: "wired",
|
|
171
|
+
label: "wired input",
|
|
172
|
+
hasValue: false,
|
|
173
|
+
icon: "fa fa-arrow-right"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
value: "setter",
|
|
177
|
+
icon: "fa fa-crosshairs",
|
|
178
|
+
label: "global variable",
|
|
179
|
+
options: candidateNodes
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
typeField: "#node-input-sourceNodeType"
|
|
183
|
+
}).typedInput("type", node.sourceNodeType || "wired").typedInput("value", node.sourceNode || "");
|
|
184
|
+
|
|
185
|
+
// Show/hide Input Field based on sourceNodeType
|
|
186
|
+
const updateInputFieldDisplay = () => {
|
|
187
|
+
let sourceType = $("#node-input-sourceNode").typedInput("type");
|
|
188
|
+
$("#input-field-row").toggle(sourceType === "wired");
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
$("#node-input-sourceNode").on("change", updateInputFieldDisplay);
|
|
192
|
+
updateInputFieldDisplay();
|
|
193
|
+
|
|
194
|
+
// Setup inputField typedInput with only msg type
|
|
195
|
+
$("#node-input-inputField").typedInput({
|
|
196
|
+
types: ["msg"],
|
|
197
|
+
typeField: "#node-input-inputFieldType"
|
|
198
|
+
}).typedInput("type", node.inputFieldType || "msg").typedInput("value", node.inputField || "payload");
|
|
199
|
+
|
|
200
|
+
// Show/hide sections based on inputMode
|
|
201
|
+
const updateDisplay = () => {
|
|
202
|
+
let mode = $("#node-input-inputMode").val();
|
|
203
|
+
$("#boolean-options").toggle(mode === "boolean");
|
|
204
|
+
$("#value-options").toggle(mode === "value");
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
$("#node-input-inputMode").on("change", updateDisplay);
|
|
208
|
+
updateDisplay();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<script type="text/markdown" data-help-name="alarm-collector">
|
|
214
|
+
Monitors a condition and emits alarm events on state transitions.
|
|
215
|
+
|
|
216
|
+
### Inputs
|
|
217
|
+
- **payload** (number | boolean): Sensor value or trigger boolean
|
|
218
|
+
|
|
219
|
+
### Outputs
|
|
220
|
+
- **payload** (boolean): true=alarmed, false=cleared
|
|
221
|
+
- **alarm** (object): Full alarm event data with metadata
|
|
222
|
+
|
|
223
|
+
### Configuration
|
|
224
|
+
|
|
225
|
+
**Alarm Registry**: Reference an alarm-config node to register this alarm. The registry displays all active alarms and allows quick navigation to collectors.
|
|
226
|
+
|
|
227
|
+
**Data Source (Optional)**: Select a network-point-register to auto-subscribe, or use wired input messages.
|
|
228
|
+
|
|
229
|
+
### Modes
|
|
230
|
+
|
|
231
|
+
**Boolean Trigger**: msg.payload = true/false triggers alarm after hysteresis time
|
|
232
|
+
|
|
233
|
+
**Numeric Threshold**: msg.payload = number, evaluated against high/low thresholds with time + magnitude hysteresis
|
|
234
|
+
|
|
235
|
+
### Hysteresis
|
|
236
|
+
|
|
237
|
+
**Time-based**: Condition must persist for N milliseconds before triggering alarm or clearing it
|
|
238
|
+
- Prevents false alarms from sensor noise
|
|
239
|
+
- Configurable delay (default 500ms)
|
|
240
|
+
|
|
241
|
+
**Magnitude-based** (numeric mode only): Additional deadband before clearing alarm
|
|
242
|
+
- Requires sensor to move away from threshold to prevent chatter
|
|
243
|
+
- Example: highThreshold=85, magnitude=2 requires cool to 83°F to clear (not 84°F)
|
|
244
|
+
|
|
245
|
+
### Event Emission
|
|
246
|
+
|
|
247
|
+
Emits `bldgblocks:alarms:state-change` event only on **state transitions** (false→true or true→false)
|
|
248
|
+
- Registers/updates alarm in the registry with current status
|
|
249
|
+
- Service listens to these events and relays downstream
|
|
250
|
+
- Can be wired directly to notification/storage nodes
|
|
251
|
+
|
|
252
|
+
### Status
|
|
253
|
+
- **Green dot**: No alarm, condition not met
|
|
254
|
+
- **Yellow ring**: Condition met, waiting for hysteresis timer to expire
|
|
255
|
+
- **Red ring**: Alarm active (condition has persisted past hysteresis window)
|
|
256
|
+
|
|
257
|
+
### References
|
|
258
|
+
- Pair with [alarm-service] for event relay
|
|
259
|
+
- Route output to [ntfy], [InfluxDB], [Slack], etc. for notifications and storage
|
|
260
|
+
</script>
|