@bldgblocks/node-red-contrib-control 0.1.34 → 0.1.37

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.
Files changed (110) hide show
  1. package/nodes/accumulate-block.html +18 -8
  2. package/nodes/accumulate-block.js +39 -44
  3. package/nodes/add-block.html +1 -1
  4. package/nodes/add-block.js +18 -11
  5. package/nodes/alarm-collector.html +260 -0
  6. package/nodes/alarm-collector.js +292 -0
  7. package/nodes/alarm-config.html +129 -0
  8. package/nodes/alarm-config.js +126 -0
  9. package/nodes/alarm-service.html +96 -0
  10. package/nodes/alarm-service.js +142 -0
  11. package/nodes/analog-switch-block.js +25 -36
  12. package/nodes/and-block.js +44 -15
  13. package/nodes/average-block.js +46 -41
  14. package/nodes/boolean-switch-block.js +10 -28
  15. package/nodes/boolean-to-number-block.html +18 -5
  16. package/nodes/boolean-to-number-block.js +24 -16
  17. package/nodes/cache-block.js +24 -37
  18. package/nodes/call-status-block.html +91 -32
  19. package/nodes/call-status-block.js +398 -115
  20. package/nodes/changeover-block.html +5 -0
  21. package/nodes/changeover-block.js +167 -162
  22. package/nodes/comment-block.html +1 -1
  23. package/nodes/comment-block.js +14 -9
  24. package/nodes/compare-block.html +14 -4
  25. package/nodes/compare-block.js +23 -18
  26. package/nodes/contextual-label-block.html +5 -0
  27. package/nodes/contextual-label-block.js +6 -16
  28. package/nodes/convert-block.html +25 -39
  29. package/nodes/convert-block.js +31 -16
  30. package/nodes/count-block.html +11 -5
  31. package/nodes/count-block.js +34 -32
  32. package/nodes/delay-block.js +58 -53
  33. package/nodes/divide-block.js +43 -45
  34. package/nodes/edge-block.html +17 -10
  35. package/nodes/edge-block.js +43 -41
  36. package/nodes/enum-switch-block.js +6 -6
  37. package/nodes/frequency-block.html +6 -1
  38. package/nodes/frequency-block.js +64 -74
  39. package/nodes/global-getter.html +51 -15
  40. package/nodes/global-getter.js +43 -13
  41. package/nodes/global-setter.html +1 -1
  42. package/nodes/global-setter.js +40 -12
  43. package/nodes/history-buffer.html +96 -0
  44. package/nodes/history-buffer.js +464 -0
  45. package/nodes/history-collector.html +29 -1
  46. package/nodes/history-collector.js +46 -16
  47. package/nodes/history-config.html +13 -1
  48. package/nodes/history-service.html +84 -0
  49. package/nodes/history-service.js +66 -0
  50. package/nodes/hysteresis-block.html +5 -0
  51. package/nodes/hysteresis-block.js +13 -16
  52. package/nodes/interpolate-block.html +20 -2
  53. package/nodes/interpolate-block.js +39 -50
  54. package/nodes/join.html +78 -0
  55. package/nodes/join.js +78 -0
  56. package/nodes/latch-block.js +12 -14
  57. package/nodes/load-sequence-block.js +102 -110
  58. package/nodes/max-block.js +26 -26
  59. package/nodes/memory-block.js +57 -58
  60. package/nodes/min-block.js +26 -25
  61. package/nodes/minmax-block.js +35 -34
  62. package/nodes/modulo-block.js +45 -43
  63. package/nodes/multiply-block.js +43 -41
  64. package/nodes/negate-block.html +17 -7
  65. package/nodes/negate-block.js +25 -19
  66. package/nodes/network-point-read.html +128 -0
  67. package/nodes/network-point-read.js +230 -0
  68. package/nodes/{network-register.html → network-point-register.html} +94 -7
  69. package/nodes/{network-register.js → network-point-register.js} +18 -4
  70. package/nodes/network-point-write.html +149 -0
  71. package/nodes/network-point-write.js +222 -0
  72. package/nodes/network-service-bridge.html +131 -0
  73. package/nodes/network-service-bridge.js +376 -0
  74. package/nodes/network-service-read.html +81 -0
  75. package/nodes/{network-read.js → network-service-read.js} +4 -3
  76. package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
  77. package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
  78. package/nodes/network-service-write.html +89 -0
  79. package/nodes/{network-write.js → network-service-write.js} +3 -3
  80. package/nodes/nullify-block.js +13 -15
  81. package/nodes/on-change-block.html +17 -9
  82. package/nodes/on-change-block.js +49 -46
  83. package/nodes/oneshot-block.html +13 -10
  84. package/nodes/oneshot-block.js +57 -75
  85. package/nodes/or-block.js +44 -15
  86. package/nodes/pid-block.html +54 -4
  87. package/nodes/pid-block.js +459 -248
  88. package/nodes/priority-block.js +24 -35
  89. package/nodes/rate-limit-block.js +70 -72
  90. package/nodes/rate-of-change-block.html +33 -14
  91. package/nodes/rate-of-change-block.js +74 -62
  92. package/nodes/round-block.html +14 -9
  93. package/nodes/round-block.js +32 -25
  94. package/nodes/saw-tooth-wave-block.js +49 -76
  95. package/nodes/scale-range-block.html +12 -6
  96. package/nodes/scale-range-block.js +46 -39
  97. package/nodes/sine-wave-block.js +49 -57
  98. package/nodes/string-builder-block.js +6 -6
  99. package/nodes/subtract-block.js +38 -34
  100. package/nodes/thermistor-block.js +44 -44
  101. package/nodes/tick-tock-block.js +32 -32
  102. package/nodes/time-sequence-block.js +30 -42
  103. package/nodes/triangle-wave-block.js +49 -69
  104. package/nodes/tstat-block.js +34 -44
  105. package/nodes/units-block.html +90 -69
  106. package/nodes/units-block.js +22 -30
  107. package/nodes/utils.js +206 -3
  108. package/package.json +14 -6
  109. package/nodes/network-read.html +0 -56
  110. package/nodes/network-write.html +0 -65
@@ -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
- : context (string) : Configuration commands = reset (`"reset"` to clear count).
47
- : payload (boolean) : For `Accumulate True`, `true` increments count, `false` resets; for `Accumulate False`, `false` increments count, `true` resets; ignored in `Accumulate Flows`.
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
- - `Accumulate True` counts consecutive `true` `msg.payload` inputs (reset on `false` or explicit reset)
55
- - `Accumulate False` counts consecutive `false` `msg.payload` inputs (reset on `true` or explicit reset)
56
- - `Accumulate Flows` counts all valid input messages (reset only on explicit reset).
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` applies immediately;
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
- node.runtime = {
8
- name: config.name,
9
- mode: config.mode,
10
- count: 0,
11
- lastCount: null
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
- node.status({
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
- node.status({ fill: "red", shape: "ring", text: "missing message" });
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.runtime.count = 0;
36
- updateStatus();
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.runtime.mode !== "flows") {
45
- // Check for missing payload
46
- if (!msg.hasOwnProperty("payload")) {
47
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
48
- node.warn("Missing payload");
49
- if (done) done();
50
- return;
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
- // Validate input
54
- const inputValue = msg.payload;
55
- if (typeof inputValue !== "boolean") {
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 isues
63
- if (node.runtime.count > 9999) {
64
- node.runtime.count = 0;
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.runtime.mode === "true") {
63
+ if (node.mode === "true") {
69
64
  if (inputValue === true) {
70
- node.runtime.count++;
65
+ node.count++;
71
66
  } else {
72
- node.runtime.count = 0;
67
+ node.count = 0;
73
68
  }
74
- } else if (node.runtime.mode === "false") {
69
+ } else if (node.mode === "false") {
75
70
  if (inputValue === false) {
76
- node.runtime.count++;
71
+ node.count++;
77
72
  } else {
78
- node.runtime.count = 0;
73
+ node.count = 0;
79
74
  }
80
75
  }
81
76
  } else {
82
77
  // flows mode: count all valid messages
83
- node.runtime.count++;
78
+ node.count++;
84
79
  }
85
80
 
86
81
  // Output only if count changed
87
- if (node.runtime.lastCount !== node.runtime.count) {
88
- node.runtime.lastCount = node.runtime.count;
89
- node.status({ fill: "blue", shape: "dot", text: `out: ${node.runtime.count}` });
90
- send({ payload: node.runtime.count });
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
- node.status({ fill: "blue", shape: "ring", text: `out: ${node.runtime.count}` });
87
+ utils.setStatusUnchanged(node, `out: ${node.count}`);
93
88
  }
94
89
 
95
90
  if (done) done();
@@ -59,7 +59,7 @@ Outputs sum for each valid slot update.
59
59
  - Blue (dot): State changed
60
60
  - Blue (ring): State unchanged
61
61
  - Red (ring): Error
62
- - Yellow: Warning
62
+ - Yellow (ring): Warning
63
63
 
64
64
  ### References
65
65
  - [Node-RED Documentation](https://nodered.org/docs/)
@@ -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
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
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
- node.status({ fill: "red", shape: "ring", text: "missing context" });
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
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
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
- node.status({ fill: "red", shape: "ring", text: "invalid reset" });
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
- node.status({ fill: "green", shape: "dot", text: "state reset" });
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
- node.status({ fill: "red", shape: "ring", text: "invalid slots" });
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
- node.status({ fill: "green", shape: "dot", text: `slots: ${node.slots}` });
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
- node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
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
- node.status({ fill: "red", shape: "ring", text: "invalid input" });
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
- node.status({ fill: "blue", shape: isUnchanged ? "ring" : "dot", text: `${msg.context}: ${newValue.toFixed(2)}, sum: ${sum.toFixed(2)}` });
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
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
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>