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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * event-calc - Calculation node for multi-topic expressions
3
+ *
4
+ * Features:
5
+ * - Maps multiple variables to topic patterns
6
+ * - Evaluates JavaScript expressions when inputs update
7
+ * - Trigger modes: 'any' (any input updates) or 'all' (all inputs have values)
8
+ * - Safe expression evaluation using Function constructor
9
+ * - Dynamic expression update via input message
10
+ * - Built-in helper functions for common operations
11
+ */
12
+ module.exports = function(RED) {
13
+ // Helper functions available in expressions
14
+ const helpers = {
15
+ // Math shortcuts
16
+ min: (...args) => Math.min(...args.flat()),
17
+ max: (...args) => Math.max(...args.flat()),
18
+ abs: (x) => Math.abs(x),
19
+ sqrt: (x) => Math.sqrt(x),
20
+ pow: (base, exp) => Math.pow(base, exp),
21
+ log: (x) => Math.log(x),
22
+ log10: (x) => Math.log10(x),
23
+ exp: (x) => Math.exp(x),
24
+ floor: (x) => Math.floor(x),
25
+ ceil: (x) => Math.ceil(x),
26
+ sin: (x) => Math.sin(x),
27
+ cos: (x) => Math.cos(x),
28
+ tan: (x) => Math.tan(x),
29
+ PI: Math.PI,
30
+ E: Math.E,
31
+
32
+ // Aggregation
33
+ sum: (...args) => args.flat().reduce((a, b) => a + b, 0),
34
+ avg: (...args) => {
35
+ const flat = args.flat();
36
+ return flat.length > 0 ? flat.reduce((a, b) => a + b, 0) / flat.length : 0;
37
+ },
38
+ count: (...args) => args.flat().length,
39
+
40
+ // Utility
41
+ round: (value, decimals = 0) => {
42
+ const factor = Math.pow(10, decimals);
43
+ return Math.round(value * factor) / factor;
44
+ },
45
+ clamp: (value, min, max) => Math.min(Math.max(value, min), max),
46
+ map: (value, inMin, inMax, outMin, outMax) => {
47
+ return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
48
+ },
49
+ lerp: (a, b, t) => a + (b - a) * t,
50
+
51
+ // Boolean/conditional helpers
52
+ ifelse: (condition, trueVal, falseVal) => condition ? trueVal : falseVal,
53
+ between: (value, min, max) => value >= min && value <= max,
54
+
55
+ // Delta/change detection (returns difference)
56
+ delta: (current, previous) => current - previous,
57
+ pctChange: (current, previous) => previous !== 0 ? ((current - previous) / previous) * 100 : 0
58
+ };
59
+ function EventCalcNode(config) {
60
+ RED.nodes.createNode(this, config);
61
+ const node = this;
62
+
63
+ node.cacheConfig = RED.nodes.getNode(config.cache);
64
+ node.inputMappings = config.inputMappings || [];
65
+ node.expression = config.expression || '';
66
+ node.triggerOn = config.triggerOn || 'any';
67
+ node.outputTopic = config.outputTopic || 'calc/result';
68
+
69
+ const subscriptionIds = [];
70
+ const latestValues = new Map(); // name -> { topic, value, ts }
71
+
72
+ if (!node.cacheConfig) {
73
+ node.status({ fill: "red", shape: "ring", text: "no cache configured" });
74
+ return;
75
+ }
76
+
77
+ if (node.inputMappings.length === 0) {
78
+ node.status({ fill: "yellow", shape: "ring", text: "no inputs defined" });
79
+ return;
80
+ }
81
+
82
+ if (!node.expression) {
83
+ node.status({ fill: "yellow", shape: "ring", text: "no expression" });
84
+ return;
85
+ }
86
+
87
+ /**
88
+ * Attempt to calculate and output result
89
+ */
90
+ function tryCalculate(triggerTopic) {
91
+ // Check if we should trigger
92
+ if (node.triggerOn === 'all') {
93
+ // All inputs must have values
94
+ for (const input of node.inputMappings) {
95
+ if (!latestValues.has(input.name)) {
96
+ return; // Not all values available yet
97
+ }
98
+ }
99
+ }
100
+
101
+ // At least one value must exist to calculate
102
+ if (latestValues.size === 0) {
103
+ return;
104
+ }
105
+
106
+ // Build context object for expression evaluation
107
+ const context = {};
108
+ const inputDetails = {};
109
+
110
+ for (const input of node.inputMappings) {
111
+ const data = latestValues.get(input.name);
112
+ if (data) {
113
+ context[input.name] = data.value;
114
+ inputDetails[input.name] = {
115
+ topic: data.topic,
116
+ value: data.value,
117
+ ts: data.ts
118
+ };
119
+ } else {
120
+ context[input.name] = undefined;
121
+ }
122
+ }
123
+
124
+ // Evaluate expression safely
125
+ try {
126
+ // Create a function with named parameters from context + helpers
127
+ const allParams = { ...helpers, ...context };
128
+ const paramNames = Object.keys(allParams);
129
+ const paramValues = Object.values(allParams);
130
+
131
+ // Build function body with helpers and variables available
132
+ const fn = new Function(...paramNames, `return ${node.expression};`);
133
+ const result = fn(...paramValues);
134
+
135
+ const msg = {
136
+ topic: node.outputTopic,
137
+ payload: result,
138
+ inputs: inputDetails,
139
+ expression: node.expression,
140
+ trigger: triggerTopic,
141
+ timestamp: Date.now()
142
+ };
143
+
144
+ node.send(msg);
145
+
146
+ // Store result back in cache so it can be used by other calculations
147
+ node.cacheConfig.setValue(node.outputTopic, result, {
148
+ source: 'event-calc',
149
+ expression: node.expression,
150
+ inputs: Object.keys(inputDetails)
151
+ });
152
+
153
+ // Update status with result (truncate if too long)
154
+ const resultStr = String(result);
155
+ const displayResult = resultStr.length > 15 ? resultStr.substring(0, 12) + '...' : resultStr;
156
+ node.status({ fill: "green", shape: "dot", text: `= ${displayResult}` });
157
+
158
+ } catch (err) {
159
+ node.status({ fill: "red", shape: "ring", text: "eval error" });
160
+ node.error(`Expression evaluation failed: ${err.message}`, { expression: node.expression, context: context });
161
+ }
162
+ }
163
+
164
+ // Subscribe to each input pattern
165
+ for (const input of node.inputMappings) {
166
+ if (!input.name || !input.pattern) {
167
+ continue;
168
+ }
169
+
170
+ const subId = node.cacheConfig.subscribe(input.pattern, (topic, entry) => {
171
+ latestValues.set(input.name, {
172
+ topic: topic,
173
+ value: entry.value,
174
+ ts: entry.ts
175
+ });
176
+
177
+ tryCalculate(topic);
178
+ });
179
+ subscriptionIds.push(subId);
180
+ }
181
+
182
+ node.status({ fill: "green", shape: "dot", text: "ready" });
183
+
184
+ // Handle input messages for dynamic updates
185
+ node.on('input', function(msg, send, done) {
186
+ // For Node-RED 0.x compatibility
187
+ send = send || function() { node.send.apply(node, arguments); };
188
+ done = done || function(err) { if (err) node.error(err, msg); };
189
+
190
+ // Allow expression update via message
191
+ if (msg.expression && typeof msg.expression === 'string') {
192
+ node.expression = msg.expression;
193
+ node.status({ fill: "blue", shape: "dot", text: "expr updated" });
194
+ }
195
+
196
+ // Force recalculation
197
+ if (msg.payload === 'recalc' || msg.topic === 'recalc') {
198
+ tryCalculate('manual');
199
+ }
200
+
201
+ done();
202
+ });
203
+
204
+ node.on('close', function(done) {
205
+ for (const subId of subscriptionIds) {
206
+ if (node.cacheConfig) {
207
+ node.cacheConfig.unsubscribe(subId);
208
+ }
209
+ }
210
+ subscriptionIds.length = 0;
211
+ latestValues.clear();
212
+ done();
213
+ });
214
+ }
215
+
216
+ RED.nodes.registerType("event-calc", EventCalcNode);
217
+ };
@@ -0,0 +1,66 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('event-in', {
3
+ category: 'event calc',
4
+ color: '#87CEEB',
5
+ defaults: {
6
+ name: { value: "" },
7
+ cache: { value: "", type: "event-cache", required: true },
8
+ topicField: { value: "topic" },
9
+ valueField: { value: "payload" }
10
+ },
11
+ inputs: 1,
12
+ outputs: 1,
13
+ icon: "font-awesome/fa-arrow-circle-right",
14
+ label: function() {
15
+ return this.name || "event in";
16
+ },
17
+ paletteLabel: "event in"
18
+ });
19
+ </script>
20
+
21
+ <script type="text/html" data-template-name="event-in">
22
+ <div class="form-row">
23
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
24
+ <input type="text" id="node-input-name" placeholder="Name">
25
+ </div>
26
+ <div class="form-row">
27
+ <label for="node-input-cache"><i class="fa fa-database"></i> Cache</label>
28
+ <input type="text" id="node-input-cache">
29
+ </div>
30
+ <div class="form-row">
31
+ <label for="node-input-topicField"><i class="fa fa-bookmark"></i> Topic Field</label>
32
+ <input type="text" id="node-input-topicField" placeholder="topic">
33
+ <div class="form-tips">Message property containing the topic (e.g., <code>topic</code> for <code>msg.topic</code>)</div>
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-valueField"><i class="fa fa-cube"></i> Value Field</label>
37
+ <input type="text" id="node-input-valueField" placeholder="payload">
38
+ <div class="form-tips">Message property containing the value (e.g., <code>payload</code> for <code>msg.payload</code>)</div>
39
+ </div>
40
+ </script>
41
+
42
+ <script type="text/html" data-help-name="event-in">
43
+ <p>Receives messages and pushes values to the event cache.</p>
44
+
45
+ <h3>Properties</h3>
46
+ <dl class="message-properties">
47
+ <dt>Cache</dt>
48
+ <dd>The event-cache config node to use</dd>
49
+ <dt>Topic Field</dt>
50
+ <dd>Message property containing the topic (default: <code>msg.topic</code>)</dd>
51
+ <dt>Value Field</dt>
52
+ <dd>Message property containing the value (default: <code>msg.payload</code>)</dd>
53
+ </dl>
54
+
55
+ <h3>Inputs</h3>
56
+ <p>Any message with a topic and value. The topic is used as the cache key.</p>
57
+
58
+ <h3>Outputs</h3>
59
+ <p>The original message is passed through unchanged, allowing this node to be inserted into existing flows.</p>
60
+
61
+ <h3>Details</h3>
62
+ <p>This node extracts a topic and value from incoming messages and stores them in the configured event-cache.
63
+ Other nodes (event-topic, event-calc) can then subscribe to these cached values.</p>
64
+
65
+ <p>Use this node to feed data from any source into the event cache system.</p>
66
+ </script>
@@ -0,0 +1,80 @@
1
+ /**
2
+ * event-in - Input node that pushes data to the event cache
3
+ *
4
+ * Features:
5
+ * - Receives messages from any upstream Node-RED node
6
+ * - Configurable topic and value extraction from message
7
+ * - Pass-through: forwards original message after caching
8
+ */
9
+ module.exports = function(RED) {
10
+ function EventInNode(config) {
11
+ RED.nodes.createNode(this, config);
12
+ const node = this;
13
+
14
+ node.cacheConfig = RED.nodes.getNode(config.cache);
15
+ node.topicField = config.topicField || 'topic';
16
+ node.valueField = config.valueField || 'payload';
17
+
18
+ if (!node.cacheConfig) {
19
+ node.status({ fill: "red", shape: "ring", text: "no cache configured" });
20
+ return;
21
+ }
22
+
23
+ node.status({ fill: "green", shape: "dot", text: "ready" });
24
+
25
+ node.on('input', function(msg, send, done) {
26
+ // For Node-RED 0.x compatibility
27
+ send = send || function() { node.send.apply(node, arguments); };
28
+ done = done || function(err) { if (err) node.error(err, msg); };
29
+
30
+ try {
31
+ // Extract topic using RED.util.getMessageProperty
32
+ let topic;
33
+ if (node.topicField.startsWith('msg.')) {
34
+ topic = RED.util.getMessageProperty(msg, node.topicField.substring(4));
35
+ } else {
36
+ topic = RED.util.getMessageProperty(msg, node.topicField);
37
+ }
38
+
39
+ if (!topic || typeof topic !== 'string') {
40
+ node.status({ fill: "yellow", shape: "ring", text: "missing topic" });
41
+ done(new Error(`Topic not found at msg.${node.topicField} or not a string`));
42
+ return;
43
+ }
44
+
45
+ // Extract value
46
+ let value;
47
+ if (node.valueField.startsWith('msg.')) {
48
+ value = RED.util.getMessageProperty(msg, node.valueField.substring(4));
49
+ } else {
50
+ value = RED.util.getMessageProperty(msg, node.valueField);
51
+ }
52
+
53
+ // Build metadata from msg properties
54
+ const metadata = {
55
+ _msgid: msg._msgid
56
+ };
57
+
58
+ // Push to cache
59
+ node.cacheConfig.setValue(topic, value, metadata);
60
+
61
+ // Truncate topic for status display
62
+ const displayTopic = topic.length > 20 ? topic.substring(0, 17) + '...' : topic;
63
+ node.status({ fill: "green", shape: "dot", text: displayTopic });
64
+
65
+ // Pass through the message
66
+ send(msg);
67
+ done();
68
+ } catch (err) {
69
+ node.status({ fill: "red", shape: "ring", text: "error" });
70
+ done(err);
71
+ }
72
+ });
73
+
74
+ node.on('close', function(done) {
75
+ done();
76
+ });
77
+ }
78
+
79
+ RED.nodes.registerType("event-in", EventInNode);
80
+ };
@@ -0,0 +1,130 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('event-topic', {
3
+ category: 'event calc',
4
+ color: '#87CEEB',
5
+ defaults: {
6
+ name: { value: "" },
7
+ cache: { value: "", type: "event-cache", required: true },
8
+ pattern: { value: "*" },
9
+ outputFormat: { value: "value" },
10
+ outputOnStart: { value: false }
11
+ },
12
+ inputs: 1,
13
+ outputs: 1,
14
+ icon: "font-awesome/fa-filter",
15
+ label: function() {
16
+ return this.name || this.pattern || "event topic";
17
+ },
18
+ paletteLabel: "event topic",
19
+ oneditprepare: function() {
20
+ const node = this;
21
+
22
+ // Fetch topics from cache for autocomplete
23
+ function fetchTopics() {
24
+ const cacheId = $("#node-input-cache").val();
25
+ if (cacheId) {
26
+ $.getJSON("event-cache/" + cacheId + "/topics", function(topics) {
27
+ $("#node-input-pattern").autocomplete("option", "source", topics || []);
28
+ });
29
+ }
30
+ }
31
+
32
+ // Setup autocomplete on pattern input
33
+ $("#node-input-pattern").autocomplete({
34
+ source: [],
35
+ minLength: 0,
36
+ delay: 0
37
+ }).on("focus", function() {
38
+ if (!$(this).val() || $(this).val() === "*") {
39
+ $(this).autocomplete("search", "");
40
+ }
41
+ });
42
+
43
+ // Refresh topics when cache selection changes
44
+ $("#node-input-cache").on("change", fetchTopics);
45
+
46
+ // Initial fetch
47
+ setTimeout(fetchTopics, 100);
48
+ }
49
+ });
50
+ </script>
51
+
52
+ <script type="text/html" data-template-name="event-topic">
53
+ <div class="form-row">
54
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
55
+ <input type="text" id="node-input-name" placeholder="Name">
56
+ </div>
57
+ <div class="form-row">
58
+ <label for="node-input-cache"><i class="fa fa-database"></i> Cache</label>
59
+ <input type="text" id="node-input-cache">
60
+ </div>
61
+ <div class="form-row">
62
+ <label for="node-input-pattern"><i class="fa fa-bookmark"></i> Topic Pattern</label>
63
+ <input type="text" id="node-input-pattern" placeholder="*">
64
+ <div class="form-tips">
65
+ Wildcards: <code>?</code> = exactly one character, <code>*</code> = one or more characters.<br>
66
+ Examples: <code>sensor?</code> matches sensor1, <code>sensors/*</code> matches sensors/temp
67
+ </div>
68
+ </div>
69
+ <div class="form-row">
70
+ <label for="node-input-outputFormat"><i class="fa fa-sign-out"></i> Output Format</label>
71
+ <select id="node-input-outputFormat" style="width:70%;">
72
+ <option value="value">Value only (msg.payload = value)</option>
73
+ <option value="full">Full entry (msg.payload = {value, ts, metadata})</option>
74
+ <option value="all">All matching (msg.payload = {topic: value, ...})</option>
75
+ </select>
76
+ </div>
77
+ <div class="form-row">
78
+ <label>&nbsp;</label>
79
+ <input type="checkbox" id="node-input-outputOnStart" style="display:inline-block; width:auto; vertical-align:top;">
80
+ <label for="node-input-outputOnStart" style="width:auto;">Output existing cached values on deploy</label>
81
+ </div>
82
+ </script>
83
+
84
+ <script type="text/html" data-help-name="event-topic">
85
+ <p>Subscribes to a topic pattern and outputs when matching topics update in the cache.</p>
86
+
87
+ <h3>Properties</h3>
88
+ <dl class="message-properties">
89
+ <dt>Cache</dt>
90
+ <dd>The event-cache config node to use</dd>
91
+ <dt>Topic Pattern</dt>
92
+ <dd>Pattern with wildcards. <code>?</code> = exactly one character, <code>*</code> = one or more characters.</dd>
93
+ <dt>Output Format</dt>
94
+ <dd>
95
+ <ul>
96
+ <li><b>Value only</b>: <code>msg.payload</code> contains just the value</li>
97
+ <li><b>Full entry</b>: <code>msg.payload</code> contains <code>{value, ts, metadata}</code></li>
98
+ <li><b>All matching</b>: <code>msg.payload</code> contains all cached values matching the pattern</li>
99
+ </ul>
100
+ </dd>
101
+ <dt>Output on deploy</dt>
102
+ <dd>If checked, outputs all currently cached values matching the pattern when the flow starts</dd>
103
+ </dl>
104
+
105
+ <h3>Inputs</h3>
106
+ <dl class="message-properties">
107
+ <dt class="optional">pattern <span class="property-type">string</span></dt>
108
+ <dd>Dynamically change the subscription pattern</dd>
109
+ <dt class="optional">payload/topic = "refresh"</dt>
110
+ <dd>Output all currently cached values matching the pattern</dd>
111
+ </dl>
112
+
113
+ <h3>Outputs</h3>
114
+ <dl class="message-properties">
115
+ <dt>topic <span class="property-type">string</span></dt>
116
+ <dd>The topic that was updated</dd>
117
+ <dt>payload <span class="property-type">any</span></dt>
118
+ <dd>The value (format depends on Output Format setting)</dd>
119
+ <dt>timestamp <span class="property-type">number</span></dt>
120
+ <dd>Unix timestamp when the value was cached</dd>
121
+ </dl>
122
+
123
+ <h3>Wildcard Examples</h3>
124
+ <ul>
125
+ <li><code>sensor?</code> - matches sensor1, sensorA (exactly one char)</li>
126
+ <li><code>sensors/*</code> - matches sensors/temp, sensors/room1 (one or more chars after /)</li>
127
+ <li><code>*/temp</code> - matches room1/temp, sensors/temp</li>
128
+ <li><code>*</code> - matches any topic with one or more characters</li>
129
+ </ul>
130
+ </script>
@@ -0,0 +1,140 @@
1
+ /**
2
+ * event-topic - Subscription node for topic patterns
3
+ *
4
+ * Features:
5
+ * - Subscribes to cache using MQTT-style topic patterns
6
+ * - Outputs when matching topics update
7
+ * - Multiple output formats: value only, full entry, or all matching
8
+ * - Optional output of existing values on start
9
+ * - Dynamic pattern change via input message
10
+ */
11
+ module.exports = function(RED) {
12
+ function EventTopicNode(config) {
13
+ RED.nodes.createNode(this, config);
14
+ const node = this;
15
+
16
+ node.cacheConfig = RED.nodes.getNode(config.cache);
17
+ node.pattern = config.pattern || '#';
18
+ node.outputFormat = config.outputFormat || 'value';
19
+ node.outputOnStart = config.outputOnStart || false;
20
+
21
+ let subscriptionId = null;
22
+
23
+ if (!node.cacheConfig) {
24
+ node.status({ fill: "red", shape: "ring", text: "no cache configured" });
25
+ return;
26
+ }
27
+
28
+ /**
29
+ * Build output message based on configured format
30
+ */
31
+ function buildOutputMessage(topic, entry) {
32
+ switch (node.outputFormat) {
33
+ case 'value':
34
+ return {
35
+ topic: topic,
36
+ payload: entry.value,
37
+ timestamp: entry.ts
38
+ };
39
+ case 'full':
40
+ return {
41
+ topic: topic,
42
+ payload: {
43
+ value: entry.value,
44
+ ts: entry.ts,
45
+ metadata: entry.metadata
46
+ }
47
+ };
48
+ case 'all':
49
+ const all = node.cacheConfig.getMatching(node.pattern);
50
+ const values = {};
51
+ for (const [t, e] of all) {
52
+ values[t] = e.value;
53
+ }
54
+ return {
55
+ topic: topic,
56
+ payload: values,
57
+ trigger: {
58
+ topic: topic,
59
+ value: entry.value
60
+ },
61
+ timestamp: entry.ts
62
+ };
63
+ default:
64
+ return {
65
+ topic: topic,
66
+ payload: entry.value
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Subscribe to the current pattern
73
+ */
74
+ function subscribe() {
75
+ subscriptionId = node.cacheConfig.subscribe(node.pattern, (topic, entry) => {
76
+ const msg = buildOutputMessage(topic, entry);
77
+ node.send(msg);
78
+
79
+ // Truncate topic for status display
80
+ const displayTopic = topic.length > 20 ? topic.substring(0, 17) + '...' : topic;
81
+ node.status({ fill: "green", shape: "dot", text: displayTopic });
82
+ });
83
+ }
84
+
85
+ // Initial subscription
86
+ subscribe();
87
+ node.status({ fill: "green", shape: "dot", text: node.pattern });
88
+
89
+ // Output existing values on start if configured
90
+ if (node.outputOnStart) {
91
+ setImmediate(() => {
92
+ const matching = node.cacheConfig.getMatching(node.pattern);
93
+ for (const [topic, entry] of matching) {
94
+ const msg = buildOutputMessage(topic, entry);
95
+ node.send(msg);
96
+ }
97
+ });
98
+ }
99
+
100
+ // Handle input messages for dynamic pattern change
101
+ node.on('input', function(msg, send, done) {
102
+ // For Node-RED 0.x compatibility
103
+ send = send || function() { node.send.apply(node, arguments); };
104
+ done = done || function(err) { if (err) node.error(err, msg); };
105
+
106
+ if (msg.pattern && typeof msg.pattern === 'string') {
107
+ // Unsubscribe from old pattern
108
+ if (subscriptionId && node.cacheConfig) {
109
+ node.cacheConfig.unsubscribe(subscriptionId);
110
+ }
111
+
112
+ // Update pattern and resubscribe
113
+ node.pattern = msg.pattern;
114
+ subscribe();
115
+
116
+ node.status({ fill: "blue", shape: "dot", text: node.pattern });
117
+ }
118
+
119
+ // Allow manual trigger to output all current values
120
+ if (msg.topic === 'refresh' || msg.payload === 'refresh') {
121
+ const matching = node.cacheConfig.getMatching(node.pattern);
122
+ for (const [topic, entry] of matching) {
123
+ const outMsg = buildOutputMessage(topic, entry);
124
+ send(outMsg);
125
+ }
126
+ }
127
+
128
+ done();
129
+ });
130
+
131
+ node.on('close', function(done) {
132
+ if (subscriptionId && node.cacheConfig) {
133
+ node.cacheConfig.unsubscribe(subscriptionId);
134
+ }
135
+ done();
136
+ });
137
+ }
138
+
139
+ RED.nodes.registerType("event-topic", EventTopicNode);
140
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "node-red-contrib-event-calc",
3
+ "version": "0.1.1",
4
+ "description": "Node-RED nodes for event caching and calculations with topic wildcard patterns",
5
+ "author": {
6
+ "name": "Holger Amort"
7
+ },
8
+ "keywords": [
9
+ "node-red",
10
+ "events",
11
+ "cache",
12
+ "wildcards",
13
+ "reactive",
14
+ "calculation",
15
+ "streaming"
16
+ ],
17
+ "license": "MIT",
18
+ "homepage": "https://github.com/ErnstHolger/node-red#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/ErnstHolger/node-red/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/ErnstHolger/node-red.git"
25
+ },
26
+ "node-red": {
27
+ "version": ">=2.0.0",
28
+ "nodes": {
29
+ "event-cache": "nodes/event-cache.js",
30
+ "event-in": "nodes/event-in.js",
31
+ "event-topic": "nodes/event-topic.js",
32
+ "event-calc": "nodes/event-calc.js"
33
+ }
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "scripts": {
39
+ "publish:dry": "npm publish --dry-run"
40
+ },
41
+ "dependencies": {}
42
+ }