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.
- package/README.md +36 -2
- package/examples/change-detection-example.json +448 -0
- package/nodes/event-cache.js +18 -2
- package/nodes/event-calc.js +70 -10
- package/nodes/event-flatten.html +53 -0
- package/nodes/event-flatten.js +25 -0
- package/nodes/event-preview.html +264 -0
- package/nodes/event-preview.js +125 -0
- package/nodes/event-simulator.js +1 -1
- package/package.json +3 -2
- package/nodes/event-json.html +0 -64
- package/nodes/event-json.js +0 -69
- package/playwright.config.js +0 -22
- package/tests/external-trigger.spec.js +0 -141
|
@@ -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
|
+
};
|
package/nodes/event-simulator.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-event-calc",
|
|
3
|
-
"version": "
|
|
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-
|
|
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
|
}
|
package/nodes/event-json.html
DELETED
|
@@ -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>
|
package/nodes/event-json.js
DELETED
|
@@ -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
|
-
};
|
package/playwright.config.js
DELETED
|
@@ -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
|
-
});
|