@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
@@ -0,0 +1,292 @@
1
+ module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
4
+ function AlarmCollectorNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // Initialize configuration
9
+ node.name = config.name || "alarm-collector";
10
+ node.alarmConfigId = config.alarmConfig;
11
+ node.inputMode = config.inputMode || "value";
12
+ node.inputField = config.inputField || "payload";
13
+ node.alarmWhenTrue = config.alarmWhenTrue !== false;
14
+ node.highThreshold = parseFloat(config.highThreshold) || 85;
15
+ node.lowThreshold = parseFloat(config.lowThreshold) || 68;
16
+ node.compareMode = config.compareMode || "either";
17
+ node.hysteresisTime = parseInt(config.hysteresisTime) || 500;
18
+ node.hysteresisMagnitude = parseFloat(config.hysteresisMagnitude) || 2;
19
+ node.priority = config.priority || "normal";
20
+ node.topic = config.topic || "Alarms_Default";
21
+ node.title = config.title || "Alarm";
22
+ node.message = config.message || "Condition triggered";
23
+ node.tags = config.tags || "";
24
+ node.units = config.units || "";
25
+
26
+ // Get reference to alarm-config node
27
+ node.alarmConfig = RED.nodes.getNode(node.alarmConfigId);
28
+ if (!node.alarmConfig) {
29
+ utils.setStatusWarn(node, "Alarm registry not configured");
30
+ }
31
+
32
+ // Getter pattern: optional target global-setter node selection
33
+ node.sourceNodeId = config.sourceNode || null;
34
+ node.sourceNodeType = config.sourceNodeType || "wired";
35
+ let setterNode = null;
36
+ if (node.sourceNodeType === "setter" && node.sourceNodeId) {
37
+ setterNode = RED.nodes.getNode(node.sourceNodeId);
38
+ }
39
+
40
+ // Runtime state
41
+ node.currentValue = null;
42
+ node.alarmState = false;
43
+ node.lastEmittedState = null;
44
+ node.hysteresisTimer = null;
45
+ node.conditionMet = false;
46
+ node.valueChangedListener = null;
47
+
48
+ utils.setStatusOK(node, `idle`);
49
+
50
+ // ====================================================================
51
+ // Helper: Evaluate alarm condition and emit event if state changed
52
+ // ====================================================================
53
+ function evaluateAndEmit(inputValue) {
54
+ // Evaluate alarm condition based on input mode
55
+ let conditionNowMet = false;
56
+ let numericValue = null;
57
+
58
+ if (node.inputMode === "boolean") {
59
+ // Boolean mode: compare directly with alarmWhenTrue
60
+ conditionNowMet = (inputValue === node.alarmWhenTrue);
61
+ node.currentValue = inputValue;
62
+ } else {
63
+ // Value mode: parse as numeric and check thresholds
64
+ numericValue = inputValue;
65
+ if (typeof inputValue === 'object' && inputValue !== null && inputValue.value !== undefined) {
66
+ numericValue = inputValue.value;
67
+ }
68
+ numericValue = parseFloat(numericValue);
69
+
70
+ if (isNaN(numericValue)) {
71
+ utils.setStatusError(node, "Invalid numeric input");
72
+ return;
73
+ }
74
+
75
+ node.currentValue = numericValue;
76
+
77
+ // Check thresholds
78
+ if (node.compareMode === "either") {
79
+ conditionNowMet = (numericValue > node.highThreshold) || (numericValue < node.lowThreshold);
80
+ } else if (node.compareMode === "high-only") {
81
+ conditionNowMet = (numericValue > node.highThreshold);
82
+ } else if (node.compareMode === "low-only") {
83
+ conditionNowMet = (numericValue < node.lowThreshold);
84
+ }
85
+ }
86
+
87
+ // Time-based hysteresis logic
88
+ if (conditionNowMet && !node.conditionMet) {
89
+ // Condition just became true - start hysteresis timer
90
+ node.conditionMet = true;
91
+
92
+ if (node.hysteresisTimer) clearTimeout(node.hysteresisTimer);
93
+
94
+ node.hysteresisTimer = setTimeout(() => {
95
+ if (node.conditionMet && node.alarmState === false) {
96
+ // Condition stayed true for hysteresisTime ms
97
+ node.alarmState = true;
98
+ emitAlarmEvent("false → true");
99
+ }
100
+ node.hysteresisTimer = null;
101
+ }, node.hysteresisTime);
102
+
103
+ } else if (!conditionNowMet && node.conditionMet) {
104
+ // Condition just became false - cancel pending timer
105
+ node.conditionMet = false;
106
+
107
+ if (node.hysteresisTimer) {
108
+ clearTimeout(node.hysteresisTimer);
109
+ node.hysteresisTimer = null;
110
+ }
111
+
112
+ // Check magnitude hysteresis before clearing
113
+ let shouldClear = true;
114
+ if (node.inputMode === "value" && node.alarmState === true) {
115
+ if (node.compareMode === "either" || node.compareMode === "high-only") {
116
+ const clearThreshold = node.highThreshold - node.hysteresisMagnitude;
117
+ if (numericValue > clearThreshold) {
118
+ shouldClear = false;
119
+ }
120
+ }
121
+ if (node.compareMode === "either" || node.compareMode === "low-only") {
122
+ const clearThreshold = node.lowThreshold + node.hysteresisMagnitude;
123
+ if (numericValue < clearThreshold) {
124
+ shouldClear = false;
125
+ }
126
+ }
127
+ }
128
+
129
+ if (shouldClear && node.alarmState === true) {
130
+ node.alarmState = false;
131
+ emitAlarmEvent("true → false");
132
+ }
133
+ }
134
+
135
+ // Update status display
136
+ let statusText;
137
+ if (node.inputMode === "boolean") {
138
+ statusText = `${inputValue ? "true" : "false"}`;
139
+ } else {
140
+ statusText = `${numericValue.toFixed(2)} ${node.units}`;
141
+ }
142
+
143
+ if (node.alarmState) {
144
+ utils.setStatusError(node, statusText + " [ALARM]");
145
+ } else if (node.conditionMet) {
146
+ utils.setStatusWarn(node, statusText + " (hysteresis)");
147
+ } else {
148
+ utils.setStatusOK(node, statusText);
149
+ }
150
+ }
151
+
152
+ // ====================================================================
153
+ // Emit alarm event (only on state transition)
154
+ // ====================================================================
155
+ function emitAlarmEvent(transition) {
156
+ if (node.lastEmittedState === node.alarmState) {
157
+ return;
158
+ }
159
+
160
+ node.lastEmittedState = node.alarmState;
161
+
162
+ const eventData = {
163
+ nodeId: node.id,
164
+ nodeName: node.name,
165
+ value: node.currentValue,
166
+ highThreshold: node.inputMode === "value" ? node.highThreshold : undefined,
167
+ lowThreshold: node.inputMode === "value" ? node.lowThreshold : undefined,
168
+ compareMode: node.inputMode === "value" ? node.compareMode : undefined,
169
+ state: node.alarmState,
170
+ priority: node.priority,
171
+ topic: node.topic,
172
+ title: node.title,
173
+ message: node.message,
174
+ tags: node.tags,
175
+ units: node.units,
176
+ timestamp: new Date().toISOString(),
177
+ transition: transition
178
+ };
179
+
180
+ // Register/update alarm in registry
181
+ if (node.alarmConfig) {
182
+ const alarmName = node.name;
183
+ node.alarmConfig.register(alarmName, {
184
+ nodeId: node.id,
185
+ pointId: node.currentValue,
186
+ severity: node.priority,
187
+ status: node.alarmState ? 'active' : 'cleared',
188
+ title: node.title,
189
+ message: node.message,
190
+ topic: node.topic,
191
+ timestamp: new Date().toISOString()
192
+ });
193
+ }
194
+
195
+ // Emit to fixed event - service listens here
196
+ RED.events.emit("bldgblocks:alarms:state-change", eventData);
197
+ }
198
+
199
+ // ====================================================================
200
+ // Setup listeners based on mode (wired or target node)
201
+ // ====================================================================
202
+
203
+ // If target global-setter selected, listen to value changes (same as global-getter)
204
+ if (setterNode && setterNode.varName) {
205
+ node.valueChangedListener = function(evt) {
206
+ if (evt.key === setterNode.varName && evt.store === setterNode.storeName) {
207
+ // Extract value from the global data object
208
+ let val = evt.data;
209
+ if (val && typeof val === 'object' && val.hasOwnProperty('value')) {
210
+ val = val.value;
211
+ }
212
+ if (val !== undefined && val !== null) {
213
+ evaluateAndEmit(val);
214
+ }
215
+ }
216
+ };
217
+
218
+ RED.events.on("bldgblocks:global:value-changed", node.valueChangedListener);
219
+ utils.setStatusOK(node, `monitoring ${setterNode.varName}`);
220
+ }
221
+
222
+ // Wired input handler
223
+ node.on("input", async function(msg, send, done) {
224
+ send = send || function() { node.send.apply(node, arguments); };
225
+
226
+ if (!msg) {
227
+ utils.setStatusError(node, "invalid message");
228
+ if (done) done();
229
+ return;
230
+ }
231
+
232
+ // If using target node, ignore wired input
233
+ if (setterNode) {
234
+ if (done) done();
235
+ return;
236
+ }
237
+
238
+ // Get input value from configured msg property using evaluateNodeProperty
239
+ try {
240
+ let inputValue = await utils.evaluateNodeProperty(node.inputField, node.inputFieldType || "msg", node, msg);
241
+
242
+ if (inputValue === undefined || inputValue === null) {
243
+ utils.setStatusError(node, `missing field: ${node.inputField}`);
244
+ if (done) done();
245
+ return;
246
+ }
247
+
248
+ // Evaluate based on input mode
249
+ if (node.inputMode === "boolean") {
250
+ inputValue = Boolean(inputValue);
251
+ } else {
252
+ inputValue = parseFloat(inputValue);
253
+ if (isNaN(inputValue)) {
254
+ utils.setStatusError(node, "invalid numeric input");
255
+ if (done) done();
256
+ return;
257
+ }
258
+ }
259
+
260
+ evaluateAndEmit(inputValue);
261
+ } catch (err) {
262
+ utils.setStatusError(node, `Error reading input: ${err.message}`);
263
+ node.error(err);
264
+ }
265
+
266
+ if (done) done();
267
+ });
268
+
269
+ node.on("close", function(done) {
270
+ // Cleanup timers
271
+ if (node.hysteresisTimer) {
272
+ clearTimeout(node.hysteresisTimer);
273
+ node.hysteresisTimer = null;
274
+ }
275
+
276
+ // Unregister alarm from registry
277
+ if (node.alarmConfig) {
278
+ node.alarmConfig.unregister(node.name, node.id);
279
+ }
280
+
281
+ // Remove global value-changed listener
282
+ if (node.valueChangedListener) {
283
+ RED.events.off("bldgblocks:global:value-changed", node.valueChangedListener);
284
+ node.valueChangedListener = null;
285
+ }
286
+
287
+ done();
288
+ });
289
+ }
290
+
291
+ RED.nodes.registerType("alarm-collector", AlarmCollectorNode);
292
+ };
@@ -0,0 +1,129 @@
1
+ <script type="text/html" data-template-name="alarm-config">
2
+ <div class="form-row">
3
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-config-input-name" placeholder="Alarm Registry">
5
+ </div>
6
+ <div class="form-tips">
7
+ <p><b>Alarm Registry</b></p>
8
+ <p>This node maintains all active alarms and their current status (active/cleared).</p>
9
+ </div>
10
+
11
+ <div class="form-row">
12
+ <label><i class="fa fa-exclamation-triangle"></i> Alarms</label>
13
+ <div id="node-input-alarm-list-div"
14
+ style="
15
+ border:1px solid #ccc;
16
+ height:250px;
17
+ overflow-y:auto;
18
+ padding:5px;
19
+ box-sizing:border-box;">
20
+ <ul id="node-input-alarm-list" style="margin:0;padding-left:1.2em;"></ul>
21
+ </div>
22
+ </div>
23
+ </script>
24
+
25
+ <script type="text/javascript">
26
+ RED.nodes.registerType("alarm-config", {
27
+ category: 'config',
28
+ defaults: {
29
+ name: { value: "" }
30
+ },
31
+ label: function() {
32
+ return this.name || "Alarm Registry";
33
+ },
34
+ oneditprepare: function() {
35
+ const node = this;
36
+ const $list = $("#node-input-alarm-list");
37
+
38
+ function loadAlarms() {
39
+ $.getJSON(`/alarm-config/list/${node.id}`, function(data) {
40
+ $list.empty();
41
+ if (!data.length) {
42
+ return $list.append('<li style="color:#999;">No alarms registered</li>');
43
+ }
44
+
45
+ // Data already sorted by server
46
+ data.forEach(alarm => {
47
+ const li = $('<li>')
48
+ .css({
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ padding: '4px 0',
52
+ borderBottom: '1px solid #eee'
53
+ });
54
+
55
+ // Status indicator (colored circle)
56
+ const statusColor = alarm.status === 'active' ? '#d32f2f' : '#4caf50';
57
+ const statusIcon = alarm.status === 'active' ? 'fa-circle' : 'fa-check-circle';
58
+ const statusDot = $('<i>')
59
+ .addClass(`fa ${statusIcon}`)
60
+ .css({
61
+ color: statusColor,
62
+ marginRight: '8px',
63
+ width: '16px'
64
+ });
65
+
66
+ // Alarm info (name, severity, point)
67
+ const infoText = $('<span>')
68
+ .html(`<strong>${alarm.name}</strong> <span style="color:#666;font-size:0.9em;">[${alarm.severity}] #${alarm.pointId}</span>`)
69
+ .css({ flexGrow: 1 });
70
+
71
+ // Reveal button (find node on canvas)
72
+ const revealBtn = $('<button type="button" class="editor-button">')
73
+ .attr('title', 'Reveal alarm collector on canvas')
74
+ .html('<i class="fa fa-search"></i>')
75
+ .on('click', function(e) {
76
+ e.stopPropagation();
77
+ RED.view.reveal(alarm.nodeId);
78
+ });
79
+
80
+ li.append(statusDot, infoText, revealBtn);
81
+ $list.append(li);
82
+ });
83
+ }).fail(function() {
84
+ $list.html('<li style="color:red;">Could not load alarms</li>');
85
+ });
86
+ }
87
+
88
+ // Load alarms on editor open
89
+ loadAlarms();
90
+
91
+ // Optionally: reload on a timer for live updates
92
+ // setInterval(loadAlarms, 2000);
93
+ }
94
+ });
95
+ </script>
96
+
97
+ <script type="text/markdown" data-help-name="alarm-config">
98
+ Maintains a registry of active alarms and their status.
99
+
100
+ ### Details
101
+ The Alarm Registry keeps track of all active alarms generated by alarm-collector nodes. Each alarm has:
102
+ - **Name**: Unique identifier (usually derived from point ID and condition)
103
+ - **Status**: Current state (active or cleared)
104
+ - **Severity**: Level assigned by the alarm-collector
105
+ - **Point ID**: Source network point that triggered the alarm
106
+
107
+ This config node is referenced by alarm-collector nodes and used by the alarm-service node to subscribe to alarm updates.
108
+
109
+ ### Features
110
+ - **Live Status Display**: Shows all active alarms with color-coded status (red = active, green = cleared)
111
+ - **Reveal Button**: Click to find an alarm collector on the canvas
112
+ - **Severity Indication**: See the priority level of each alarm at a glance
113
+
114
+ ### Usage
115
+ 1. Create a single alarm-config node for your workflow
116
+ 2. Reference it from each alarm-collector node
117
+ 3. The alarm-service node listens to events from all collectors and uses this registry for state tracking
118
+
119
+ ### API for Developers
120
+ * `register(alarmName, meta)`: Register an alarm
121
+ * `unregister(alarmName, nodeId)`: Remove an alarm
122
+ * `lookup(alarmName)`: Find alarm details by name
123
+ * `updateStatus(alarmName, status)`: Update alarm state
124
+ * `getAll()`: Get all alarms
125
+
126
+ ### References
127
+ - [Node-RED Documentation](https://nodered.org/docs/)
128
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
129
+ </script>
@@ -0,0 +1,126 @@
1
+ module.exports = function(RED) {
2
+
3
+ function AlarmConfigNode(config) {
4
+ RED.nodes.createNode(this, config);
5
+ const node = this;
6
+
7
+ // Register this registry with utils for global lookup
8
+ const utils = require('./utils')(RED);
9
+ utils.registerRegistryNode(node);
10
+
11
+ // The Map: stores alarm metadata by name
12
+ // Format: { "alarmName": { nodeId: "abc.123", pointId: 101, severity: "high", status: "active", ... } }
13
+ node.alarms = new Map();
14
+
15
+ // Register an alarm in the registry
16
+ node.register = function(alarmName, meta) {
17
+ if (!alarmName || typeof alarmName !== 'string') {
18
+ return false;
19
+ }
20
+
21
+ if (node.alarms.has(alarmName)) {
22
+ const existing = node.alarms.get(alarmName);
23
+ // Allow update if it's the same node
24
+ if (existing.nodeId !== meta.nodeId) {
25
+ return false;
26
+ }
27
+ // Merge updates (preserving status if provided)
28
+ meta = Object.assign({}, existing, meta);
29
+ }
30
+ node.alarms.set(alarmName, meta);
31
+ return true;
32
+ };
33
+
34
+ // Unregister an alarm
35
+ node.unregister = function(alarmName, nodeId) {
36
+ if (node.alarms.has(alarmName) && node.alarms.get(alarmName).nodeId === nodeId) {
37
+ node.alarms.delete(alarmName);
38
+ }
39
+ };
40
+
41
+ // Lookup an alarm by name
42
+ node.lookup = function(alarmName) {
43
+ return node.alarms.get(alarmName);
44
+ };
45
+
46
+ // Update alarm status
47
+ node.updateStatus = function(alarmName, status) {
48
+ if (node.alarms.has(alarmName)) {
49
+ const alarm = node.alarms.get(alarmName);
50
+ alarm.status = status; // 'active' or 'cleared'
51
+ alarm.lastUpdate = new Date().toISOString();
52
+ return true;
53
+ }
54
+ return false;
55
+ };
56
+
57
+ // Get all alarms
58
+ node.getAll = function() {
59
+ const arr = [];
60
+ for (const [name, meta] of node.alarms.entries()) {
61
+ arr.push({ name, ...meta });
62
+ }
63
+ return arr;
64
+ };
65
+ }
66
+ RED.nodes.registerType("alarm-config", AlarmConfigNode);
67
+
68
+ // --- HTTP Endpoint: List all alarms in a specific config ---
69
+ // Route: /alarm-config/list/<ConfigID>
70
+ RED.httpAdmin.get('/alarm-config/list/:configId', RED.auth.needsPermission('alarm-config.read'), function(req, res) {
71
+ const configId = req.params.configId;
72
+
73
+ // Find the alarm-config node
74
+ const configNode = RED.nodes.getNode(configId);
75
+ if (!configNode) {
76
+ return res.status(404).json({ error: 'Configuration node not found' });
77
+ }
78
+
79
+ // Get all alarms from this config
80
+ const alarms = configNode.getAll();
81
+
82
+ // Sort by name
83
+ alarms.sort((a, b) => a.name.localeCompare(b.name));
84
+
85
+ res.json(alarms);
86
+ });
87
+
88
+ // --- HTTP Endpoint: Check if alarm exists ---
89
+ // Route: /alarm-config/check/<ConfigID>/<AlarmName>/<CurrentNodeID>
90
+ RED.httpAdmin.get('/alarm-config/check/:configId/:alarmName/:nodeId', RED.auth.needsPermission('alarm-config.read'), function(req, res) {
91
+ const configId = req.params.configId;
92
+ const alarmName = decodeURIComponent(req.params.alarmName);
93
+ const checkNodeId = req.params.nodeId;
94
+
95
+ // Find the alarm-config node
96
+ const configNode = RED.nodes.getNode(configId);
97
+
98
+ let entry = null;
99
+ let result = "unavailable";
100
+ let collision = false;
101
+
102
+ if (!configNode) {
103
+ // Config exists in editor but not deployed yet, or doesn't exist
104
+ return res.json({ status: result, warning: "Configuration not deployed" });
105
+ }
106
+
107
+ // Check for the alarm
108
+ entry = configNode.lookup(alarmName);
109
+ if (entry) {
110
+ // Collision if alarm exists AND belongs to a different node
111
+ if (entry.nodeId !== checkNodeId) {
112
+ collision = true;
113
+ }
114
+ }
115
+
116
+ if (collision) {
117
+ result = "collision";
118
+ } else if (!collision && entry) {
119
+ result = "assigned";
120
+ } else {
121
+ result = "available";
122
+ }
123
+
124
+ res.json({ status: result, details: entry });
125
+ });
126
+ };
@@ -0,0 +1,96 @@
1
+ <script type="text/html" data-template-name="alarm-service">
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="Alarm Relay">
5
+ </div>
6
+
7
+ <div class="form-row">
8
+ <label for="node-input-filterTopic"><i class="fa fa-filter"></i> Filter by Topic (optional)</label>
9
+ <input type="text" id="node-input-filterTopic" placeholder="Leave blank to relay all topics">
10
+ </div>
11
+
12
+ <div class="form-row">
13
+ <label for="node-input-filterPriority"><i class="fa fa-exclamation-triangle"></i> Filter by Priority (optional)</label>
14
+ <select id="node-input-filterPriority">
15
+ <option value="">All Priorities</option>
16
+ <option value="urgent">Urgent</option>
17
+ <option value="high">High</option>
18
+ <option value="normal">Normal</option>
19
+ <option value="low">Low</option>
20
+ </select>
21
+ </div>
22
+ </script>
23
+
24
+ <script type="text/javascript">
25
+ RED.nodes.registerType("alarm-service", {
26
+ category: "bldgblocks alarms",
27
+ color: "#800020",
28
+ defaults: {
29
+ name: { value: "" },
30
+ filterTopic: { value: "" },
31
+ filterPriority: { value: "" }
32
+ },
33
+ inputs: 0,
34
+ outputs: 1,
35
+ outputLabels: ["alarm events"],
36
+ icon: "font-awesome/fa-bell",
37
+ label: function() {
38
+ return this.name || "alarm-service";
39
+ }
40
+ });
41
+ </script>
42
+
43
+ <script type="text/markdown" data-help-name="alarm-service">
44
+ Listens to alarm events from all collectors and relays them downstream with status information.
45
+
46
+ ### Configuration
47
+ - **Node Name**: Display name for this relay instance
48
+ - **Filter by Topic** (optional): Only relay alarms matching a specific topic (e.g., "Alarms_HVAC")
49
+ - **Filter by Priority** (optional): Only relay alarms with specific priority level
50
+
51
+ ### Outputs
52
+ Single output with status included in message:
53
+ - **payload** (object): Full alarm event data with topic, priority, title, message, etc.
54
+ - **status** (object): `{ state: "triggered" | "cleared", transition: "false → true" | "true → false" }`
55
+ - **activeAlarmCount** (number): Current number of active alarms being tracked
56
+ - **alarmKey** (string): Topic or auto-generated ID for grouping related alarms
57
+
58
+ ### How It Works
59
+ 1. Listens to `bldgblocks:alarms:state-change` event emitted by all collectors
60
+ 2. Filters alarms by topic and priority (if configured)
61
+ 3. Tracks state transitions (cleared→alarmed or alarmed→cleared)
62
+ 4. Includes transition state in `msg.status` for downstream routing logic
63
+ 5. Updates status showing number of active alarms
64
+
65
+ ### Status
66
+ - **Green dot**: Service running, no current alarms
67
+ - **Yellow ring**: Multiple alarms active
68
+ - **Red ring**: Alarm(s) active
69
+
70
+ ### Downstream Routing
71
+
72
+ Route using `msg.status.state` field:
73
+
74
+ ```
75
+ [alarm-service]
76
+ ├─ [Switch node] → status.state == "triggered"
77
+ │ ├─ [Format for ntfy] → [HTTP POST] → ntfy.sh (push notification)
78
+ │ ├─ [InfluxDB out] → InfluxDB (record alarm trigger)
79
+ │ └─ [Email] → Send alert email
80
+
81
+ └─ [Switch node] → status.state == "cleared"
82
+ ├─ [Format for ntfy] → [HTTP POST] → ntfy.sh (all clear)
83
+ └─ [InfluxDB out] → InfluxDB (record alarm cleared)
84
+ ```
85
+
86
+ ### Control Messages
87
+
88
+ Send control messages via wired input (optional):
89
+ - `{ context: "getStatus" }` — Returns array of active alarms
90
+ - `{ context: "clearAll", payload: true }` — Clears alarm tracking (for testing/reset)
91
+
92
+ ### References
93
+ - Pair with [alarm-collector] nodes to monitor conditions
94
+ - Use `msg.status.state` to conditionally handle triggered vs cleared alarms
95
+ - Multiple services can listen to same collectors (fan-out/routing pattern)
96
+ </script>