node-red-contrib-event-calc 2.0.3 → 3.3.6

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,53 @@
1
+ <style>
2
+ .event-calc-white-text { fill: #ffffff !important; }
3
+ .red-ui-palette-node[data-palette-type="event-flatten"] .red-ui-palette-label { color: #ffffff !important; }
4
+ </style>
5
+
6
+ <script type="text/javascript">
7
+ RED.nodes.registerType('event-flatten', {
8
+ category: 'event calc',
9
+ color: '#758467',
10
+ defaults: {
11
+ name: { value: "" }
12
+ },
13
+ inputs: 1,
14
+ outputs: 1,
15
+ icon: "font-awesome/fa-expand",
16
+ label: function() {
17
+ return this.name || "event flatten";
18
+ },
19
+ paletteLabel: "event flatten",
20
+ labelStyle: function() { return (this.name ? "node_label_italic" : "") + " event-calc-white-text"; }
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="event-flatten">
25
+ <div class="form-row">
26
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
+ <input type="text" id="node-input-name" placeholder="Name">
28
+ </div>
29
+ </script>
30
+
31
+ <script type="text/html" data-help-name="event-flatten">
32
+ <p>Flattens <code>msg.payload</code> into the top-level message object.</p>
33
+
34
+ <h3>Behavior</h3>
35
+ <p>Takes all properties from <code>msg.payload</code> (must be a plain object) and assigns them
36
+ directly onto <code>msg</code>, then removes <code>msg.payload</code>.</p>
37
+
38
+ <h3>Example</h3>
39
+ <pre>
40
+ Input:
41
+ msg.payload = { topic: "sensor/temp", value: 25.5, timestamp: "2026-02-06T..." }
42
+
43
+ Output:
44
+ msg.topic = "sensor/temp"
45
+ msg.value = 25.5
46
+ msg.timestamp = "2026-02-06T..."</pre>
47
+
48
+ <h3>Status</h3>
49
+ <ul>
50
+ <li><b>Green</b> - Successfully flattened, shows number of fields merged</li>
51
+ <li><b>Yellow</b> - Payload was not a plain object, message passed through unchanged</li>
52
+ </ul>
53
+ </script>
@@ -0,0 +1,25 @@
1
+ module.exports = function(RED) {
2
+ function EventFlattenNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ node.on('input', function(msg, send, done) {
7
+ send = send || function() { node.send.apply(node, arguments); };
8
+ done = done || function(err) { if (err) node.error(err, msg); };
9
+
10
+ const payload = msg.payload;
11
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
12
+ delete msg.payload;
13
+ Object.assign(msg, payload);
14
+ node.status({ fill: "green", shape: "dot", text: `${Object.keys(payload).length} fields` });
15
+ } else {
16
+ node.status({ fill: "yellow", shape: "ring", text: "payload not an object" });
17
+ }
18
+
19
+ send(msg);
20
+ done();
21
+ });
22
+ }
23
+
24
+ RED.nodes.registerType("event-flatten", EventFlattenNode);
25
+ };
@@ -0,0 +1,264 @@
1
+ <style>
2
+ .event-preview-white-text { fill: #ffffff !important; }
3
+ .red-ui-palette-node[data-palette-type="event-preview"] .red-ui-palette-label { color: #ffffff !important; }
4
+ .event-preview-display {
5
+ background: #2d2d2d;
6
+ border: 1px solid #555;
7
+ border-radius: 4px;
8
+ padding: 10px;
9
+ font-family: monospace;
10
+ font-size: 12px;
11
+ color: #eee;
12
+ margin-top: 10px;
13
+ max-height: 400px;
14
+ overflow-y: auto;
15
+ }
16
+ .event-preview-display .field {
17
+ margin-bottom: 8px;
18
+ border-bottom: 1px solid #444;
19
+ padding-bottom: 8px;
20
+ }
21
+ .event-preview-display .field:last-child {
22
+ border-bottom: none;
23
+ margin-bottom: 0;
24
+ padding-bottom: 0;
25
+ }
26
+ .event-preview-display .label {
27
+ color: #888;
28
+ font-size: 11px;
29
+ text-transform: uppercase;
30
+ margin-bottom: 2px;
31
+ }
32
+ .event-preview-display .value {
33
+ color: #4fc3f7;
34
+ word-break: break-all;
35
+ white-space: pre-wrap;
36
+ max-height: 150px;
37
+ overflow-y: auto;
38
+ }
39
+ .event-preview-display .value.field-topic {
40
+ color: #81c784;
41
+ }
42
+ .event-preview-display .value.field-timestamp {
43
+ color: #ffb74d;
44
+ }
45
+ .event-preview-display .value.field-payload {
46
+ color: #4fc3f7;
47
+ }
48
+ .event-preview-display .no-message {
49
+ color: #888;
50
+ font-style: italic;
51
+ }
52
+ .event-preview-controls {
53
+ margin-bottom: 10px;
54
+ display: flex;
55
+ gap: 10px;
56
+ align-items: center;
57
+ }
58
+ .event-preview-controls button {
59
+ padding: 5px 15px;
60
+ border-radius: 3px;
61
+ border: 1px solid #666;
62
+ cursor: pointer;
63
+ font-size: 12px;
64
+ }
65
+ .event-preview-controls button.play {
66
+ background: #4caf50;
67
+ color: white;
68
+ border-color: #388e3c;
69
+ }
70
+ .event-preview-controls button.pause {
71
+ background: #ff9800;
72
+ color: white;
73
+ border-color: #f57c00;
74
+ }
75
+ .event-preview-controls .status {
76
+ font-size: 11px;
77
+ color: #888;
78
+ }
79
+ .event-preview-controls .status.active {
80
+ color: #4caf50;
81
+ }
82
+ .event-preview-controls .status.paused {
83
+ color: #ff9800;
84
+ }
85
+ </style>
86
+
87
+ <script type="text/javascript">
88
+ RED.nodes.registerType('event-preview', {
89
+ category: 'event calc',
90
+ color: '#758467',
91
+ defaults: {
92
+ name: { value: "" }
93
+ },
94
+ inputs: 1,
95
+ outputs: 1,
96
+ icon: "font-awesome/fa-eye",
97
+ label: function() {
98
+ return this.name || "event preview";
99
+ },
100
+ paletteLabel: "event preview",
101
+ labelStyle: function() { return (this.name ? "node_label_italic" : "") + " event-preview-white-text"; },
102
+ oneditprepare: function() {
103
+ const node = this;
104
+ let isActive = true;
105
+
106
+ function formatValue(key, value) {
107
+ if (value === null) return 'null';
108
+ if (value === undefined) return 'undefined';
109
+ if (key === 'timestamp' && typeof value === 'number') {
110
+ const d = new Date(value);
111
+ return d.toLocaleString() + ' (' + value + ')';
112
+ }
113
+ if (typeof value === 'object') {
114
+ return JSON.stringify(value, null, 2);
115
+ }
116
+ return String(value);
117
+ }
118
+
119
+ function updateDisplay(data) {
120
+ const container = $('#event-preview-fields');
121
+ container.empty();
122
+
123
+ if (data && Object.keys(data).length > 0) {
124
+ // Define preferred order for common fields
125
+ const preferredOrder = ['topic', 'timestamp', 'payload'];
126
+ const keys = Object.keys(data);
127
+
128
+ // Sort keys: preferred first, then alphabetical
129
+ keys.sort((a, b) => {
130
+ const aIndex = preferredOrder.indexOf(a);
131
+ const bIndex = preferredOrder.indexOf(b);
132
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
133
+ if (aIndex !== -1) return -1;
134
+ if (bIndex !== -1) return 1;
135
+ return a.localeCompare(b);
136
+ });
137
+
138
+ keys.forEach(key => {
139
+ const fieldDiv = $('<div class="field"></div>');
140
+ const labelDiv = $('<div class="label"></div>').text(key);
141
+ const valueDiv = $('<div class="value"></div>')
142
+ .addClass('field-' + key)
143
+ .text(formatValue(key, data[key]));
144
+ fieldDiv.append(labelDiv).append(valueDiv);
145
+ container.append(fieldDiv);
146
+ });
147
+
148
+ $('#event-preview-no-message').hide();
149
+ container.show();
150
+ } else {
151
+ $('#event-preview-no-message').show();
152
+ container.hide();
153
+ }
154
+ }
155
+
156
+ function updateButtonState(active) {
157
+ isActive = active;
158
+ if (active) {
159
+ $('#event-preview-play-btn').hide();
160
+ $('#event-preview-pause-btn').show();
161
+ $('#event-preview-status').text('Live').removeClass('paused').addClass('active');
162
+ } else {
163
+ $('#event-preview-play-btn').show();
164
+ $('#event-preview-pause-btn').hide();
165
+ $('#event-preview-status').text('Paused').removeClass('active').addClass('paused');
166
+ }
167
+ }
168
+
169
+ // Get initial state
170
+ $.getJSON('event-preview/' + node.id + '/latest', updateDisplay);
171
+ $.getJSON('event-preview/' + node.id + '/active', function(data) {
172
+ if (data) updateButtonState(data.active);
173
+ });
174
+
175
+ // Subscribe to real-time updates
176
+ RED.comms.subscribe("event-preview:" + node.id, function(topic, data) {
177
+ if (isActive) {
178
+ updateDisplay(data);
179
+ }
180
+ });
181
+
182
+ // Play button
183
+ $('#event-preview-play-btn').on('click', function() {
184
+ $.post('event-preview/' + node.id + '/active/true', function(data) {
185
+ updateButtonState(data.active);
186
+ });
187
+ });
188
+
189
+ // Pause button
190
+ $('#event-preview-pause-btn').on('click', function() {
191
+ $.post('event-preview/' + node.id + '/active/false', function(data) {
192
+ updateButtonState(data.active);
193
+ });
194
+ });
195
+
196
+ // Cleanup on dialog close
197
+ $('#dialog-form').on('dialogclose', function() {
198
+ RED.comms.unsubscribe("event-preview:" + node.id);
199
+ });
200
+ },
201
+ oneditcancel: function() {
202
+ RED.comms.unsubscribe("event-preview:" + this.id);
203
+ },
204
+ oneditsave: function() {
205
+ RED.comms.unsubscribe("event-preview:" + this.id);
206
+ }
207
+ });
208
+ </script>
209
+
210
+ <script type="text/html" data-template-name="event-preview">
211
+ <div class="form-row">
212
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
213
+ <input type="text" id="node-input-name" placeholder="Name">
214
+ </div>
215
+ <div class="form-row">
216
+ <label><i class="fa fa-eye"></i> Live Preview</label>
217
+ <div class="event-preview-controls">
218
+ <button id="event-preview-play-btn" class="play" style="display:none;"><i class="fa fa-play"></i> Play</button>
219
+ <button id="event-preview-pause-btn" class="pause"><i class="fa fa-pause"></i> Pause</button>
220
+ <span id="event-preview-status" class="status active">Live</span>
221
+ </div>
222
+ <div class="event-preview-display">
223
+ <div id="event-preview-no-message" class="no-message">No message received yet</div>
224
+ <div id="event-preview-fields" style="display:none;"></div>
225
+ </div>
226
+ </div>
227
+ </script>
228
+
229
+ <script type="text/html" data-help-name="event-preview">
230
+ <p>Displays ALL fields of the latest message with real-time updates.</p>
231
+
232
+ <h3>Features</h3>
233
+ <ul>
234
+ <li><b>Shows all msg fields</b> - Not just topic/payload, but everything</li>
235
+ <li><b>Real-time updates</b> - Display updates instantly when messages arrive</li>
236
+ <li><b>Play/Pause control</b> - Pause updates to inspect a message</li>
237
+ <li>Passes message through for chaining</li>
238
+ </ul>
239
+
240
+ <h3>Field Display</h3>
241
+ <ul>
242
+ <li><b>topic</b> - Green</li>
243
+ <li><b>timestamp</b> - Orange (formatted as date + Unix timestamp)</li>
244
+ <li><b>payload</b> - Blue</li>
245
+ <li>Other fields shown in alphabetical order</li>
246
+ </ul>
247
+
248
+ <h3>Status Display</h3>
249
+ <p>The node status shows: <code>topic | payload | time</code></p>
250
+ <ul>
251
+ <li><b>Green</b> - Active (receiving updates)</li>
252
+ <li><b>Grey</b> - Paused</li>
253
+ </ul>
254
+
255
+ <h3>Edit Dialog</h3>
256
+ <p>Open the node's edit dialog to see a live-updating formatted view:</p>
257
+ <ul>
258
+ <li><b>Play</b> - Resume receiving updates</li>
259
+ <li><b>Pause</b> - Freeze the display to inspect current message</li>
260
+ </ul>
261
+
262
+ <h3>Outputs</h3>
263
+ <p>The original message is passed through unchanged.</p>
264
+ </script>
@@ -0,0 +1,125 @@
1
+ /**
2
+ * event-preview - Message preview node with live updates
3
+ *
4
+ * Displays topic, timestamp, and payload of the latest message
5
+ * Real-time updates via WebSocket with play/pause control
6
+ */
7
+ module.exports = function(RED) {
8
+ function EventPreviewNode(config) {
9
+ RED.nodes.createNode(this, config);
10
+ const node = this;
11
+
12
+ node.name = config.name || '';
13
+ node.latestMessage = null;
14
+ node.active = true; // Whether to send updates
15
+
16
+ node.on('input', function(msg, send, done) {
17
+ send = send || function() { node.send.apply(node, arguments); };
18
+ done = done || function(err) { if (err) node.error(err, msg); };
19
+
20
+ // Store the entire message (clone to avoid reference issues)
21
+ const msgCopy = {};
22
+ for (const key of Object.keys(msg)) {
23
+ if (key !== '_msgid') { // Skip internal Node-RED field
24
+ msgCopy[key] = msg[key];
25
+ }
26
+ }
27
+ // Ensure timestamp exists
28
+ if (!msgCopy.timestamp) {
29
+ msgCopy.timestamp = Date.now();
30
+ }
31
+ node.latestMessage = msgCopy;
32
+
33
+ // Format timestamp for display
34
+ const ts = new Date(node.latestMessage.timestamp);
35
+ const timeStr = ts.toLocaleTimeString();
36
+
37
+ // Format payload for status (truncate if needed)
38
+ let payloadStr;
39
+ if (typeof msg.payload === 'object') {
40
+ payloadStr = JSON.stringify(msg.payload);
41
+ } else {
42
+ payloadStr = String(msg.payload);
43
+ }
44
+ if (payloadStr.length > 30) {
45
+ payloadStr = payloadStr.substring(0, 27) + '...';
46
+ }
47
+
48
+ // Update node status with preview
49
+ node.status({
50
+ fill: node.active ? "green" : "grey",
51
+ shape: "dot",
52
+ text: `${msg.topic || '-'} | ${payloadStr} | ${timeStr}`
53
+ });
54
+
55
+ // Send real-time update to editor if active
56
+ if (node.active) {
57
+ RED.comms.publish("event-preview:" + node.id, node.latestMessage);
58
+ }
59
+
60
+ // Send message through to allow chaining
61
+ send(msg);
62
+ done();
63
+ });
64
+
65
+ // Handle pause/play commands from editor
66
+ node.on('close', function(done) {
67
+ node.latestMessage = null;
68
+ done();
69
+ });
70
+ }
71
+
72
+ RED.nodes.registerType("event-preview", EventPreviewNode);
73
+
74
+ // HTTP endpoint to get latest message data
75
+ RED.httpAdmin.get("/event-preview/:id/latest", function(req, res) {
76
+ const node = RED.nodes.getNode(req.params.id);
77
+ if (node && node.latestMessage) {
78
+ res.json(node.latestMessage);
79
+ } else {
80
+ res.json(null);
81
+ }
82
+ });
83
+
84
+ // HTTP endpoint to toggle active state
85
+ RED.httpAdmin.post("/event-preview/:id/toggle", function(req, res) {
86
+ const node = RED.nodes.getNode(req.params.id);
87
+ if (node) {
88
+ node.active = !node.active;
89
+ node.status({
90
+ fill: node.active ? "green" : "grey",
91
+ shape: "dot",
92
+ text: node.active ? "active" : "paused"
93
+ });
94
+ res.json({ active: node.active });
95
+ } else {
96
+ res.sendStatus(404);
97
+ }
98
+ });
99
+
100
+ // HTTP endpoint to set active state
101
+ RED.httpAdmin.post("/event-preview/:id/active/:state", function(req, res) {
102
+ const node = RED.nodes.getNode(req.params.id);
103
+ if (node) {
104
+ node.active = req.params.state === 'true';
105
+ node.status({
106
+ fill: node.active ? "green" : "grey",
107
+ shape: "dot",
108
+ text: node.active ? "active" : "paused"
109
+ });
110
+ res.json({ active: node.active });
111
+ } else {
112
+ res.sendStatus(404);
113
+ }
114
+ });
115
+
116
+ // HTTP endpoint to get active state
117
+ RED.httpAdmin.get("/event-preview/:id/active", function(req, res) {
118
+ const node = RED.nodes.getNode(req.params.id);
119
+ if (node) {
120
+ res.json({ active: node.active });
121
+ } else {
122
+ res.sendStatus(404);
123
+ }
124
+ });
125
+ };
@@ -87,7 +87,7 @@ module.exports = function(RED) {
87
87
  const msg = {
88
88
  topic: topic,
89
89
  payload: value,
90
- timestamp: now,
90
+ timestamp: new Date(now).toISOString(),
91
91
  _simulator: {
92
92
  waveform: node.waveform,
93
93
  frequency: node.frequency,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-event-calc",
3
- "version": "2.0.3",
3
+ "version": "3.3.6",
4
4
  "description": "Node-RED nodes for event caching and calculations",
5
5
  "author": {
6
6
  "name": "Holger Amort"
@@ -29,7 +29,8 @@
29
29
  "event-in": "nodes/event-in.js",
30
30
  "event-topic": "nodes/event-topic.js",
31
31
  "event-calc": "nodes/event-calc.js",
32
- "event-json": "nodes/event-json.js",
32
+ "event-flatten": "nodes/event-flatten.js",
33
+ "event-preview": "nodes/event-preview.js",
33
34
  "event-simulator": "nodes/event-simulator.js",
34
35
  "event-chart": "nodes/event-chart.js"
35
36
  }
@@ -1,64 +0,0 @@
1
- <style>
2
- .event-json-white-text { fill: #ffffff !important; }
3
- .red-ui-palette-node[data-palette-type="event-json"] .red-ui-palette-label { color: #ffffff !important; }
4
- </style>
5
-
6
- <script type="text/javascript">
7
- RED.nodes.registerType('event-json', {
8
- category: 'event calc',
9
- color: '#758467',
10
- defaults: {
11
- name: { value: "" }
12
- },
13
- inputs: 1,
14
- outputs: 1,
15
- icon: "font-awesome/fa-exchange",
16
- label: function() {
17
- return this.name || "event json";
18
- },
19
- paletteLabel: "event json",
20
- labelStyle: function() { return (this.name ? "node_label_italic" : "") + " event-json-white-text"; }
21
- });
22
- </script>
23
-
24
- <script type="text/html" data-template-name="event-json">
25
- <div class="form-row">
26
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
- <input type="text" id="node-input-name" placeholder="Name">
28
- </div>
29
- </script>
30
-
31
- <script type="text/html" data-help-name="event-json">
32
- <p>Bidirectional JSON envelope converter. Automatically detects direction.</p>
33
-
34
- <h3>Behavior</h3>
35
- <dl class="message-properties">
36
- <dt>Unwrap (JSON to Message)</dt>
37
- <dd>If payload is <code>{value, topic?, timestamp?}</code>, extracts value to <code>msg.payload</code> and copies topic/timestamp to msg.</dd>
38
- <dt>Wrap (Message to JSON)</dt>
39
- <dd>If payload is any other value, wraps it as <code>{timestamp, topic, value}</code>.</dd>
40
- </dl>
41
-
42
- <h3>Examples</h3>
43
- <h4>Wrap (before MQTT publish)</h4>
44
- <pre>
45
- Input: msg.topic = "sensor/temp", msg.payload = 25.5
46
- Output: msg.payload = {
47
- timestamp: 1704900000000,
48
- topic: "sensor/temp",
49
- value: 25.5
50
- }</pre>
51
-
52
- <h4>Unwrap (after MQTT subscribe)</h4>
53
- <pre>
54
- Input: msg.payload = '{"timestamp":1704900000000,"topic":"sensor/temp","value":25.5}'
55
- Output: msg.topic = "sensor/temp"
56
- msg.payload = 25.5
57
- msg.timestamp = 1704900000000</pre>
58
-
59
- <h3>Status</h3>
60
- <ul>
61
- <li><b>Green "wrapped"</b> - Created JSON envelope</li>
62
- <li><b>Blue "unwrapped"</b> - Extracted from JSON envelope</li>
63
- </ul>
64
- </script>
@@ -1,69 +0,0 @@
1
- /**
2
- * event-json - Bidirectional JSON envelope converter
3
- *
4
- * Automatically detects direction:
5
- * - Object with {value, topic?, timestamp?} -> extracts to msg
6
- * - Any other payload -> wraps in {timestamp, topic, value}
7
- */
8
- module.exports = function(RED) {
9
- function EventJsonNode(config) {
10
- RED.nodes.createNode(this, config);
11
- const node = this;
12
-
13
- node.on('input', function(msg, send, done) {
14
- send = send || function() { node.send.apply(node, arguments); };
15
- done = done || function(err) { if (err) node.error(err, msg); };
16
-
17
- try {
18
- let data = msg.payload;
19
-
20
- // If string, try to parse as JSON
21
- if (typeof data === 'string') {
22
- try {
23
- data = JSON.parse(data);
24
- } catch (e) {
25
- // Not JSON string - wrap it
26
- msg.payload = {
27
- timestamp: Date.now(),
28
- topic: msg.topic,
29
- value: msg.payload
30
- };
31
- node.status({ fill: "green", shape: "dot", text: "wrapped" });
32
- send(msg);
33
- done();
34
- return;
35
- }
36
- }
37
-
38
- // Check if it's an envelope object (has 'value' property)
39
- if (typeof data === 'object' && data !== null && 'value' in data) {
40
- // Unwrap: extract from envelope
41
- if (data.topic) {
42
- msg.topic = data.topic;
43
- }
44
- if (data.timestamp) {
45
- msg.timestamp = data.timestamp;
46
- }
47
- msg.payload = data.value;
48
- node.status({ fill: "blue", shape: "dot", text: "unwrapped" });
49
- } else {
50
- // Wrap: create envelope
51
- msg.payload = {
52
- timestamp: Date.now(),
53
- topic: msg.topic,
54
- value: data
55
- };
56
- node.status({ fill: "green", shape: "dot", text: "wrapped" });
57
- }
58
-
59
- send(msg);
60
- done();
61
- } catch (err) {
62
- node.status({ fill: "red", shape: "ring", text: "error" });
63
- done(err);
64
- }
65
- });
66
- }
67
-
68
- RED.nodes.registerType("event-json", EventJsonNode);
69
- };
@@ -1,22 +0,0 @@
1
- // @ts-check
2
- const { defineConfig } = require('@playwright/test');
3
-
4
- module.exports = defineConfig({
5
- testDir: './tests',
6
- fullyParallel: false,
7
- forbidOnly: !!process.env.CI,
8
- retries: process.env.CI ? 2 : 0,
9
- workers: 1,
10
- reporter: 'html',
11
- use: {
12
- baseURL: 'http://localhost:1880',
13
- trace: 'on-first-retry',
14
- screenshot: 'only-on-failure',
15
- },
16
- projects: [
17
- {
18
- name: 'chromium',
19
- use: { browserName: 'chromium' },
20
- },
21
- ],
22
- });