node-red-contrib-event-calc 0.1.1
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 +247 -0
- package/examples/event-calc-example.json +184 -0
- package/nodes/event-cache.html +30 -0
- package/nodes/event-cache.js +327 -0
- package/nodes/event-calc.html +307 -0
- package/nodes/event-calc.js +217 -0
- package/nodes/event-in.html +66 -0
- package/nodes/event-in.js +80 -0
- package/nodes/event-topic.html +130 -0
- package/nodes/event-topic.js +140 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# node-red-contrib-event-calc
|
|
2
|
+
|
|
3
|
+
Node-RED nodes for event caching and calculations with topic wildcard patterns.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides a central cache for event/streaming data values with reactive updates. It enables subscription to topic patterns and calculations when values change, making it easy to build event-driven data processing flows.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
13
|
+
│ event-cache (config node) │
|
|
14
|
+
│ • Stores: Map<topic, {value, ts, metadata}> │
|
|
15
|
+
│ • Event emitter for topic updates │
|
|
16
|
+
│ • Wildcard pattern matching │
|
|
17
|
+
└──────────────────────────────────────────────────────────────┘
|
|
18
|
+
│ │ │
|
|
19
|
+
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
|
20
|
+
│event-in │ │event- │ │event- │
|
|
21
|
+
│ │ │topic │ │calc │
|
|
22
|
+
│ pushes │ │subscribes│ │multi-sub│
|
|
23
|
+
│to cache │ │to pattern│ │+ expr │
|
|
24
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install node-red-contrib-event-calc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install directly from the Node-RED palette manager.
|
|
34
|
+
|
|
35
|
+
## Nodes
|
|
36
|
+
|
|
37
|
+
### event-cache (Config Node)
|
|
38
|
+
|
|
39
|
+
Central cache that stores topic values and manages subscriptions. Configure:
|
|
40
|
+
|
|
41
|
+
- **Max Entries**: Maximum topics to cache (default: 10000). Oldest entries removed when exceeded.
|
|
42
|
+
- **TTL**: Time-to-live in milliseconds. Set to 0 for no expiry.
|
|
43
|
+
|
|
44
|
+
### event-in
|
|
45
|
+
|
|
46
|
+
Receives messages from any upstream node and pushes values to the cache.
|
|
47
|
+
|
|
48
|
+
**Properties:**
|
|
49
|
+
- **Topic Field**: Where to get the topic (default: `msg.topic`)
|
|
50
|
+
- **Value Field**: Where to get the value (default: `msg.payload`)
|
|
51
|
+
|
|
52
|
+
The original message passes through, allowing insertion into existing flows.
|
|
53
|
+
|
|
54
|
+
### event-topic
|
|
55
|
+
|
|
56
|
+
Subscribes to a topic pattern and outputs when matching topics update.
|
|
57
|
+
|
|
58
|
+
**Properties:**
|
|
59
|
+
- **Topic Pattern**: Pattern with wildcards (`?` for single level, `*` for any levels)
|
|
60
|
+
- **Output Format**:
|
|
61
|
+
- *Value only*: `msg.payload` = value
|
|
62
|
+
- *Full entry*: `msg.payload` = `{value, ts, metadata}`
|
|
63
|
+
- *All matching*: `msg.payload` = `{topic1: value1, topic2: value2, ...}`
|
|
64
|
+
- **Output on deploy**: Emit cached values when flow starts
|
|
65
|
+
|
|
66
|
+
**Dynamic control via input:**
|
|
67
|
+
- `msg.pattern`: Change subscription pattern
|
|
68
|
+
- `msg.payload = 'refresh'`: Output all currently cached values
|
|
69
|
+
|
|
70
|
+
### event-calc
|
|
71
|
+
|
|
72
|
+
Subscribes to multiple topics and evaluates an expression when values update.
|
|
73
|
+
|
|
74
|
+
**Properties:**
|
|
75
|
+
- **Input Variables**: Map variable names to topic patterns
|
|
76
|
+
- **Expression**: JavaScript expression using the variables
|
|
77
|
+
- **Trigger**: When to calculate
|
|
78
|
+
- *Any input updates*: Calculate on every update
|
|
79
|
+
- *Only when all inputs have values*: Wait for all values
|
|
80
|
+
|
|
81
|
+
**Output:**
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"topic": "calc/result",
|
|
85
|
+
"payload": 21.5,
|
|
86
|
+
"inputs": {
|
|
87
|
+
"a": { "topic": "sensors/room1/temp", "value": 22, "ts": 1704000000000 },
|
|
88
|
+
"b": { "topic": "sensors/room2/temp", "value": 21, "ts": 1704000001000 }
|
|
89
|
+
},
|
|
90
|
+
"expression": "(a + b) / 2",
|
|
91
|
+
"trigger": "sensors/room1/temp"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Wildcard Patterns
|
|
96
|
+
|
|
97
|
+
Two wildcards are supported:
|
|
98
|
+
|
|
99
|
+
| Pattern | Matches | Doesn't Match |
|
|
100
|
+
|---------|---------|---------------|
|
|
101
|
+
| `sensor?` | `sensor1`, `sensorA` | `sensor`, `sensor12` |
|
|
102
|
+
| `sensors/*` | `sensors/temp`, `sensors/room1/temp` | `sensors` (nothing after /) |
|
|
103
|
+
| `*/temp` | `room/temp`, `sensors/temp` | `temp` (nothing before /) |
|
|
104
|
+
| `*` | Any topic with 1+ chars | Empty string |
|
|
105
|
+
|
|
106
|
+
- `?` matches exactly one character
|
|
107
|
+
- `*` matches one or more characters
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
### Average Temperature
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
[inject: room1/temp] → [event-in] → [cache]
|
|
115
|
+
[inject: room2/temp] → [event-in] → [cache]
|
|
116
|
+
|
|
117
|
+
[event-calc] → [debug]
|
|
118
|
+
inputs: a = sensors/room1/temp
|
|
119
|
+
b = sensors/room2/temp
|
|
120
|
+
expression: (a + b) / 2
|
|
121
|
+
trigger: all
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Monitor All Sensors
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
[any-input: sensors/*] → [event-in] → [cache]
|
|
128
|
+
|
|
129
|
+
[event-topic: sensors/*] → [debug]
|
|
130
|
+
outputFormat: all
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Calculate Power (Voltage × Current)
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
[event-calc]
|
|
137
|
+
inputs: v = power/voltage
|
|
138
|
+
i = power/current
|
|
139
|
+
expression: v * i
|
|
140
|
+
topic: power/watts
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Built-in Functions
|
|
144
|
+
|
|
145
|
+
### Math
|
|
146
|
+
| Function | Description |
|
|
147
|
+
|----------|-------------|
|
|
148
|
+
| `min(a, b, ...)` | Minimum value |
|
|
149
|
+
| `max(a, b, ...)` | Maximum value |
|
|
150
|
+
| `abs(x)` | Absolute value |
|
|
151
|
+
| `sqrt(x)` | Square root |
|
|
152
|
+
| `pow(base, exp)` | Power |
|
|
153
|
+
| `log(x)`, `log10(x)` | Logarithms |
|
|
154
|
+
| `floor(x)`, `ceil(x)` | Rounding |
|
|
155
|
+
| `sin(x)`, `cos(x)`, `tan(x)` | Trigonometry |
|
|
156
|
+
| `PI`, `E` | Constants |
|
|
157
|
+
|
|
158
|
+
### Aggregation
|
|
159
|
+
| Function | Description |
|
|
160
|
+
|----------|-------------|
|
|
161
|
+
| `sum(a, b, ...)` | Sum of values |
|
|
162
|
+
| `avg(a, b, ...)` | Average of values |
|
|
163
|
+
| `count(a, b, ...)` | Count of values |
|
|
164
|
+
|
|
165
|
+
### Utility
|
|
166
|
+
| Function | Description |
|
|
167
|
+
|----------|-------------|
|
|
168
|
+
| `round(value, decimals)` | Round to N decimals |
|
|
169
|
+
| `clamp(value, min, max)` | Constrain to range |
|
|
170
|
+
| `map(value, inMin, inMax, outMin, outMax)` | Scale between ranges |
|
|
171
|
+
| `lerp(a, b, t)` | Linear interpolation |
|
|
172
|
+
| `ifelse(cond, trueVal, falseVal)` | Conditional |
|
|
173
|
+
| `between(value, min, max)` | Range check (returns boolean) |
|
|
174
|
+
| `delta(current, previous)` | Difference |
|
|
175
|
+
| `pctChange(current, previous)` | Percentage change |
|
|
176
|
+
|
|
177
|
+
## Expression Examples
|
|
178
|
+
|
|
179
|
+
| Expression | Description |
|
|
180
|
+
|------------|-------------|
|
|
181
|
+
| `a + b` | Sum |
|
|
182
|
+
| `avg(a, b)` | Average |
|
|
183
|
+
| `max(a, b, c)` | Maximum |
|
|
184
|
+
| `sqrt(a*a + b*b)` | Pythagorean |
|
|
185
|
+
| `round(a, 2)` | Round to 2 decimals |
|
|
186
|
+
| `clamp(a, 0, 100)` | Constrain 0-100 |
|
|
187
|
+
| `map(a, 0, 1023, 0, 100)` | Scale ADC to % |
|
|
188
|
+
| `ifelse(a > b, 'high', 'low')` | Conditional |
|
|
189
|
+
| `pctChange(a, b)` | % change from b to a |
|
|
190
|
+
|
|
191
|
+
## Scalability
|
|
192
|
+
|
|
193
|
+
The event-cache is optimized for high subscriber counts:
|
|
194
|
+
|
|
195
|
+
| Subscription Type | Lookup Complexity | Best For |
|
|
196
|
+
|-------------------|-------------------|----------|
|
|
197
|
+
| Exact topic (e.g., `sensors/room1/temp`) | O(1) | High-frequency updates, many subscribers |
|
|
198
|
+
| Wildcard pattern (e.g., `sensors/*`) | O(w) | Flexible matching, fewer patterns |
|
|
199
|
+
|
|
200
|
+
Where `w` = number of wildcard subscriptions (typically much smaller than total subscribers).
|
|
201
|
+
|
|
202
|
+
**Example performance:**
|
|
203
|
+
- 1000 exact subscriptions to different topics: O(1) per update
|
|
204
|
+
- 10 wildcard patterns + 1000 exact subscriptions: O(10) per update
|
|
205
|
+
- Pure wildcard subscriptions: O(n) per update
|
|
206
|
+
|
|
207
|
+
**Recommendations for high scale:**
|
|
208
|
+
- Prefer exact topic matches when possible
|
|
209
|
+
- Use wildcards sparingly for monitoring/logging
|
|
210
|
+
- Check stats endpoint: `GET /event-cache/:id/stats`
|
|
211
|
+
|
|
212
|
+
## API (for custom nodes)
|
|
213
|
+
|
|
214
|
+
The event-cache node exposes methods for programmatic access:
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
const cache = RED.nodes.getNode(configId);
|
|
218
|
+
|
|
219
|
+
// Set a value
|
|
220
|
+
cache.setValue('topic/path', 42, { source: 'sensor' });
|
|
221
|
+
|
|
222
|
+
// Get a value
|
|
223
|
+
const entry = cache.getValue('topic/path');
|
|
224
|
+
// { value: 42, ts: 1704000000000, metadata: { source: 'sensor' } }
|
|
225
|
+
|
|
226
|
+
// Get matching values
|
|
227
|
+
const temps = cache.getMatching('sensors/*');
|
|
228
|
+
// Map { 'sensors/room1/temp' => {...}, 'sensors/room2/temp' => {...} }
|
|
229
|
+
|
|
230
|
+
// Subscribe to updates
|
|
231
|
+
const subId = cache.subscribe('sensors/*', (topic, entry) => {
|
|
232
|
+
console.log(`${topic} = ${entry.value}`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Unsubscribe
|
|
236
|
+
cache.unsubscribe(subId);
|
|
237
|
+
|
|
238
|
+
// Get all topics
|
|
239
|
+
const topics = cache.getTopics();
|
|
240
|
+
|
|
241
|
+
// Clear cache
|
|
242
|
+
cache.clear();
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "event-calc-example-flow",
|
|
4
|
+
"type": "tab",
|
|
5
|
+
"label": "Event Calc Example",
|
|
6
|
+
"disabled": false,
|
|
7
|
+
"info": "This flow demonstrates the event-calc nodes:\n- event-in: feeds data into the cache\n- event-topic: subscribes to topic patterns\n- event-calc: performs calculations on cached values"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "cache1",
|
|
11
|
+
"type": "event-cache",
|
|
12
|
+
"name": "Main Cache",
|
|
13
|
+
"maxEntries": "10000",
|
|
14
|
+
"ttl": "0"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "inject-temp1",
|
|
18
|
+
"type": "inject",
|
|
19
|
+
"z": "event-calc-example-flow",
|
|
20
|
+
"name": "Room 1 Temp",
|
|
21
|
+
"props": [
|
|
22
|
+
{ "p": "topic", "vt": "str" },
|
|
23
|
+
{ "p": "payload" }
|
|
24
|
+
],
|
|
25
|
+
"topic": "sensors/room1/temp",
|
|
26
|
+
"payload": "",
|
|
27
|
+
"payloadType": "date",
|
|
28
|
+
"repeat": "",
|
|
29
|
+
"crontab": "",
|
|
30
|
+
"once": false,
|
|
31
|
+
"onceDelay": 0.1,
|
|
32
|
+
"x": 130,
|
|
33
|
+
"y": 100,
|
|
34
|
+
"wires": [["random-temp1"]]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "random-temp1",
|
|
38
|
+
"type": "function",
|
|
39
|
+
"z": "event-calc-example-flow",
|
|
40
|
+
"name": "Random 20-25",
|
|
41
|
+
"func": "msg.payload = 20 + Math.random() * 5;\nreturn msg;",
|
|
42
|
+
"outputs": 1,
|
|
43
|
+
"x": 300,
|
|
44
|
+
"y": 100,
|
|
45
|
+
"wires": [["event-in1"]]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "inject-temp2",
|
|
49
|
+
"type": "inject",
|
|
50
|
+
"z": "event-calc-example-flow",
|
|
51
|
+
"name": "Room 2 Temp",
|
|
52
|
+
"props": [
|
|
53
|
+
{ "p": "topic", "vt": "str" },
|
|
54
|
+
{ "p": "payload" }
|
|
55
|
+
],
|
|
56
|
+
"topic": "sensors/room2/temp",
|
|
57
|
+
"payload": "",
|
|
58
|
+
"payloadType": "date",
|
|
59
|
+
"repeat": "",
|
|
60
|
+
"crontab": "",
|
|
61
|
+
"once": false,
|
|
62
|
+
"onceDelay": 0.1,
|
|
63
|
+
"x": 130,
|
|
64
|
+
"y": 160,
|
|
65
|
+
"wires": [["random-temp2"]]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "random-temp2",
|
|
69
|
+
"type": "function",
|
|
70
|
+
"z": "event-calc-example-flow",
|
|
71
|
+
"name": "Random 18-23",
|
|
72
|
+
"func": "msg.payload = 18 + Math.random() * 5;\nreturn msg;",
|
|
73
|
+
"outputs": 1,
|
|
74
|
+
"x": 300,
|
|
75
|
+
"y": 160,
|
|
76
|
+
"wires": [["event-in1"]]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "event-in1",
|
|
80
|
+
"type": "event-in",
|
|
81
|
+
"z": "event-calc-example-flow",
|
|
82
|
+
"name": "",
|
|
83
|
+
"cache": "cache1",
|
|
84
|
+
"topicField": "topic",
|
|
85
|
+
"valueField": "payload",
|
|
86
|
+
"x": 490,
|
|
87
|
+
"y": 130,
|
|
88
|
+
"wires": [[]]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "event-topic1",
|
|
92
|
+
"type": "event-topic",
|
|
93
|
+
"z": "event-calc-example-flow",
|
|
94
|
+
"name": "All Temps",
|
|
95
|
+
"cache": "cache1",
|
|
96
|
+
"pattern": "sensors/*/temp",
|
|
97
|
+
"outputFormat": "value",
|
|
98
|
+
"outputOnStart": false,
|
|
99
|
+
"x": 490,
|
|
100
|
+
"y": 240,
|
|
101
|
+
"wires": [["debug-topic"]]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "debug-topic",
|
|
105
|
+
"type": "debug",
|
|
106
|
+
"z": "event-calc-example-flow",
|
|
107
|
+
"name": "Topic Updates",
|
|
108
|
+
"active": true,
|
|
109
|
+
"tosidebar": true,
|
|
110
|
+
"console": false,
|
|
111
|
+
"tostatus": true,
|
|
112
|
+
"complete": "true",
|
|
113
|
+
"targetType": "full",
|
|
114
|
+
"statusVal": "payload",
|
|
115
|
+
"statusType": "auto",
|
|
116
|
+
"x": 690,
|
|
117
|
+
"y": 240,
|
|
118
|
+
"wires": []
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "event-calc1",
|
|
122
|
+
"type": "event-calc",
|
|
123
|
+
"z": "event-calc-example-flow",
|
|
124
|
+
"name": "Avg Temp",
|
|
125
|
+
"cache": "cache1",
|
|
126
|
+
"inputMappings": [
|
|
127
|
+
{ "name": "a", "pattern": "sensors/room1/temp" },
|
|
128
|
+
{ "name": "b", "pattern": "sensors/room2/temp" }
|
|
129
|
+
],
|
|
130
|
+
"expression": "(a + b) / 2",
|
|
131
|
+
"triggerOn": "all",
|
|
132
|
+
"outputTopic": "calc/avg_temp",
|
|
133
|
+
"x": 490,
|
|
134
|
+
"y": 320,
|
|
135
|
+
"wires": [["debug-calc"]]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "debug-calc",
|
|
139
|
+
"type": "debug",
|
|
140
|
+
"z": "event-calc-example-flow",
|
|
141
|
+
"name": "Calculation",
|
|
142
|
+
"active": true,
|
|
143
|
+
"tosidebar": true,
|
|
144
|
+
"console": false,
|
|
145
|
+
"tostatus": true,
|
|
146
|
+
"complete": "true",
|
|
147
|
+
"targetType": "full",
|
|
148
|
+
"statusVal": "payload",
|
|
149
|
+
"statusType": "auto",
|
|
150
|
+
"x": 690,
|
|
151
|
+
"y": 320,
|
|
152
|
+
"wires": []
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "comment1",
|
|
156
|
+
"type": "comment",
|
|
157
|
+
"z": "event-calc-example-flow",
|
|
158
|
+
"name": "1. Click inject buttons to simulate sensor readings",
|
|
159
|
+
"info": "",
|
|
160
|
+
"x": 220,
|
|
161
|
+
"y": 40,
|
|
162
|
+
"wires": []
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "comment2",
|
|
166
|
+
"type": "comment",
|
|
167
|
+
"z": "event-calc-example-flow",
|
|
168
|
+
"name": "2. event-topic outputs when any temp updates",
|
|
169
|
+
"info": "",
|
|
170
|
+
"x": 590,
|
|
171
|
+
"y": 200,
|
|
172
|
+
"wires": []
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "comment3",
|
|
176
|
+
"type": "comment",
|
|
177
|
+
"z": "event-calc-example-flow",
|
|
178
|
+
"name": "3. event-calc computes average (waits for both values)",
|
|
179
|
+
"info": "",
|
|
180
|
+
"x": 620,
|
|
181
|
+
"y": 280,
|
|
182
|
+
"wires": []
|
|
183
|
+
}
|
|
184
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('event-cache', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: "" },
|
|
6
|
+
maxEntries: { value: 10000, validate: RED.validators.number() },
|
|
7
|
+
ttl: { value: 0, validate: RED.validators.number() }
|
|
8
|
+
},
|
|
9
|
+
label: function() {
|
|
10
|
+
return this.name || "Event Cache";
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script type="text/html" data-template-name="event-cache">
|
|
16
|
+
<div class="form-row">
|
|
17
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
18
|
+
<input type="text" id="node-config-input-name" placeholder="Event Cache">
|
|
19
|
+
</div>
|
|
20
|
+
<div class="form-row">
|
|
21
|
+
<label for="node-config-input-maxEntries"><i class="fa fa-database"></i> Max Entries</label>
|
|
22
|
+
<input type="number" id="node-config-input-maxEntries" placeholder="10000">
|
|
23
|
+
<div class="form-tips">Maximum number of topics to cache. Oldest entries are removed when limit is reached.</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label for="node-config-input-ttl"><i class="fa fa-clock-o"></i> TTL (ms)</label>
|
|
27
|
+
<input type="number" id="node-config-input-ttl" placeholder="0">
|
|
28
|
+
<div class="form-tips">Time-to-live in milliseconds. Set to 0 for no expiry.</div>
|
|
29
|
+
</div>
|
|
30
|
+
</script>
|