node-red-contrib-event-calc 3.3.3 → 3.3.15

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/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # node-red-contrib-event-calc
2
2
 
3
- Node-RED nodes for event caching and streaming calculations with a local pub/sub event hub.
3
+ Node-RED nodes for event caching, streaming calculations, alarm management, and event framing.
4
4
 
5
5
  ## Overview
6
6
 
7
7
  This package provides a local in-memory event hub with topic-based publish/subscribe and latest-value caching for reactive data flows within Node-RED. Stream data from MQTT, OPC-UA, or any source, then perform calculations that trigger automatically when values update.
8
8
 
9
+ Also includes ISA-88 batch structure tracking, ISA-18.2 alarm lifecycle management, and simple event recording.
10
+
9
11
  ## Architecture
10
12
 
11
13
  ```
12
14
  ┌──────────────────────────────────────────────────────────────┐
13
15
  │ event-cache (config node) │
14
- │ • Stores: Map<topic, {value, ts, metadata}>
16
+ │ • Stores: Map<topic, {value, ts, metadata, previous}>
15
17
  │ • Event emitter for topic updates │
16
18
  │ • LRU eviction, optional TTL │
17
19
  └──────────────────────────────────────────────────────────────┘
@@ -34,14 +36,16 @@ Or install directly from the Node-RED palette manager.
34
36
 
35
37
  ## Nodes
36
38
 
37
- ### event-cache (Config Node)
39
+ ### Core Nodes
40
+
41
+ #### event-cache (Config Node)
38
42
 
39
43
  Central cache that stores topic values and manages subscriptions. Configure:
40
44
 
41
45
  - **Max Entries**: Maximum topics to cache (default: 10000). Oldest entries removed when exceeded.
42
46
  - **TTL**: Time-to-live in milliseconds. Set to 0 for no expiry.
43
47
 
44
- ### event-in
48
+ #### event-in
45
49
 
46
50
  Receives messages from any upstream node and pushes values to the cache.
47
51
 
@@ -51,7 +55,7 @@ Receives messages from any upstream node and pushes values to the cache.
51
55
 
52
56
  The original message passes through, allowing insertion into existing flows.
53
57
 
54
- ### event-topic
58
+ #### event-topic
55
59
 
56
60
  Subscribes to a topic and outputs when that topic updates.
57
61
 
@@ -66,7 +70,7 @@ Subscribes to a topic and outputs when that topic updates.
66
70
  - `msg.topic`: Change subscription topic
67
71
  - `msg.payload = 'refresh'`: Output current cached value
68
72
 
69
- ### event-calc
73
+ #### event-calc
70
74
 
71
75
  Subscribes to multiple topics and evaluates an expression when values update.
72
76
 
@@ -92,71 +96,113 @@ Subscribes to multiple topics and evaluates an expression when values update.
92
96
  }
93
97
  ```
94
98
 
95
- ### event-json
99
+ ### Event Frame Nodes
96
100
 
97
- Bidirectional JSON envelope converter for MQTT messaging.
101
+ #### event-frame (ISA-88 Batch Structure)
98
102
 
99
- **Behavior:**
100
- - **Unwrap**: If payload is `{value, topic?, timestamp?}`, extracts to msg properties
101
- - **Wrap**: If payload is any other value, wraps as `{timestamp, topic, value}`
103
+ Creates ISA-88 procedural model records with hierarchical parent-child linking.
102
104
 
103
- **Usage:**
104
- ```
105
- [MQTT in] [event-json] [event-in] (unwrap JSON from broker)
106
- [event-topic] [event-json] [MQTT out] (wrap for broker)
105
+ **ISA-88 Levels:**
106
+ - **Procedure** — top-level batch recipe
107
+ - **Unit Procedure** sequence within a unit
108
+ - **Operation** major processing step
109
+ - **Phase** — lowest-level action
110
+
111
+ **Features:**
112
+ - Trigger-based: truthy starts, falsy ends
113
+ - Auto-generated UUIDs
114
+ - Parent-child linking via `parentLevel` config or `msg.frame_id` chaining
115
+ - Cascade end: ending a parent auto-ends all children recursively
116
+ - Shared global context tracker for cross-node coordination
117
+
118
+ **Output Record:**
119
+ ```json
120
+ {
121
+ "id": "auto-generated UUID",
122
+ "starttime": "2024-01-15T10:30:00.000Z",
123
+ "endtime": "9999-12-31T23:59:59.000Z",
124
+ "name": "Mixing",
125
+ "parent_id": "parent-uuid-or-empty",
126
+ "level": "operation",
127
+ "state": "running",
128
+ "batch_id": "BATCH-001",
129
+ "unit": "Reactor-1",
130
+ "metadata": ""
131
+ }
107
132
  ```
108
133
 
109
- ### event-simulator
134
+ #### simple-frame (Simple Event Recorder)
110
135
 
111
- Generates simulated data for testing. Supports sine waves, random values, and ramps.
136
+ A simplified event frame for tracking any event with start/end time — no hierarchy, no cascade.
112
137
 
113
- ### event-chart
138
+ **Properties:**
139
+ - **Event Type**: Category label (str/msg/flow/global/env)
140
+ - **Event Name**: Descriptive name (str/msg/flow/global/env)
141
+ - **Metadata Field**: Optional msg property for extra data
142
+ - **Two outputs**: start and end
114
143
 
115
- Real-time charting node for visualizing cached event data.
144
+ **Output Record:**
145
+ ```json
146
+ {
147
+ "id": "auto-generated UUID",
148
+ "starttime": "2024-01-15T10:30:00.000Z",
149
+ "endtime": "9999-12-31T23:59:59.000Z",
150
+ "type": "maintenance",
151
+ "name": "Oil Change",
152
+ "state": "running",
153
+ "metadata": ""
154
+ }
155
+ ```
116
156
 
117
- ## Examples
157
+ ### Alarm Management
118
158
 
119
- ### Average Temperature
159
+ #### event-alarm (ISA-18.2 Alarm Lifecycle)
120
160
 
121
- ```
122
- [inject: room1/temp] → [event-in] → [cache]
123
- [inject: room2/temp] → [event-in] → [cache]
124
-
125
- [event-calc] → [debug]
126
- inputs: a = sensors/room1/temp
127
- b = sensors/room2/temp
128
- expression: (a + b) / 2
129
- trigger: all
130
- ```
161
+ Tracks alarms through the full ISA-18.2 lifecycle with four outputs.
131
162
 
132
- ### Time-based Calculations (External Trigger)
163
+ **Alarm States:**
164
+ - **UNACK_ALM** — Active + Unacknowledged (just raised)
165
+ - **ACK_ALM** — Active + Acknowledged (operator acked, condition still true)
166
+ - **UNACK_RTN** — Inactive + Unacknowledged (condition cleared, not yet acked)
167
+ - **NORM** — Normal (fully resolved)
133
168
 
169
+ **Lifecycle Paths:**
134
170
  ```
135
- [inject: every 1 min][event-calc (external trigger)] [MQTT out]
136
- inputs: a = sensors/power
137
- b = sensors/voltage
138
- expression: a * b
171
+ Path A: NORM UNACK_ALMACK_ALM → NORM (ack first, then clear)
172
+ Path B: NORM UNACK_ALM → UNACK_RTN → NORM (clear first, then ack)
139
173
  ```
140
174
 
141
- ### MQTT Round-trip with JSON Envelope
175
+ **Properties:**
176
+ - **Condition ID/Name**: Alarm identifier and description
177
+ - **Input Mappings**: Map variable names to cache topics (like event-calc)
178
+ - **Condition**: Expression that evaluates to true/false (e.g. `active == true`)
179
+ - **Severity**: 0-1000 scale
180
+ - **4 Outputs**: raised, acknowledged, cleared, resolved
181
+
182
+ **Actions (via msg.payload):**
183
+ - `{ action: "ack", source: "topic" }` — Acknowledge specific alarm
184
+ - `{ action: "ack_all" }` — Acknowledge all active alarms
185
+ - `{ action: "list" }` — List all active alarms
142
186
 
143
- ```
144
- [MQTT in] → [event-json] → [event-in] → [cache]
187
+ ### Utility Nodes
145
188
 
146
- [event-calc] → [event-json] → [MQTT out]
147
- ```
189
+ #### event-flatten
148
190
 
149
- ### Calculate Power (Voltage × Current)
191
+ Flattens nested objects into individual topic/value pairs for the cache.
150
192
 
151
- ```
152
- [event-calc]
153
- inputs: v = power/voltage
154
- i = power/current
155
- expression: v * i
156
- topic: power/watts
157
- ```
193
+ #### event-preview
194
+
195
+ Preview cached values directly in the Node-RED editor.
196
+
197
+ #### event-simulator
198
+
199
+ Generates simulated data for testing. Supports sine waves, random values, and ramps.
158
200
 
159
- ## Built-in Functions
201
+ #### event-chart
202
+
203
+ Real-time charting node for visualizing cached event data.
204
+
205
+ ## Built-in Functions (event-calc)
160
206
 
161
207
  ### Math
162
208
  | Function | Description |
@@ -190,6 +236,28 @@ Real-time charting node for visualizing cached event data.
190
236
  | `delta(current, previous)` | Difference |
191
237
  | `pctChange(current, previous)` | Percentage change |
192
238
 
239
+ ### Change Detection
240
+ | Function | Description |
241
+ |----------|-------------|
242
+ | `now()` | Current timestamp in milliseconds |
243
+ | `prev('varName')` | Previous value of a variable |
244
+ | `hasChanged('varName')` | `true` if value differs from previous (false on first message) |
245
+ | `timeSinceLastChange('varName')` | Milliseconds since the value last changed |
246
+
247
+ ### Date/Time
248
+ | Function | Description |
249
+ |----------|-------------|
250
+ | `hour()` | Current hour (0-23) |
251
+ | `minute()` | Current minute (0-59) |
252
+ | `second()` | Current second (0-59) |
253
+ | `day()` | Day of week (0=Sun, 1=Mon, ..., 6=Sat) |
254
+ | `dayOfMonth()` | Day of month (1-31) |
255
+ | `month()` | Month (1-12) |
256
+ | `year()` | Full year (e.g. 2026) |
257
+ | `isWeekday()` | `true` if Monday-Friday |
258
+ | `isWeekend()` | `true` if Saturday or Sunday |
259
+ | `hoursBetween(start, end)` | `true` if current hour is within range (wraps midnight) |
260
+
193
261
  ## Expression Examples
194
262
 
195
263
  | Expression | Description |
@@ -202,7 +270,9 @@ Real-time charting node for visualizing cached event data.
202
270
  | `clamp(a, 0, 100)` | Constrain 0-100 |
203
271
  | `map(a, 0, 1023, 0, 100)` | Scale ADC to % |
204
272
  | `ifelse(a > b, 'high', 'low')` | Conditional |
205
- | `pctChange(a, b)` | % change from b to a |
273
+ | `hasChanged('temp')` | Did temperature just change? |
274
+ | `temp - prev('temp')` | Delta from previous value |
275
+ | `isWeekday() && hoursBetween(8, 18)` | During business hours? |
206
276
 
207
277
  ## API (for custom nodes)
208
278
 
@@ -216,7 +286,7 @@ cache.setValue('topic/path', 42, { source: 'sensor' });
216
286
 
217
287
  // Get a value
218
288
  const entry = cache.getValue('topic/path');
219
- // { value: 42, ts: 1704000000000, metadata: { source: 'sensor' } }
289
+ // { value: 42, ts: 1704000000000, metadata: { source: 'sensor' }, previous: { value: 40, ts: ..., metadata: ... } }
220
290
 
221
291
  // Subscribe to updates
222
292
  const subId = cache.subscribe('sensors/room1/temp', (topic, entry) => {
@@ -236,9 +306,11 @@ cache.clear();
236
306
  ## HTTP Admin Endpoints
237
307
 
238
308
  ```
239
- GET /event-cache/:id/stats - Cache statistics
240
- GET /event-cache/:id/topics - List all topics
241
- POST /event-cache/:id/clear - Clear cache
309
+ GET /event-cache/:id/stats - Cache statistics
310
+ GET /event-cache/:id/topics - List all topics
311
+ POST /event-cache/:id/clear - Clear cache
312
+ GET /event-frame/tracker - Current batch tracker state
313
+ GET /event-alarm/:id/alarms - Active alarms for a node
242
314
  ```
243
315
 
244
316
  ## License
@@ -0,0 +1,288 @@
1
+ [
2
+ {
3
+ "id": "alarm-sim-flow",
4
+ "type": "tab",
5
+ "label": "Alarm Lifecycle Simulation",
6
+ "disabled": false,
7
+ "info": "Self-running alarm lifecycle simulation.\n\nEvery few seconds a random alarm is raised. After a random delay it gets\nacknowledged, and after another random delay the condition clears.\nThe event-alarm nodes track the full ISA-18.2 lifecycle.\n\nBoth lifecycle paths are exercised:\n Path A: Raised > Acked > Cleared (Resolved)\n Path B: Raised > Cleared > Acked (Resolved)"
8
+ },
9
+ {
10
+ "id": "sim-cache",
11
+ "type": "event-cache",
12
+ "name": "Sim Cache",
13
+ "maxEntries": "10000",
14
+ "ttl": "0"
15
+ },
16
+ {
17
+ "id": "comment-sim-title",
18
+ "type": "comment",
19
+ "z": "alarm-sim-flow",
20
+ "name": "Alarm Lifecycle Simulation (self-running)",
21
+ "info": "Raises random alarms every N seconds, then auto-acks and auto-clears after random delays.\nWatch the debug sidebar to see the full lifecycle play out.",
22
+ "x": 230,
23
+ "y": 40,
24
+ "wires": []
25
+ },
26
+ {
27
+ "id": "sim-trigger",
28
+ "type": "inject",
29
+ "z": "alarm-sim-flow",
30
+ "name": "Every 5s: raise random alarm",
31
+ "props": [
32
+ { "p": "payload", "v": "", "vt": "date" }
33
+ ],
34
+ "repeat": "5",
35
+ "crontab": "",
36
+ "once": true,
37
+ "onceDelay": "2",
38
+ "x": 200,
39
+ "y": 120,
40
+ "wires": [["sim-raise-alarm"]]
41
+ },
42
+ {
43
+ "id": "sim-raise-alarm",
44
+ "type": "function",
45
+ "z": "alarm-sim-flow",
46
+ "name": "Raise Random Alarm",
47
+ "func": "const alarms = [\n { topic: 'sim/plant/PumpTrip_P1A/active', name: 'PumpTrip_P1A' },\n { topic: 'sim/plant/BreakerFault_MCC3/active', name: 'BreakerFault_MCC3' },\n { topic: 'sim/plant/HighLevel_Thickener/active', name: 'HighLevel_Thickener' },\n { topic: 'sim/plant/VibrationHigh_Screen2/active', name: 'VibrationHigh_Screen2' },\n { topic: 'sim/plant/EStop_Conveyor7/active', name: 'EStop_Conveyor7' },\n { topic: 'sim/plant/MagnetiteLevel_Low/active', name: 'MagnetiteLevel_Low' }\n];\n\n// Pick a random alarm\nconst alarm = alarms[Math.floor(Math.random() * alarms.length)];\n\n// Activate it\nconst activateMsg = {\n topic: alarm.topic,\n payload: true\n};\n\n// Schedule ack after 3-10s random delay\nconst ackDelay = 3000 + Math.floor(Math.random() * 7000);\n// Schedule clear after 5-15s random delay\nconst clearDelay = 5000 + Math.floor(Math.random() * 10000);\n\n// Randomly choose lifecycle path:\n// Path A (60%): ack first, then clear\n// Path B (40%): clear first, then ack\nconst ackFirst = Math.random() < 0.6;\n\nconst ackMsg = {\n payload: { action: 'ack', source: alarm.topic },\n _alarm: alarm.name,\n _delay: ackFirst ? ackDelay : clearDelay + ackDelay\n};\n\nconst clearMsg = {\n topic: alarm.topic,\n payload: false,\n _alarm: alarm.name,\n _delay: ackFirst ? clearDelay + ackDelay : clearDelay\n};\n\n// Store scheduled actions in context for the delay nodes\nnode.status({ text: `${alarm.name} (${ackFirst ? 'ack-first' : 'clear-first'})` });\n\n// Output 1: activate now\n// Output 2: ack (with delay metadata)\n// Output 3: clear (with delay metadata)\nreturn [activateMsg, ackMsg, clearMsg];",
48
+ "outputs": 3,
49
+ "outputLabels": ["activate", "ack (delayed)", "clear (delayed)"],
50
+ "x": 480,
51
+ "y": 120,
52
+ "wires": [["sim-event-in"], ["sim-delay-ack"], ["sim-delay-clear"]]
53
+ },
54
+ {
55
+ "id": "sim-delay-ack",
56
+ "type": "function",
57
+ "z": "alarm-sim-flow",
58
+ "name": "Random Delay (ack)",
59
+ "func": "const delay = msg._delay || 5000;\nconst alarmName = msg._alarm || 'unknown';\n\nreturn new Promise(resolve => {\n const timerId = setTimeout(() => {\n node.status({ text: `ack ${alarmName} after ${(delay/1000).toFixed(1)}s` });\n delete msg._delay;\n delete msg._alarm;\n resolve(msg);\n }, delay);\n \n // Clean up on node close\n node.on('close', () => clearTimeout(timerId));\n});",
60
+ "outputs": 1,
61
+ "x": 480,
62
+ "y": 200,
63
+ "wires": [["sim-ack-router"]]
64
+ },
65
+ {
66
+ "id": "sim-delay-clear",
67
+ "type": "function",
68
+ "z": "alarm-sim-flow",
69
+ "name": "Random Delay (clear)",
70
+ "func": "const delay = msg._delay || 8000;\nconst alarmName = msg._alarm || 'unknown';\n\nreturn new Promise(resolve => {\n const timerId = setTimeout(() => {\n node.status({ text: `clear ${alarmName} after ${(delay/1000).toFixed(1)}s` });\n delete msg._delay;\n delete msg._alarm;\n resolve(msg);\n }, delay);\n \n // Clean up on node close\n node.on('close', () => clearTimeout(timerId));\n});",
71
+ "outputs": 1,
72
+ "x": 490,
73
+ "y": 280,
74
+ "wires": [["sim-event-in"]]
75
+ },
76
+ {
77
+ "id": "sim-event-in",
78
+ "type": "event-in",
79
+ "z": "alarm-sim-flow",
80
+ "name": "Push to Cache",
81
+ "cache": "sim-cache",
82
+ "topicSource": "msg",
83
+ "topicPattern": "",
84
+ "x": 710,
85
+ "y": 120,
86
+ "wires": [[]]
87
+ },
88
+ {
89
+ "id": "sim-ack-router",
90
+ "type": "function",
91
+ "z": "alarm-sim-flow",
92
+ "name": "Route ACK to alarm node",
93
+ "func": "// Route the ack to the correct alarm node based on source topic\nconst source = msg.payload.source;\nconst routes = {\n 'sim/plant/PumpTrip_P1A/active': 0,\n 'sim/plant/BreakerFault_MCC3/active': 1,\n 'sim/plant/HighLevel_Thickener/active': 2,\n 'sim/plant/VibrationHigh_Screen2/active': 3,\n 'sim/plant/EStop_Conveyor7/active': 4,\n 'sim/plant/MagnetiteLevel_Low/active': 5\n};\n\nconst outputs = [null, null, null, null, null, null];\nconst idx = routes[source];\nif (idx !== undefined) {\n outputs[idx] = msg;\n}\nreturn outputs;",
94
+ "outputs": 6,
95
+ "outputLabels": ["Pump", "Breaker", "Thickener", "Vibration", "EStop", "Magnetite"],
96
+ "x": 720,
97
+ "y": 200,
98
+ "wires": [["sim-alarm-pump"], ["sim-alarm-breaker"], ["sim-alarm-thickener"], ["sim-alarm-vibration"], ["sim-alarm-estop"], ["sim-alarm-magnetite"]]
99
+ },
100
+ {
101
+ "id": "comment-alarm-nodes",
102
+ "type": "comment",
103
+ "z": "alarm-sim-flow",
104
+ "name": "--- Event Alarm Nodes ---",
105
+ "info": "",
106
+ "x": 940,
107
+ "y": 40,
108
+ "wires": []
109
+ },
110
+ {
111
+ "id": "sim-alarm-pump",
112
+ "type": "event-alarm",
113
+ "z": "alarm-sim-flow",
114
+ "name": "Pump P1A Trip",
115
+ "cache": "sim-cache",
116
+ "conditionId": "PumpTrip_P1A",
117
+ "conditionName": "Heavy Media Pump P1A Trip",
118
+ "inputMappings": [
119
+ { "name": "active", "topic": "sim/plant/PumpTrip_P1A/active" }
120
+ ],
121
+ "condition": "active == true",
122
+ "severity": "800",
123
+ "outputTopic": "alarm/PumpTrip_P1A",
124
+ "x": 960,
125
+ "y": 100,
126
+ "wires": [["debug-sim-raised"], ["debug-sim-acked"], ["debug-sim-cleared"], ["debug-sim-resolved"]]
127
+ },
128
+ {
129
+ "id": "sim-alarm-breaker",
130
+ "type": "event-alarm",
131
+ "z": "alarm-sim-flow",
132
+ "name": "Breaker MCC3 Fault",
133
+ "cache": "sim-cache",
134
+ "conditionId": "BreakerFault_MCC3",
135
+ "conditionName": "MCC3 Main Breaker Fault",
136
+ "inputMappings": [
137
+ { "name": "active", "topic": "sim/plant/BreakerFault_MCC3/active" }
138
+ ],
139
+ "condition": "active == true",
140
+ "severity": "900",
141
+ "outputTopic": "alarm/BreakerFault_MCC3",
142
+ "x": 970,
143
+ "y": 180,
144
+ "wires": [["debug-sim-raised"], ["debug-sim-acked"], ["debug-sim-cleared"], ["debug-sim-resolved"]]
145
+ },
146
+ {
147
+ "id": "sim-alarm-thickener",
148
+ "type": "event-alarm",
149
+ "z": "alarm-sim-flow",
150
+ "name": "Thickener Level High",
151
+ "cache": "sim-cache",
152
+ "conditionId": "HighLevel_Thickener",
153
+ "conditionName": "Thickener Overflow Level High",
154
+ "inputMappings": [
155
+ { "name": "active", "topic": "sim/plant/HighLevel_Thickener/active" }
156
+ ],
157
+ "condition": "active == true",
158
+ "severity": "600",
159
+ "outputTopic": "alarm/HighLevel_Thickener",
160
+ "x": 970,
161
+ "y": 260,
162
+ "wires": [["debug-sim-raised"], ["debug-sim-acked"], ["debug-sim-cleared"], ["debug-sim-resolved"]]
163
+ },
164
+ {
165
+ "id": "sim-alarm-vibration",
166
+ "type": "event-alarm",
167
+ "z": "alarm-sim-flow",
168
+ "name": "Screen 2 Vibration",
169
+ "cache": "sim-cache",
170
+ "conditionId": "VibrationHigh_Screen2",
171
+ "conditionName": "Screen 2 Vibration Alarm High",
172
+ "inputMappings": [
173
+ { "name": "active", "topic": "sim/plant/VibrationHigh_Screen2/active" }
174
+ ],
175
+ "condition": "active == true",
176
+ "severity": "500",
177
+ "outputTopic": "alarm/VibrationHigh_Screen2",
178
+ "x": 970,
179
+ "y": 340,
180
+ "wires": [["debug-sim-raised"], ["debug-sim-acked"], ["debug-sim-cleared"], ["debug-sim-resolved"]]
181
+ },
182
+ {
183
+ "id": "sim-alarm-estop",
184
+ "type": "event-alarm",
185
+ "z": "alarm-sim-flow",
186
+ "name": "Conveyor 7 EStop",
187
+ "cache": "sim-cache",
188
+ "conditionId": "EStop_Conveyor7",
189
+ "conditionName": "Conveyor 7 Emergency Stop",
190
+ "inputMappings": [
191
+ { "name": "active", "topic": "sim/plant/EStop_Conveyor7/active" }
192
+ ],
193
+ "condition": "active == true",
194
+ "severity": "1000",
195
+ "outputTopic": "alarm/EStop_Conveyor7",
196
+ "x": 970,
197
+ "y": 420,
198
+ "wires": [["debug-sim-raised"], ["debug-sim-acked"], ["debug-sim-cleared"], ["debug-sim-resolved"]]
199
+ },
200
+ {
201
+ "id": "sim-alarm-magnetite",
202
+ "type": "event-alarm",
203
+ "z": "alarm-sim-flow",
204
+ "name": "Magnetite Level Low",
205
+ "cache": "sim-cache",
206
+ "conditionId": "MagnetiteLevel_Low",
207
+ "conditionName": "Magnetite Storage Tank Level Low",
208
+ "inputMappings": [
209
+ { "name": "active", "topic": "sim/plant/MagnetiteLevel_Low/active" }
210
+ ],
211
+ "condition": "active == true",
212
+ "severity": "400",
213
+ "outputTopic": "alarm/MagnetiteLevel_Low",
214
+ "x": 970,
215
+ "y": 500,
216
+ "wires": [["debug-sim-raised"], ["debug-sim-acked"], ["debug-sim-cleared"], ["debug-sim-resolved"]]
217
+ },
218
+ {
219
+ "id": "comment-debug-outputs",
220
+ "type": "comment",
221
+ "z": "alarm-sim-flow",
222
+ "name": "--- Lifecycle Debug Outputs ---",
223
+ "info": "",
224
+ "x": 1270,
225
+ "y": 40,
226
+ "wires": []
227
+ },
228
+ {
229
+ "id": "debug-sim-raised",
230
+ "type": "debug",
231
+ "z": "alarm-sim-flow",
232
+ "name": "RAISED",
233
+ "active": true,
234
+ "tosidebar": true,
235
+ "console": false,
236
+ "tostatus": true,
237
+ "statusVal": "payload.condition_name",
238
+ "statusType": "msg",
239
+ "x": 1240,
240
+ "y": 100,
241
+ "wires": []
242
+ },
243
+ {
244
+ "id": "debug-sim-acked",
245
+ "type": "debug",
246
+ "z": "alarm-sim-flow",
247
+ "name": "ACKNOWLEDGED",
248
+ "active": true,
249
+ "tosidebar": true,
250
+ "console": false,
251
+ "tostatus": true,
252
+ "statusVal": "payload.condition_name",
253
+ "statusType": "msg",
254
+ "x": 1260,
255
+ "y": 180,
256
+ "wires": []
257
+ },
258
+ {
259
+ "id": "debug-sim-cleared",
260
+ "type": "debug",
261
+ "z": "alarm-sim-flow",
262
+ "name": "CLEARED",
263
+ "active": true,
264
+ "tosidebar": true,
265
+ "console": false,
266
+ "tostatus": true,
267
+ "statusVal": "payload.condition_name",
268
+ "statusType": "msg",
269
+ "x": 1250,
270
+ "y": 260,
271
+ "wires": []
272
+ },
273
+ {
274
+ "id": "debug-sim-resolved",
275
+ "type": "debug",
276
+ "z": "alarm-sim-flow",
277
+ "name": "RESOLVED",
278
+ "active": true,
279
+ "tosidebar": true,
280
+ "console": false,
281
+ "tostatus": true,
282
+ "statusVal": "payload.condition_name",
283
+ "statusType": "msg",
284
+ "x": 1250,
285
+ "y": 340,
286
+ "wires": []
287
+ }
288
+ ]