node-red-contrib-event-calc 0.1.1 → 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.
- package/nodes/event-cache.js +84 -139
- package/nodes/event-calc.html +44 -22
- package/nodes/event-calc.js +73 -26
- package/nodes/event-chart.html +239 -0
- package/nodes/event-chart.js +106 -0
- package/nodes/event-in.html +8 -2
- package/nodes/event-simulator.html +156 -0
- package/nodes/event-simulator.js +185 -0
- package/nodes/event-topic.html +23 -31
- package/nodes/event-topic.js +33 -40
- package/package.json +4 -2
package/nodes/event-calc.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* event-calc - Calculation node for multi-topic expressions
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* - Maps
|
|
5
|
+
* - Maps variables to exact topics
|
|
6
6
|
* - Evaluates JavaScript expressions when inputs update
|
|
7
7
|
* - Trigger modes: 'any' (any input updates) or 'all' (all inputs have values)
|
|
8
8
|
* - Safe expression evaluation using Function constructor
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - Built-in helper functions for common operations
|
|
11
11
|
*/
|
|
12
12
|
module.exports = function(RED) {
|
|
13
|
+
|
|
13
14
|
// Helper functions available in expressions
|
|
14
15
|
const helpers = {
|
|
15
16
|
// Math shortcuts
|
|
@@ -67,7 +68,6 @@ module.exports = function(RED) {
|
|
|
67
68
|
node.outputTopic = config.outputTopic || 'calc/result';
|
|
68
69
|
|
|
69
70
|
const subscriptionIds = [];
|
|
70
|
-
const latestValues = new Map(); // name -> { topic, value, ts }
|
|
71
71
|
|
|
72
72
|
if (!node.cacheConfig) {
|
|
73
73
|
node.status({ fill: "red", shape: "ring", text: "no cache configured" });
|
|
@@ -84,32 +84,43 @@ module.exports = function(RED) {
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Track subscribed topics to ignore updates from our own output
|
|
88
|
+
const subscribedTopics = new Set();
|
|
89
|
+
for (const input of node.inputMappings) {
|
|
90
|
+
const topicName = input.topic || input.pattern;
|
|
91
|
+
if (topicName) {
|
|
92
|
+
subscribedTopics.add(topicName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
/**
|
|
88
97
|
* Attempt to calculate and output result
|
|
89
98
|
*/
|
|
90
|
-
function tryCalculate(triggerTopic) {
|
|
91
|
-
//
|
|
99
|
+
function tryCalculate(triggerTopic, latestValues) {
|
|
100
|
+
// Ignore updates triggered by our own output
|
|
101
|
+
if (triggerTopic === node.outputTopic) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
if (node.triggerOn === 'all') {
|
|
93
|
-
// All inputs must have values
|
|
94
106
|
for (const input of node.inputMappings) {
|
|
95
107
|
if (!latestValues.has(input.name)) {
|
|
96
|
-
return;
|
|
108
|
+
return;
|
|
97
109
|
}
|
|
98
110
|
}
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
// At least one value must exist to calculate
|
|
102
113
|
if (latestValues.size === 0) {
|
|
103
114
|
return;
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
// Build context object for expression evaluation
|
|
107
117
|
const context = {};
|
|
108
118
|
const inputDetails = {};
|
|
119
|
+
const missingInputs = [];
|
|
109
120
|
|
|
110
121
|
for (const input of node.inputMappings) {
|
|
111
122
|
const data = latestValues.get(input.name);
|
|
112
|
-
if (data) {
|
|
123
|
+
if (data && data.value !== undefined && data.value !== null) {
|
|
113
124
|
context[input.name] = data.value;
|
|
114
125
|
inputDetails[input.name] = {
|
|
115
126
|
topic: data.topic,
|
|
@@ -118,63 +129,98 @@ module.exports = function(RED) {
|
|
|
118
129
|
};
|
|
119
130
|
} else {
|
|
120
131
|
context[input.name] = undefined;
|
|
132
|
+
missingInputs.push(input.name);
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
//
|
|
136
|
+
// Build topics mapping: variable name -> topic
|
|
137
|
+
const topics = { _output: node.outputTopic };
|
|
138
|
+
const timestamps = {};
|
|
139
|
+
for (const [name, details] of Object.entries(inputDetails)) {
|
|
140
|
+
topics[name] = details.topic;
|
|
141
|
+
timestamps[name] = details.ts;
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
try {
|
|
126
|
-
// Create a function with named parameters from context + helpers
|
|
127
145
|
const allParams = { ...helpers, ...context };
|
|
128
146
|
const paramNames = Object.keys(allParams);
|
|
129
147
|
const paramValues = Object.values(allParams);
|
|
130
148
|
|
|
131
|
-
// Build function body with helpers and variables available
|
|
132
149
|
const fn = new Function(...paramNames, `return ${node.expression};`);
|
|
133
150
|
const result = fn(...paramValues);
|
|
134
151
|
|
|
152
|
+
// Check for NaN or invalid result
|
|
153
|
+
if (typeof result === 'number' && isNaN(result)) {
|
|
154
|
+
const errorMsg = {
|
|
155
|
+
topic: node.outputTopic,
|
|
156
|
+
payload: {
|
|
157
|
+
error: 'Expression resulted in NaN',
|
|
158
|
+
missingInputs: missingInputs,
|
|
159
|
+
expression: node.expression
|
|
160
|
+
},
|
|
161
|
+
inputs: inputDetails,
|
|
162
|
+
trigger: triggerTopic,
|
|
163
|
+
timestamp: Date.now()
|
|
164
|
+
};
|
|
165
|
+
node.send([null, errorMsg]);
|
|
166
|
+
node.status({ fill: "yellow", shape: "ring", text: "NaN" });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
135
170
|
const msg = {
|
|
136
171
|
topic: node.outputTopic,
|
|
137
172
|
payload: result,
|
|
173
|
+
topics: topics,
|
|
138
174
|
inputs: inputDetails,
|
|
175
|
+
timestamps: timestamps,
|
|
139
176
|
expression: node.expression,
|
|
140
177
|
trigger: triggerTopic,
|
|
141
178
|
timestamp: Date.now()
|
|
142
179
|
};
|
|
143
180
|
|
|
144
|
-
node.send(msg);
|
|
181
|
+
node.send([msg, null]);
|
|
145
182
|
|
|
146
|
-
// Store result back in cache so it can be used by other calculations
|
|
147
183
|
node.cacheConfig.setValue(node.outputTopic, result, {
|
|
148
184
|
source: 'event-calc',
|
|
149
185
|
expression: node.expression,
|
|
150
186
|
inputs: Object.keys(inputDetails)
|
|
151
187
|
});
|
|
152
188
|
|
|
153
|
-
// Update status with result (truncate if too long)
|
|
154
189
|
const resultStr = String(result);
|
|
155
190
|
const displayResult = resultStr.length > 15 ? resultStr.substring(0, 12) + '...' : resultStr;
|
|
156
191
|
node.status({ fill: "green", shape: "dot", text: `= ${displayResult}` });
|
|
157
192
|
|
|
158
193
|
} catch (err) {
|
|
194
|
+
const errorMsg = {
|
|
195
|
+
topic: node.outputTopic,
|
|
196
|
+
payload: {
|
|
197
|
+
error: err.message,
|
|
198
|
+
expression: node.expression,
|
|
199
|
+
context: context
|
|
200
|
+
},
|
|
201
|
+
inputs: inputDetails,
|
|
202
|
+
trigger: triggerTopic,
|
|
203
|
+
timestamp: Date.now()
|
|
204
|
+
};
|
|
205
|
+
node.send([null, errorMsg]);
|
|
159
206
|
node.status({ fill: "red", shape: "ring", text: "eval error" });
|
|
160
|
-
node.error(`Expression evaluation failed: ${err.message}`, { expression: node.expression, context: context });
|
|
161
207
|
}
|
|
162
208
|
}
|
|
163
209
|
|
|
164
|
-
// Subscribe to
|
|
210
|
+
// Subscribe to inputs
|
|
211
|
+
const latestValues = new Map();
|
|
212
|
+
|
|
165
213
|
for (const input of node.inputMappings) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
214
|
+
const topicName = input.topic || input.pattern;
|
|
215
|
+
if (!input.name || !topicName) continue;
|
|
169
216
|
|
|
170
|
-
const subId = node.cacheConfig.subscribe(
|
|
217
|
+
const subId = node.cacheConfig.subscribe(topicName, (topic, entry) => {
|
|
171
218
|
latestValues.set(input.name, {
|
|
172
219
|
topic: topic,
|
|
173
220
|
value: entry.value,
|
|
174
221
|
ts: entry.ts
|
|
175
222
|
});
|
|
176
|
-
|
|
177
|
-
tryCalculate(topic);
|
|
223
|
+
tryCalculate(topic, latestValues);
|
|
178
224
|
});
|
|
179
225
|
subscriptionIds.push(subId);
|
|
180
226
|
}
|
|
@@ -193,9 +239,11 @@ module.exports = function(RED) {
|
|
|
193
239
|
node.status({ fill: "blue", shape: "dot", text: "expr updated" });
|
|
194
240
|
}
|
|
195
241
|
|
|
196
|
-
// Force recalculation
|
|
242
|
+
// Force recalculation (use special topic to bypass self-output check)
|
|
197
243
|
if (msg.payload === 'recalc' || msg.topic === 'recalc') {
|
|
198
|
-
|
|
244
|
+
if (latestValues.size > 0) {
|
|
245
|
+
tryCalculate('_recalc', latestValues);
|
|
246
|
+
}
|
|
199
247
|
}
|
|
200
248
|
|
|
201
249
|
done();
|
|
@@ -208,7 +256,6 @@ module.exports = function(RED) {
|
|
|
208
256
|
}
|
|
209
257
|
}
|
|
210
258
|
subscriptionIds.length = 0;
|
|
211
|
-
latestValues.clear();
|
|
212
259
|
done();
|
|
213
260
|
});
|
|
214
261
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.event-calc-white-text { fill: #ffffff !important; }
|
|
3
|
+
.red-ui-palette-node[data-palette-type="event-chart"] .red-ui-palette-label { color: #ffffff !important; }
|
|
4
|
+
</style>
|
|
5
|
+
|
|
6
|
+
<script type="text/javascript">
|
|
7
|
+
RED.nodes.registerType('event-chart', {
|
|
8
|
+
category: 'event calc',
|
|
9
|
+
color: '#758467',
|
|
10
|
+
defaults: {
|
|
11
|
+
name: { value: "" },
|
|
12
|
+
title: { value: "Event Chart" },
|
|
13
|
+
maxPoints: { value: 200, validate: RED.validators.number() },
|
|
14
|
+
timestampField: { value: "timestamp" },
|
|
15
|
+
valueField: { value: "payload" },
|
|
16
|
+
seriesField: { value: "topic" }
|
|
17
|
+
},
|
|
18
|
+
inputs: 1,
|
|
19
|
+
outputs: 0,
|
|
20
|
+
icon: "font-awesome/fa-line-chart",
|
|
21
|
+
paletteLabel: "chart",
|
|
22
|
+
label: function() {
|
|
23
|
+
return this.name || this.title || "Event Chart";
|
|
24
|
+
},
|
|
25
|
+
labelStyle: function() {
|
|
26
|
+
return (this.name ? "node_label_italic" : "") + " event-calc-white-text";
|
|
27
|
+
},
|
|
28
|
+
oneditprepare: function() {
|
|
29
|
+
var node = this;
|
|
30
|
+
var chartInstance = null;
|
|
31
|
+
var container = document.getElementById('event-chart-preview-container');
|
|
32
|
+
|
|
33
|
+
// Create canvas
|
|
34
|
+
var canvas = document.createElement('canvas');
|
|
35
|
+
canvas.id = 'event-chart-canvas-' + node.id;
|
|
36
|
+
canvas.style.width = '100%';
|
|
37
|
+
canvas.style.height = '100%';
|
|
38
|
+
container.innerHTML = '';
|
|
39
|
+
container.appendChild(canvas);
|
|
40
|
+
|
|
41
|
+
// Clear button
|
|
42
|
+
$('#event-chart-clear-btn').on('click', function() {
|
|
43
|
+
$.post('event-chart/' + node.id + '/clear', function(data) {
|
|
44
|
+
if (chartInstance) {
|
|
45
|
+
chartInstance.data.datasets = [];
|
|
46
|
+
chartInstance.update('none');
|
|
47
|
+
}
|
|
48
|
+
}).fail(function() {
|
|
49
|
+
RED.notify("Failed to clear chart", "error");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function loadScript(src) {
|
|
54
|
+
return new Promise(function(resolve, reject) {
|
|
55
|
+
if (document.querySelector('script[src="' + src + '"]')) {
|
|
56
|
+
resolve();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
var script = document.createElement('script');
|
|
60
|
+
script.src = src;
|
|
61
|
+
script.onload = resolve;
|
|
62
|
+
script.onerror = reject;
|
|
63
|
+
document.head.appendChild(script);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function initChart() {
|
|
68
|
+
if (chartInstance) {
|
|
69
|
+
chartInstance.destroy();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var ctx = document.getElementById('event-chart-canvas-' + node.id);
|
|
73
|
+
if (!ctx) return;
|
|
74
|
+
|
|
75
|
+
chartInstance = new Chart(ctx, {
|
|
76
|
+
type: 'line',
|
|
77
|
+
data: { datasets: [] },
|
|
78
|
+
options: {
|
|
79
|
+
responsive: true,
|
|
80
|
+
maintainAspectRatio: false,
|
|
81
|
+
animation: false,
|
|
82
|
+
scales: {
|
|
83
|
+
x: {
|
|
84
|
+
type: 'linear',
|
|
85
|
+
title: { display: true, text: 'Time' },
|
|
86
|
+
ticks: {
|
|
87
|
+
callback: function(value) {
|
|
88
|
+
return new Date(value).toLocaleTimeString();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
y: { title: { display: true, text: 'Value' } }
|
|
93
|
+
},
|
|
94
|
+
plugins: {
|
|
95
|
+
legend: { position: 'top' },
|
|
96
|
+
title: { display: true, text: node.title || 'Event Chart' }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Subscribe to chart data updates
|
|
102
|
+
RED.comms.subscribe("event-chart-data-" + node.id, function(topic, msg) {
|
|
103
|
+
if (chartInstance && msg && msg.data) {
|
|
104
|
+
updateChart(msg.data);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Load initial data
|
|
109
|
+
$.getJSON('event-chart/' + node.id + '/data', function(data) {
|
|
110
|
+
if (data && data.data) {
|
|
111
|
+
updateChart(data.data);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function updateChart(data) {
|
|
117
|
+
if (!chartInstance) return;
|
|
118
|
+
|
|
119
|
+
var colors = [
|
|
120
|
+
'#2196F3', '#FF5722', '#4CAF50', '#9C27B0',
|
|
121
|
+
'#FF9800', '#00BCD4', '#E91E63', '#8BC34A',
|
|
122
|
+
'#3F51B5', '#CDDC39', '#009688', '#FFC107'
|
|
123
|
+
];
|
|
124
|
+
var datasets = [];
|
|
125
|
+
var i = 0;
|
|
126
|
+
for (var series in data) {
|
|
127
|
+
datasets.push({
|
|
128
|
+
label: series,
|
|
129
|
+
data: data[series],
|
|
130
|
+
borderColor: colors[i % colors.length],
|
|
131
|
+
backgroundColor: colors[i % colors.length] + '20',
|
|
132
|
+
pointRadius: 2,
|
|
133
|
+
borderWidth: 2,
|
|
134
|
+
tension: 0.1,
|
|
135
|
+
fill: false
|
|
136
|
+
});
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
chartInstance.data.datasets = datasets;
|
|
140
|
+
chartInstance.update('none');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Load Chart.js
|
|
144
|
+
if (typeof Chart === 'undefined') {
|
|
145
|
+
loadScript('https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js')
|
|
146
|
+
.then(function() {
|
|
147
|
+
setTimeout(initChart, 100);
|
|
148
|
+
})
|
|
149
|
+
.catch(function(err) {
|
|
150
|
+
container.innerHTML = '<p style="color:red">Failed to load Chart.js</p>';
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
initChart();
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
oneditcancel: function() {
|
|
157
|
+
RED.comms.unsubscribe("event-chart-data-" + this.id);
|
|
158
|
+
},
|
|
159
|
+
oneditsave: function() {
|
|
160
|
+
RED.comms.unsubscribe("event-chart-data-" + this.id);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<script type="text/html" data-template-name="event-chart">
|
|
166
|
+
<div class="form-row">
|
|
167
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
168
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
169
|
+
</div>
|
|
170
|
+
<div class="form-row">
|
|
171
|
+
<label for="node-input-title"><i class="fa fa-header"></i> Title</label>
|
|
172
|
+
<input type="text" id="node-input-title" placeholder="Event Chart">
|
|
173
|
+
</div>
|
|
174
|
+
<div class="form-row">
|
|
175
|
+
<label for="node-input-valueField"><i class="fa fa-line-chart"></i> Value Field</label>
|
|
176
|
+
<input type="text" id="node-input-valueField" placeholder="payload">
|
|
177
|
+
</div>
|
|
178
|
+
<div class="form-row">
|
|
179
|
+
<label for="node-input-timestampField"><i class="fa fa-clock-o"></i> Timestamp Field</label>
|
|
180
|
+
<input type="text" id="node-input-timestampField" placeholder="timestamp">
|
|
181
|
+
</div>
|
|
182
|
+
<div class="form-row">
|
|
183
|
+
<label for="node-input-seriesField"><i class="fa fa-tags"></i> Series Field</label>
|
|
184
|
+
<input type="text" id="node-input-seriesField" placeholder="topic">
|
|
185
|
+
</div>
|
|
186
|
+
<div class="form-row">
|
|
187
|
+
<label for="node-input-maxPoints"><i class="fa fa-database"></i> Max Points</label>
|
|
188
|
+
<input type="number" id="node-input-maxPoints" placeholder="200">
|
|
189
|
+
</div>
|
|
190
|
+
<div class="form-row">
|
|
191
|
+
<label><i class="fa fa-area-chart"></i> Preview</label>
|
|
192
|
+
<div id="event-chart-preview-container" style="height: 250px; border: 1px solid #ccc; border-radius: 4px; padding: 5px; background: #fafafa;">
|
|
193
|
+
<p style="color:#888; text-align:center; padding-top:100px;">Loading chart...</p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="form-row">
|
|
197
|
+
<button type="button" id="event-chart-clear-btn" class="red-ui-button" style="width: 100%;">
|
|
198
|
+
<i class="fa fa-trash"></i> Clear Chart Data
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
</script>
|
|
202
|
+
|
|
203
|
+
<script type="text/html" data-help-name="event-chart">
|
|
204
|
+
<p>Displays time-series data on an interactive chart.</p>
|
|
205
|
+
|
|
206
|
+
<h3>Inputs</h3>
|
|
207
|
+
<dl class="message-properties">
|
|
208
|
+
<dt>payload <span class="property-type">number</span></dt>
|
|
209
|
+
<dd>The value to plot (configurable field name)</dd>
|
|
210
|
+
<dt class="optional">timestamp <span class="property-type">number</span></dt>
|
|
211
|
+
<dd>Unix timestamp in ms (defaults to current time)</dd>
|
|
212
|
+
<dt class="optional">topic <span class="property-type">string</span></dt>
|
|
213
|
+
<dd>Series name for grouping data (default: "default")</dd>
|
|
214
|
+
</dl>
|
|
215
|
+
|
|
216
|
+
<h3>Properties</h3>
|
|
217
|
+
<ul>
|
|
218
|
+
<li><b>Title</b> - Chart title displayed at top</li>
|
|
219
|
+
<li><b>Value Field</b> - Message property for value (default: payload)</li>
|
|
220
|
+
<li><b>Timestamp Field</b> - Message property for timestamp</li>
|
|
221
|
+
<li><b>Series Field</b> - Message property for series name</li>
|
|
222
|
+
<li><b>Max Points</b> - Maximum points per series (older points removed)</li>
|
|
223
|
+
</ul>
|
|
224
|
+
|
|
225
|
+
<h3>Usage</h3>
|
|
226
|
+
<p>Connect to any node that outputs numeric data. The chart will automatically:</p>
|
|
227
|
+
<ul>
|
|
228
|
+
<li>Group data by series (topic)</li>
|
|
229
|
+
<li>Display multiple series with different colors</li>
|
|
230
|
+
<li>Show real-time updates as data arrives</li>
|
|
231
|
+
<li>Limit data points to prevent memory issues</li>
|
|
232
|
+
</ul>
|
|
233
|
+
|
|
234
|
+
<h3>Clear Data</h3>
|
|
235
|
+
<p>Use the "Clear Chart Data" button, or send <code>msg.payload = "_clear"</code>.</p>
|
|
236
|
+
|
|
237
|
+
<h3>Preview</h3>
|
|
238
|
+
<p>Double-click the node to see a live preview of the chart with current data.</p>
|
|
239
|
+
</script>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function EventChartNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
node.title = config.title || 'Event Chart';
|
|
7
|
+
node.maxPoints = parseInt(config.maxPoints) || 200;
|
|
8
|
+
node.timestampField = config.timestampField || 'timestamp';
|
|
9
|
+
node.valueField = config.valueField || 'payload';
|
|
10
|
+
node.seriesField = config.seriesField || 'topic';
|
|
11
|
+
|
|
12
|
+
// Store data per series
|
|
13
|
+
node.chartData = {};
|
|
14
|
+
|
|
15
|
+
node.clearChart = function() {
|
|
16
|
+
node.chartData = {};
|
|
17
|
+
node.status({ fill: "grey", shape: "ring", text: "cleared" });
|
|
18
|
+
emitData();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
node.on('input', function(msg) {
|
|
22
|
+
// Handle clear command
|
|
23
|
+
if (msg.payload === '_clear' || msg.topic === '_clear') {
|
|
24
|
+
node.clearChart();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const series = RED.util.getMessageProperty(msg, node.seriesField) || 'default';
|
|
29
|
+
let timestamp = RED.util.getMessageProperty(msg, node.timestampField);
|
|
30
|
+
let value = RED.util.getMessageProperty(msg, node.valueField);
|
|
31
|
+
|
|
32
|
+
// Handle timestamp
|
|
33
|
+
if (timestamp === undefined || timestamp === null) {
|
|
34
|
+
timestamp = Date.now();
|
|
35
|
+
} else if (typeof timestamp !== 'number') {
|
|
36
|
+
timestamp = new Date(timestamp).getTime();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle value
|
|
40
|
+
if (value === undefined || value === null) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof value !== 'number') {
|
|
45
|
+
value = parseFloat(value);
|
|
46
|
+
if (isNaN(value)) return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!node.chartData[series]) {
|
|
50
|
+
node.chartData[series] = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
node.chartData[series].push({
|
|
54
|
+
x: timestamp,
|
|
55
|
+
y: value
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Limit points per series
|
|
59
|
+
if (node.chartData[series].length > node.maxPoints) {
|
|
60
|
+
node.chartData[series].shift();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const totalPoints = Object.values(node.chartData).reduce((sum, arr) => sum + arr.length, 0);
|
|
64
|
+
const seriesCount = Object.keys(node.chartData).length;
|
|
65
|
+
node.status({ fill: "green", shape: "dot", text: `${seriesCount} series, ${totalPoints} pts` });
|
|
66
|
+
|
|
67
|
+
emitData();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function emitData() {
|
|
71
|
+
RED.comms.publish("event-chart-data-" + node.id, {
|
|
72
|
+
id: node.id,
|
|
73
|
+
title: node.title,
|
|
74
|
+
data: node.chartData
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
node.on('close', function() {
|
|
79
|
+
node.chartData = {};
|
|
80
|
+
node.status({});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
RED.nodes.registerType("event-chart", EventChartNode);
|
|
85
|
+
|
|
86
|
+
// Clear chart data endpoint
|
|
87
|
+
RED.httpAdmin.post("/event-chart/:id/clear", function(req, res) {
|
|
88
|
+
const node = RED.nodes.getNode(req.params.id);
|
|
89
|
+
if (node && node.clearChart) {
|
|
90
|
+
node.clearChart();
|
|
91
|
+
res.sendStatus(200);
|
|
92
|
+
} else {
|
|
93
|
+
res.status(404).send("Node not found");
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Get chart data endpoint
|
|
98
|
+
RED.httpAdmin.get("/event-chart/:id/data", function(req, res) {
|
|
99
|
+
const node = RED.nodes.getNode(req.params.id);
|
|
100
|
+
if (node) {
|
|
101
|
+
res.json({ title: node.title, data: node.chartData || {} });
|
|
102
|
+
} else {
|
|
103
|
+
res.status(404).send("Node not found");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
};
|
package/nodes/event-in.html
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.event-calc-white-text { fill: #ffffff !important; }
|
|
3
|
+
.red-ui-palette-node[data-palette-type="event-in"] .red-ui-palette-label { color: #ffffff !important; }
|
|
4
|
+
</style>
|
|
5
|
+
|
|
1
6
|
<script type="text/javascript">
|
|
2
7
|
RED.nodes.registerType('event-in', {
|
|
3
8
|
category: 'event calc',
|
|
4
|
-
color: '#
|
|
9
|
+
color: '#758467',
|
|
5
10
|
defaults: {
|
|
6
11
|
name: { value: "" },
|
|
7
12
|
cache: { value: "", type: "event-cache", required: true },
|
|
@@ -14,7 +19,8 @@
|
|
|
14
19
|
label: function() {
|
|
15
20
|
return this.name || "event in";
|
|
16
21
|
},
|
|
17
|
-
paletteLabel: "event in"
|
|
22
|
+
paletteLabel: "event in",
|
|
23
|
+
labelStyle: function() { return (this.name ? "node_label_italic" : "") + " event-calc-white-text"; }
|
|
18
24
|
});
|
|
19
25
|
</script>
|
|
20
26
|
|