node-red-contrib-event-calc 0.1.2 → 0.1.4

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,156 @@
1
+ <style>
2
+ .event-calc-white-text { fill: #ffffff !important; }
3
+ .red-ui-palette-node[data-palette-type="event-simulator"] .red-ui-palette-label { color: #ffffff !important; }
4
+ </style>
5
+
6
+ <script type="text/javascript">
7
+ RED.nodes.registerType('event-simulator', {
8
+ category: 'event calc',
9
+ color: '#758467',
10
+ defaults: {
11
+ name: { value: "" },
12
+ waveform: { value: "sinusoid" },
13
+ amplitude: { value: 1, validate: RED.validators.number() },
14
+ frequency: { value: 1, validate: RED.validators.number() },
15
+ offset: { value: 0, validate: RED.validators.number() },
16
+ interval: { value: 100, validate: RED.validators.number() },
17
+ startOnDeploy: { value: true },
18
+ noiseLevel: { value: 0, validate: RED.validators.number() },
19
+ topicCount: { value: 1, validate: RED.validators.number() },
20
+ phaseSpread: { value: true }
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ icon: "font-awesome/fa-signal",
25
+ label: function() {
26
+ return this.name || this.waveform || "simulator";
27
+ },
28
+ paletteLabel: "simulator",
29
+ labelStyle: function() {
30
+ return (this.name ? "node_label_italic" : "") + " event-calc-white-text";
31
+ }
32
+ });
33
+ </script>
34
+
35
+ <script type="text/html" data-template-name="event-simulator">
36
+ <div class="form-row">
37
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
38
+ <input type="text" id="node-input-name" placeholder="Name (used as topic base)">
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-topicCount"><i class="fa fa-clone"></i> Topic Count</label>
42
+ <input type="number" id="node-input-topicCount" min="1" max="100" value="1">
43
+ <div class="form-tips">Number of topics to generate (Name1, Name2, ... if count > 1)</div>
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-waveform"><i class="fa fa-signal"></i> Waveform</label>
47
+ <select id="node-input-waveform">
48
+ <option value="sinusoid">Sinusoid</option>
49
+ <option value="cosine">Cosine</option>
50
+ <option value="sawtooth">Sawtooth</option>
51
+ <option value="triangle">Triangle</option>
52
+ <option value="rectangle">Rectangle (Square)</option>
53
+ <option value="pulse">Pulse</option>
54
+ <option value="random">Random</option>
55
+ <option value="randomWalk">Random Walk</option>
56
+ </select>
57
+ </div>
58
+ <div class="form-row">
59
+ <label for="node-input-amplitude"><i class="fa fa-arrows-v"></i> Amplitude</label>
60
+ <input type="number" id="node-input-amplitude" step="0.1" value="1">
61
+ </div>
62
+ <div class="form-row">
63
+ <label for="node-input-frequency"><i class="fa fa-tachometer"></i> Frequency (Hz)</label>
64
+ <input type="number" id="node-input-frequency" step="0.1" min="0.001" value="1">
65
+ </div>
66
+ <div class="form-row">
67
+ <label for="node-input-offset"><i class="fa fa-arrows-h"></i> Offset</label>
68
+ <input type="number" id="node-input-offset" step="0.1" value="0">
69
+ </div>
70
+ <div class="form-row">
71
+ <label for="node-input-interval"><i class="fa fa-clock-o"></i> Interval (ms)</label>
72
+ <input type="number" id="node-input-interval" min="10" value="100">
73
+ </div>
74
+ <div class="form-row">
75
+ <label for="node-input-noiseLevel"><i class="fa fa-random"></i> Noise Level</label>
76
+ <input type="number" id="node-input-noiseLevel" step="0.01" min="0" max="1" value="0">
77
+ <div class="form-tips">Noise as fraction of amplitude (0-1)</div>
78
+ </div>
79
+ <div class="form-row">
80
+ <label for="node-input-phaseSpread">
81
+ <input type="checkbox" id="node-input-phaseSpread" style="width:auto; margin-left:0;" checked>
82
+ Phase spread (offset each topic's phase when count > 1)
83
+ </label>
84
+ </div>
85
+ <div class="form-row">
86
+ <label for="node-input-startOnDeploy">
87
+ <input type="checkbox" id="node-input-startOnDeploy" style="width:auto; margin-left:0;" checked>
88
+ Start on deploy
89
+ </label>
90
+ </div>
91
+ </script>
92
+
93
+ <script type="text/html" data-help-name="event-simulator">
94
+ <p>Generates simulated time-series data with various waveforms.</p>
95
+
96
+ <h3>Outputs</h3>
97
+ <dl class="message-properties">
98
+ <dt>payload <span class="property-type">number</span></dt>
99
+ <dd>Generated value</dd>
100
+ <dt>topic <span class="property-type">string</span></dt>
101
+ <dd>Topic name. If count=1: uses Name. If count>1: uses Name1, Name2, etc.</dd>
102
+ <dt>timestamp <span class="property-type">number</span></dt>
103
+ <dd>Unix timestamp in ms</dd>
104
+ </dl>
105
+
106
+ <h3>Multiple Topics</h3>
107
+ <p>Set <b>Topic Count</b> > 1 to generate multiple messages per interval:</p>
108
+ <ul>
109
+ <li>Name="Sensor", Count=3 → topics: Sensor1, Sensor2, Sensor3</li>
110
+ <li><b>Phase Spread</b>: Each topic gets an evenly-distributed phase offset</li>
111
+ <li>Useful for simulating multiple related sensors</li>
112
+ </ul>
113
+
114
+ <h3>Waveforms</h3>
115
+ <ul>
116
+ <li><b>Sinusoid</b> - Smooth sine wave</li>
117
+ <li><b>Cosine</b> - Cosine wave (90° phase shift)</li>
118
+ <li><b>Sawtooth</b> - Linear ramp up, instant drop</li>
119
+ <li><b>Triangle</b> - Linear ramp up and down</li>
120
+ <li><b>Rectangle</b> - Square wave (±amplitude)</li>
121
+ <li><b>Pulse</b> - Short pulse (10% duty cycle)</li>
122
+ <li><b>Random</b> - Random values each sample</li>
123
+ <li><b>Random Walk</b> - Brownian motion with mean reversion</li>
124
+ </ul>
125
+
126
+ <h3>Parameters</h3>
127
+ <ul>
128
+ <li><b>Topic Count</b> - Number of topics to generate (1-100)</li>
129
+ <li><b>Amplitude</b> - Peak value (signal swings ±amplitude around offset)</li>
130
+ <li><b>Frequency</b> - Cycles per second (Hz)</li>
131
+ <li><b>Offset</b> - DC offset (center value)</li>
132
+ <li><b>Interval</b> - Sample rate in milliseconds</li>
133
+ <li><b>Noise Level</b> - Random noise as fraction of amplitude</li>
134
+ <li><b>Phase Spread</b> - Distribute phase offsets across topics</li>
135
+ </ul>
136
+
137
+ <h3>Control</h3>
138
+ <p>Send messages to control the simulator:</p>
139
+ <ul>
140
+ <li><code>msg.payload = "start"</code> - Start generating</li>
141
+ <li><code>msg.payload = "stop"</code> - Stop generating</li>
142
+ <li><code>msg.payload = "reset"</code> - Reset time and restart</li>
143
+ </ul>
144
+
145
+ <h3>Dynamic Parameters</h3>
146
+ <p>Update parameters via message properties:</p>
147
+ <ul>
148
+ <li><code>msg.amplitude</code></li>
149
+ <li><code>msg.frequency</code></li>
150
+ <li><code>msg.offset</code></li>
151
+ <li><code>msg.interval</code></li>
152
+ <li><code>msg.waveform</code></li>
153
+ <li><code>msg.topicCount</code></li>
154
+ <li><code>msg.phaseSpread</code></li>
155
+ </ul>
156
+ </script>
@@ -0,0 +1,185 @@
1
+ module.exports = function(RED) {
2
+ function EventSimulatorNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ node.waveform = config.waveform || "sinusoid";
7
+ node.amplitude = parseFloat(config.amplitude) || 1;
8
+ node.frequency = parseFloat(config.frequency) || 1; // Hz
9
+ node.offset = parseFloat(config.offset) || 0;
10
+ node.interval = parseInt(config.interval) || 100; // ms
11
+ node.startOnDeploy = config.startOnDeploy !== false;
12
+ node.noiseLevel = parseFloat(config.noiseLevel) || 0;
13
+ node.topicCount = parseInt(config.topicCount) || 1;
14
+ node.phaseSpread = config.phaseSpread !== false; // Add phase offset between topics
15
+
16
+ const baseName = config.name || "simulator";
17
+
18
+ let timer = null;
19
+ let startTime = Date.now();
20
+ let sampleCount = 0;
21
+
22
+ // Waveform generators
23
+ const waveforms = {
24
+ sinusoid: (t, freq, amp, offset) => {
25
+ return amp * Math.sin(2 * Math.PI * freq * t) + offset;
26
+ },
27
+ cosine: (t, freq, amp, offset) => {
28
+ return amp * Math.cos(2 * Math.PI * freq * t) + offset;
29
+ },
30
+ sawtooth: (t, freq, amp, offset) => {
31
+ const period = 1 / freq;
32
+ const phase = (t % period) / period;
33
+ return amp * (2 * phase - 1) + offset;
34
+ },
35
+ triangle: (t, freq, amp, offset) => {
36
+ const period = 1 / freq;
37
+ const phase = (t % period) / period;
38
+ return amp * (4 * Math.abs(phase - 0.5) - 1) + offset;
39
+ },
40
+ rectangle: (t, freq, amp, offset) => {
41
+ const period = 1 / freq;
42
+ const phase = (t % period) / period;
43
+ return amp * (phase < 0.5 ? 1 : -1) + offset;
44
+ },
45
+ pulse: (t, freq, amp, offset) => {
46
+ const period = 1 / freq;
47
+ const phase = (t % period) / period;
48
+ return amp * (phase < 0.1 ? 1 : 0) + offset;
49
+ },
50
+ random: (t, freq, amp, offset) => {
51
+ return amp * (Math.random() * 2 - 1) + offset;
52
+ },
53
+ randomWalk: (t, freq, amp, offset, topicIndex) => {
54
+ // Random walk with mean reversion (per-topic state)
55
+ if (!node._lastValues) node._lastValues = {};
56
+ if (node._lastValues[topicIndex] === undefined) node._lastValues[topicIndex] = offset;
57
+ const step = (Math.random() * 2 - 1) * amp * 0.1;
58
+ const reversion = (offset - node._lastValues[topicIndex]) * 0.05;
59
+ node._lastValues[topicIndex] = Math.max(offset - amp, Math.min(offset + amp, node._lastValues[topicIndex] + step + reversion));
60
+ return node._lastValues[topicIndex];
61
+ }
62
+ };
63
+
64
+ function generateSample() {
65
+ const now = Date.now();
66
+ const t = (now - startTime) / 1000; // time in seconds
67
+ sampleCount++;
68
+
69
+ const generator = waveforms[node.waveform] || waveforms.sinusoid;
70
+
71
+ // Generate message for each topic
72
+ for (let i = 0; i < node.topicCount; i++) {
73
+ // Calculate phase offset for this topic (spread evenly across one period)
74
+ const phaseOffset = node.phaseSpread ? (i / node.topicCount) / node.frequency : 0;
75
+ const tWithPhase = t + phaseOffset;
76
+
77
+ let value = generator(tWithPhase, node.frequency, node.amplitude, node.offset, i);
78
+
79
+ // Add noise if configured
80
+ if (node.noiseLevel > 0) {
81
+ value += (Math.random() * 2 - 1) * node.noiseLevel * node.amplitude;
82
+ }
83
+
84
+ // Topic name: baseName for count=1, baseName1/baseName2/... for count>1
85
+ const topic = node.topicCount === 1 ? baseName : `${baseName}${i + 1}`;
86
+
87
+ const msg = {
88
+ topic: topic,
89
+ payload: value,
90
+ timestamp: now,
91
+ _simulator: {
92
+ waveform: node.waveform,
93
+ frequency: node.frequency,
94
+ amplitude: node.amplitude,
95
+ sample: sampleCount,
96
+ time: tWithPhase,
97
+ topicIndex: i + 1
98
+ }
99
+ };
100
+
101
+ node.send(msg);
102
+ }
103
+
104
+ // Status shows count and first value
105
+ const firstValue = generator(t, node.frequency, node.amplitude, node.offset, 0);
106
+ const statusText = node.topicCount > 1
107
+ ? `${sampleCount}: ${node.topicCount} topics`
108
+ : `${sampleCount}: ${firstValue.toFixed(3)}`;
109
+ node.status({ fill: "green", shape: "dot", text: statusText });
110
+ }
111
+
112
+ function start() {
113
+ if (timer) return;
114
+ startTime = Date.now();
115
+ sampleCount = 0;
116
+ node._lastValues = {}; // Reset random walk state
117
+ timer = setInterval(generateSample, node.interval);
118
+ const statusText = node.topicCount > 1 ? `running (${node.topicCount} topics)` : "running";
119
+ node.status({ fill: "green", shape: "dot", text: statusText });
120
+ }
121
+
122
+ function stop() {
123
+ if (timer) {
124
+ clearInterval(timer);
125
+ timer = null;
126
+ }
127
+ node.status({ fill: "grey", shape: "ring", text: "stopped" });
128
+ }
129
+
130
+ function reset() {
131
+ stop();
132
+ start();
133
+ }
134
+
135
+ // Start on deploy if configured
136
+ if (node.startOnDeploy) {
137
+ start();
138
+ } else {
139
+ node.status({ fill: "grey", shape: "ring", text: "stopped" });
140
+ }
141
+
142
+ node.on('input', function(msg, send, done) {
143
+ const command = (msg.payload || "").toString().toLowerCase();
144
+
145
+ switch (command) {
146
+ case 'start':
147
+ start();
148
+ break;
149
+ case 'stop':
150
+ stop();
151
+ break;
152
+ case 'reset':
153
+ reset();
154
+ break;
155
+ default:
156
+ // Update parameters if provided
157
+ if (typeof msg.amplitude === 'number') node.amplitude = msg.amplitude;
158
+ if (typeof msg.frequency === 'number') node.frequency = msg.frequency;
159
+ if (typeof msg.offset === 'number') node.offset = msg.offset;
160
+ if (typeof msg.interval === 'number') {
161
+ node.interval = msg.interval;
162
+ if (timer) reset();
163
+ }
164
+ if (typeof msg.topicCount === 'number' && msg.topicCount >= 1) {
165
+ node.topicCount = Math.floor(msg.topicCount);
166
+ }
167
+ if (typeof msg.phaseSpread === 'boolean') {
168
+ node.phaseSpread = msg.phaseSpread;
169
+ }
170
+ if (msg.waveform && waveforms[msg.waveform]) {
171
+ node.waveform = msg.waveform;
172
+ }
173
+ }
174
+
175
+ if (done) done();
176
+ });
177
+
178
+ node.on('close', function(done) {
179
+ stop();
180
+ done();
181
+ });
182
+ }
183
+
184
+ RED.nodes.registerType("event-simulator", EventSimulatorNode);
185
+ };
@@ -10,7 +10,7 @@
10
10
  defaults: {
11
11
  name: { value: "" },
12
12
  cache: { value: "", type: "event-cache", required: true },
13
- pattern: { value: "*" },
13
+ topic: { value: "" },
14
14
  outputFormat: { value: "value" },
15
15
  outputOnStart: { value: false }
16
16
  },
@@ -18,7 +18,7 @@
18
18
  outputs: 1,
19
19
  icon: "font-awesome/fa-filter",
20
20
  label: function() {
21
- return this.name || this.pattern || "event topic";
21
+ return this.name || this.topic || "event topic";
22
22
  },
23
23
  paletteLabel: "event topic",
24
24
  labelStyle: function() { return (this.name ? "node_label_italic" : "") + " event-calc-white-text"; },
@@ -30,18 +30,18 @@
30
30
  const cacheId = $("#node-input-cache").val();
31
31
  if (cacheId) {
32
32
  $.getJSON("event-cache/" + cacheId + "/topics", function(topics) {
33
- $("#node-input-pattern").autocomplete("option", "source", topics || []);
33
+ $("#node-input-topic").autocomplete("option", "source", topics || []);
34
34
  });
35
35
  }
36
36
  }
37
37
 
38
- // Setup autocomplete on pattern input
39
- $("#node-input-pattern").autocomplete({
38
+ // Setup autocomplete on topic input
39
+ $("#node-input-topic").autocomplete({
40
40
  source: [],
41
41
  minLength: 0,
42
42
  delay: 0
43
43
  }).on("focus", function() {
44
- if (!$(this).val() || $(this).val() === "*") {
44
+ if (!$(this).val()) {
45
45
  $(this).autocomplete("search", "");
46
46
  }
47
47
  });
@@ -65,19 +65,14 @@
65
65
  <input type="text" id="node-input-cache">
66
66
  </div>
67
67
  <div class="form-row">
68
- <label for="node-input-pattern"><i class="fa fa-bookmark"></i> Topic Pattern</label>
69
- <input type="text" id="node-input-pattern" placeholder="*">
70
- <div class="form-tips">
71
- Wildcards: <code>?</code> = exactly one character, <code>*</code> = one or more characters.<br>
72
- Examples: <code>sensor?</code> matches sensor1, <code>sensors/*</code> matches sensors/temp
73
- </div>
68
+ <label for="node-input-topic"><i class="fa fa-bookmark"></i> Topic</label>
69
+ <input type="text" id="node-input-topic" placeholder="sensors/room1/temp">
74
70
  </div>
75
71
  <div class="form-row">
76
72
  <label for="node-input-outputFormat"><i class="fa fa-sign-out"></i> Output Format</label>
77
73
  <select id="node-input-outputFormat" style="width:70%;">
78
74
  <option value="value">Value only (msg.payload = value)</option>
79
75
  <option value="full">Full entry (msg.payload = {value, ts, metadata})</option>
80
- <option value="all">All matching (msg.payload = {topic: value, ...})</option>
81
76
  </select>
82
77
  </div>
83
78
  <div class="form-row">
@@ -88,32 +83,31 @@
88
83
  </script>
89
84
 
90
85
  <script type="text/html" data-help-name="event-topic">
91
- <p>Subscribes to a topic pattern and outputs when matching topics update in the cache.</p>
86
+ <p>Subscribes to a topic and outputs when that topic updates in the cache.</p>
92
87
 
93
88
  <h3>Properties</h3>
94
89
  <dl class="message-properties">
95
90
  <dt>Cache</dt>
96
91
  <dd>The event-cache config node to use</dd>
97
- <dt>Topic Pattern</dt>
98
- <dd>Pattern with wildcards. <code>?</code> = exactly one character, <code>*</code> = one or more characters.</dd>
92
+ <dt>Topic</dt>
93
+ <dd>The exact topic to subscribe to</dd>
99
94
  <dt>Output Format</dt>
100
95
  <dd>
101
96
  <ul>
102
97
  <li><b>Value only</b>: <code>msg.payload</code> contains just the value</li>
103
98
  <li><b>Full entry</b>: <code>msg.payload</code> contains <code>{value, ts, metadata}</code></li>
104
- <li><b>All matching</b>: <code>msg.payload</code> contains all cached values matching the pattern</li>
105
99
  </ul>
106
100
  </dd>
107
101
  <dt>Output on deploy</dt>
108
- <dd>If checked, outputs all currently cached values matching the pattern when the flow starts</dd>
102
+ <dd>If checked, outputs the current cached value when the flow starts</dd>
109
103
  </dl>
110
104
 
111
105
  <h3>Inputs</h3>
112
106
  <dl class="message-properties">
113
- <dt class="optional">pattern <span class="property-type">string</span></dt>
114
- <dd>Dynamically change the subscription pattern</dd>
115
- <dt class="optional">payload/topic = "refresh"</dt>
116
- <dd>Output all currently cached values matching the pattern</dd>
107
+ <dt class="optional">topic + payload="subscribe"</dt>
108
+ <dd>Dynamically change the subscription to a new topic</dd>
109
+ <dt class="optional">payload = "refresh"</dt>
110
+ <dd>Output the current cached value</dd>
117
111
  </dl>
118
112
 
119
113
  <h3>Outputs</h3>
@@ -125,12 +119,4 @@
125
119
  <dt>timestamp <span class="property-type">number</span></dt>
126
120
  <dd>Unix timestamp when the value was cached</dd>
127
121
  </dl>
128
-
129
- <h3>Wildcard Examples</h3>
130
- <ul>
131
- <li><code>sensor?</code> - matches sensor1, sensorA (exactly one char)</li>
132
- <li><code>sensors/*</code> - matches sensors/temp, sensors/room1 (one or more chars after /)</li>
133
- <li><code>*/temp</code> - matches room1/temp, sensors/temp</li>
134
- <li><code>*</code> - matches any topic with one or more characters</li>
135
- </ul>
136
122
  </script>
@@ -1,12 +1,12 @@
1
1
  /**
2
- * event-topic - Subscription node for topic patterns
2
+ * event-topic - Subscription node for exact topics
3
3
  *
4
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
5
+ * - Subscribes to cache for a specific topic
6
+ * - Outputs when the topic updates
7
+ * - Multiple output formats: value only or full entry
8
+ * - Optional output of existing value on start
9
+ * - Dynamic topic change via input message
10
10
  */
11
11
  module.exports = function(RED) {
12
12
  function EventTopicNode(config) {
@@ -14,7 +14,8 @@ module.exports = function(RED) {
14
14
  const node = this;
15
15
 
16
16
  node.cacheConfig = RED.nodes.getNode(config.cache);
17
- node.pattern = config.pattern || '#';
17
+ // Support both old 'pattern' and new 'topic' config
18
+ node.topic = config.topic || config.pattern || '';
18
19
  node.outputFormat = config.outputFormat || 'value';
19
20
  node.outputOnStart = config.outputOnStart || false;
20
21
 
@@ -25,6 +26,11 @@ module.exports = function(RED) {
25
26
  return;
26
27
  }
27
28
 
29
+ if (!node.topic) {
30
+ node.status({ fill: "yellow", shape: "ring", text: "no topic" });
31
+ return;
32
+ }
33
+
28
34
  /**
29
35
  * Build output message based on configured format
30
36
  */
@@ -45,21 +51,6 @@ module.exports = function(RED) {
45
51
  metadata: entry.metadata
46
52
  }
47
53
  };
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
54
  default:
64
55
  return {
65
56
  topic: topic,
@@ -69,10 +60,10 @@ module.exports = function(RED) {
69
60
  }
70
61
 
71
62
  /**
72
- * Subscribe to the current pattern
63
+ * Subscribe to the current topic
73
64
  */
74
65
  function subscribe() {
75
- subscriptionId = node.cacheConfig.subscribe(node.pattern, (topic, entry) => {
66
+ subscriptionId = node.cacheConfig.subscribe(node.topic, (topic, entry) => {
76
67
  const msg = buildOutputMessage(topic, entry);
77
68
  node.send(msg);
78
69
 
@@ -84,43 +75,45 @@ module.exports = function(RED) {
84
75
 
85
76
  // Initial subscription
86
77
  subscribe();
87
- node.status({ fill: "green", shape: "dot", text: node.pattern });
78
+ const displayTopic = node.topic.length > 20 ? node.topic.substring(0, 17) + '...' : node.topic;
79
+ node.status({ fill: "green", shape: "dot", text: displayTopic });
88
80
 
89
- // Output existing values on start if configured
81
+ // Output existing value on start if configured
90
82
  if (node.outputOnStart) {
91
83
  setImmediate(() => {
92
- const matching = node.cacheConfig.getMatching(node.pattern);
93
- for (const [topic, entry] of matching) {
94
- const msg = buildOutputMessage(topic, entry);
84
+ const entry = node.cacheConfig.getValue(node.topic);
85
+ if (entry) {
86
+ const msg = buildOutputMessage(node.topic, entry);
95
87
  node.send(msg);
96
88
  }
97
89
  });
98
90
  }
99
91
 
100
- // Handle input messages for dynamic pattern change
92
+ // Handle input messages for dynamic topic change
101
93
  node.on('input', function(msg, send, done) {
102
94
  // For Node-RED 0.x compatibility
103
95
  send = send || function() { node.send.apply(node, arguments); };
104
96
  done = done || function(err) { if (err) node.error(err, msg); };
105
97
 
106
- if (msg.pattern && typeof msg.pattern === 'string') {
107
- // Unsubscribe from old pattern
98
+ if (msg.topic && typeof msg.topic === 'string' && msg.payload === 'subscribe') {
99
+ // Unsubscribe from old topic
108
100
  if (subscriptionId && node.cacheConfig) {
109
101
  node.cacheConfig.unsubscribe(subscriptionId);
110
102
  }
111
103
 
112
- // Update pattern and resubscribe
113
- node.pattern = msg.pattern;
104
+ // Update topic and resubscribe
105
+ node.topic = msg.topic;
114
106
  subscribe();
115
107
 
116
- node.status({ fill: "blue", shape: "dot", text: node.pattern });
108
+ const dt = node.topic.length > 20 ? node.topic.substring(0, 17) + '...' : node.topic;
109
+ node.status({ fill: "blue", shape: "dot", text: dt });
117
110
  }
118
111
 
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);
112
+ // Allow manual trigger to output current value
113
+ if (msg.payload === 'refresh') {
114
+ const entry = node.cacheConfig.getValue(node.topic);
115
+ if (entry) {
116
+ const outMsg = buildOutputMessage(node.topic, entry);
124
117
  send(outMsg);
125
118
  }
126
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-event-calc",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Node-RED nodes for event caching and calculations with topic wildcard patterns",
5
5
  "author": {
6
6
  "name": "Holger Amort"
@@ -29,7 +29,9 @@
29
29
  "event-cache": "nodes/event-cache.js",
30
30
  "event-in": "nodes/event-in.js",
31
31
  "event-topic": "nodes/event-topic.js",
32
- "event-calc": "nodes/event-calc.js"
32
+ "event-calc": "nodes/event-calc.js",
33
+ "event-simulator": "nodes/event-simulator.js",
34
+ "event-chart": "nodes/event-chart.js"
33
35
  }
34
36
  },
35
37
  "engines": {