node-red-contrib-influxdb3 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -8
- package/influxdb3.html +24 -0
- package/influxdb3.js +144 -48
- package/package.json +6 -2
- package/renovate.json +6 -0
package/README.md
CHANGED
|
@@ -118,14 +118,55 @@ return msg;
|
|
|
118
118
|
|
|
119
119
|
### Data Types
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
**Important:** By default, **all numbers are written as floats** to avoid schema conflicts in InfluxDB. This is because JavaScript doesn't distinguish between `1.0` and `1` (both equal `1`), which can cause issues when InfluxDB expects a float but receives an integer.
|
|
122
|
+
|
|
123
|
+
When using object format, the node handles data types as follows:
|
|
124
|
+
- **Numbers**: Written as **float fields** by default
|
|
125
|
+
- **Integers**: Must be explicitly marked (see below)
|
|
125
126
|
- **Booleans**: Written as boolean fields
|
|
126
127
|
- **Strings**: Written as string fields
|
|
127
128
|
- **Tags**: Always converted to strings
|
|
128
129
|
|
|
130
|
+
#### Writing Integer Fields
|
|
131
|
+
|
|
132
|
+
To write integers explicitly, use one of these methods:
|
|
133
|
+
|
|
134
|
+
**Method 1: Using the `integers` array**
|
|
135
|
+
```javascript
|
|
136
|
+
msg.payload = {
|
|
137
|
+
fields: {
|
|
138
|
+
temperature: 21.5, // float
|
|
139
|
+
count: 42, // will be float by default
|
|
140
|
+
total: 100 // will be float by default
|
|
141
|
+
},
|
|
142
|
+
integers: ['count', 'total'] // mark these as integers
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Method 2: Using the `i` suffix**
|
|
147
|
+
```javascript
|
|
148
|
+
msg.payload = {
|
|
149
|
+
fields: {
|
|
150
|
+
temperature: 21.5, // float
|
|
151
|
+
count: "42i", // integer (note the string with 'i' suffix)
|
|
152
|
+
total: "100i" // integer
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Example with both floats and integers:**
|
|
158
|
+
```javascript
|
|
159
|
+
msg.measurement = "sensor_data";
|
|
160
|
+
msg.payload = {
|
|
161
|
+
temperature: 21.5, // float
|
|
162
|
+
humidity: 65.0, // float (even though it looks like an integer)
|
|
163
|
+
event_count: "50i", // integer
|
|
164
|
+
tags: {
|
|
165
|
+
location: "room1"
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
129
170
|
### Message Properties
|
|
130
171
|
|
|
131
172
|
The following message properties can be used to override node configuration:
|
|
@@ -133,6 +174,7 @@ The following message properties can be used to override node configuration:
|
|
|
133
174
|
- `msg.measurement` - Override the measurement name
|
|
134
175
|
- `msg.database` - Override the database name
|
|
135
176
|
- `msg.timestamp` - Set the timestamp for the data point (Date object or milliseconds)
|
|
177
|
+
- `msg.payload.integers` - Array of field names to write as integers (e.g., `['count', 'total']`)
|
|
136
178
|
|
|
137
179
|
## Examples
|
|
138
180
|
|
|
@@ -323,10 +365,6 @@ The node will display error status and log details to the Node-RED debug panel:
|
|
|
323
365
|
|
|
324
366
|
MIT
|
|
325
367
|
|
|
326
|
-
## Author
|
|
327
|
-
|
|
328
|
-
Your Name
|
|
329
|
-
|
|
330
368
|
## Links
|
|
331
369
|
|
|
332
370
|
- [InfluxDB v3 JavaScript Client](https://github.com/InfluxCommunity/influxdb3-js)
|
package/influxdb3.html
CHANGED
|
@@ -154,6 +154,30 @@
|
|
|
154
154
|
}
|
|
155
155
|
}</pre>
|
|
156
156
|
|
|
157
|
+
<h3>Data Types</h3>
|
|
158
|
+
<p><strong>Important:</strong> By default, <strong>all numbers are written as floats</strong> to avoid schema conflicts.
|
|
159
|
+
JavaScript doesn't distinguish between <code>1.0</code> and <code>1</code>, which can cause issues when InfluxDB expects a float.</p>
|
|
160
|
+
|
|
161
|
+
<h4>Writing Integer Fields</h4>
|
|
162
|
+
<p>To explicitly write integers, use one of these methods:</p>
|
|
163
|
+
|
|
164
|
+
<p><strong>Method 1: Using the <code>integers</code> array</strong></p>
|
|
165
|
+
<pre>msg.payload = {
|
|
166
|
+
fields: {
|
|
167
|
+
temperature: 21.5, // float
|
|
168
|
+
count: 42 // will be float by default
|
|
169
|
+
},
|
|
170
|
+
integers: ['count'] // mark count as integer
|
|
171
|
+
};</pre>
|
|
172
|
+
|
|
173
|
+
<p><strong>Method 2: Using the <code>i</code> suffix</strong></p>
|
|
174
|
+
<pre>msg.payload = {
|
|
175
|
+
fields: {
|
|
176
|
+
temperature: 21.5, // float
|
|
177
|
+
count: "42i" // integer (string with 'i' suffix)
|
|
178
|
+
}
|
|
179
|
+
};</pre>
|
|
180
|
+
|
|
157
181
|
<h3>Configuration</h3>
|
|
158
182
|
<dl class="message-properties">
|
|
159
183
|
<dt>Connection</dt>
|
package/influxdb3.js
CHANGED
|
@@ -5,6 +5,66 @@
|
|
|
5
5
|
module.exports = function(RED) {
|
|
6
6
|
const { InfluxDBClient, Point } = require('@influxdata/influxdb3-client');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Normalize host URL to ensure it has trailing slash
|
|
10
|
+
*/
|
|
11
|
+
function normalizeHost(host) {
|
|
12
|
+
if (!host || typeof host !== 'string') {
|
|
13
|
+
return host;
|
|
14
|
+
}
|
|
15
|
+
return host.endsWith('/') ? host : host + '/';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Process a field value and add it to a Point
|
|
20
|
+
*/
|
|
21
|
+
function addFieldToPoint(point, key, value, integerFields) {
|
|
22
|
+
if (value === null || value === undefined) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Handle string with 'i' suffix for integers (e.g., "42i")
|
|
27
|
+
if (typeof value === 'string' && /^-?\d+i$/.test(value)) {
|
|
28
|
+
const intValue = parseInt(value.slice(0, -1), 10);
|
|
29
|
+
// Validate parsed value
|
|
30
|
+
if (!isNaN(intValue) && isFinite(intValue)) {
|
|
31
|
+
point.setIntegerField(key, intValue);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
} else if (typeof value === 'number') {
|
|
35
|
+
// Validate number
|
|
36
|
+
if (!isFinite(value)) {
|
|
37
|
+
RED.log.warn(`Skipping field '${key}': value is not finite (${value})`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Default to float for all numbers (safe default)
|
|
42
|
+
// Use integer only if explicitly marked in 'integers' array
|
|
43
|
+
if (integerFields.has(key)) {
|
|
44
|
+
const intValue = Math.floor(value);
|
|
45
|
+
if (intValue !== value) {
|
|
46
|
+
RED.log.warn(`Field '${key}': truncating ${value} to ${intValue} for integer field`);
|
|
47
|
+
}
|
|
48
|
+
point.setIntegerField(key, intValue);
|
|
49
|
+
} else {
|
|
50
|
+
point.setFloatField(key, value);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
} else if (typeof value === 'boolean') {
|
|
54
|
+
point.setBooleanField(key, value);
|
|
55
|
+
return true;
|
|
56
|
+
} else if (typeof value === 'string') {
|
|
57
|
+
point.setStringField(key, value);
|
|
58
|
+
return true;
|
|
59
|
+
} else {
|
|
60
|
+
// Complex types (arrays, objects) are not supported
|
|
61
|
+
RED.log.warn(`Skipping field '${key}': unsupported type ${typeof value}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
8
68
|
/**
|
|
9
69
|
* Configuration node to hold InfluxDB v3 connection details
|
|
10
70
|
*/
|
|
@@ -24,13 +84,31 @@ module.exports = function(RED) {
|
|
|
24
84
|
// Get or create a client instance
|
|
25
85
|
this.getClient = function() {
|
|
26
86
|
if (!this.client) {
|
|
87
|
+
// Validate configuration
|
|
88
|
+
if (!this.host) {
|
|
89
|
+
throw new Error('InfluxDB host is not configured');
|
|
90
|
+
}
|
|
91
|
+
if (!this.token) {
|
|
92
|
+
throw new Error('InfluxDB token is not configured');
|
|
93
|
+
}
|
|
94
|
+
if (!this.database) {
|
|
95
|
+
throw new Error('InfluxDB database is not configured');
|
|
96
|
+
}
|
|
97
|
+
|
|
27
98
|
try {
|
|
99
|
+
const normalizedHost = normalizeHost(this.host);
|
|
100
|
+
|
|
101
|
+
RED.log.info(`InfluxDB v3: Connecting to ${normalizedHost} with database ${this.database}`);
|
|
102
|
+
|
|
28
103
|
this.client = new InfluxDBClient({
|
|
29
|
-
host:
|
|
104
|
+
host: normalizedHost,
|
|
30
105
|
token: this.token,
|
|
31
106
|
database: this.database
|
|
32
107
|
});
|
|
108
|
+
|
|
109
|
+
RED.log.info(`InfluxDB v3: Client created successfully`);
|
|
33
110
|
} catch (error) {
|
|
111
|
+
RED.log.error(`InfluxDB v3: Failed to create client - ${error.message}`);
|
|
34
112
|
throw new Error(`Failed to create InfluxDB client: ${error.message}`);
|
|
35
113
|
}
|
|
36
114
|
}
|
|
@@ -41,10 +119,11 @@ module.exports = function(RED) {
|
|
|
41
119
|
this.on('close', function() {
|
|
42
120
|
if (this.client) {
|
|
43
121
|
try {
|
|
122
|
+
RED.log.info('InfluxDB v3: Closing client connection');
|
|
44
123
|
this.client.close();
|
|
45
124
|
this.client = null;
|
|
46
125
|
} catch (error) {
|
|
47
|
-
|
|
126
|
+
RED.log.warn(`InfluxDB v3: Error closing client - ${error.message}`);
|
|
48
127
|
}
|
|
49
128
|
}
|
|
50
129
|
});
|
|
@@ -67,6 +146,7 @@ module.exports = function(RED) {
|
|
|
67
146
|
this.database = config.database;
|
|
68
147
|
|
|
69
148
|
const node = this;
|
|
149
|
+
let statusTimeout = null;
|
|
70
150
|
|
|
71
151
|
if (!this.influxdb) {
|
|
72
152
|
this.error('InfluxDB v3 config not set');
|
|
@@ -74,6 +154,23 @@ module.exports = function(RED) {
|
|
|
74
154
|
return;
|
|
75
155
|
}
|
|
76
156
|
|
|
157
|
+
// Helper to set status with auto-clear
|
|
158
|
+
function setStatus(status, clearAfterMs = 0) {
|
|
159
|
+
if (statusTimeout) {
|
|
160
|
+
clearTimeout(statusTimeout);
|
|
161
|
+
statusTimeout = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
node.status(status);
|
|
165
|
+
|
|
166
|
+
if (clearAfterMs > 0) {
|
|
167
|
+
statusTimeout = setTimeout(() => {
|
|
168
|
+
node.status({});
|
|
169
|
+
statusTimeout = null;
|
|
170
|
+
}, clearAfterMs);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
77
174
|
// Process incoming messages
|
|
78
175
|
node.on('input', async function(msg, send, done) {
|
|
79
176
|
// For Node-RED 0.x compatibility
|
|
@@ -98,8 +195,11 @@ module.exports = function(RED) {
|
|
|
98
195
|
|
|
99
196
|
// Check if msg.payload is already in line protocol format
|
|
100
197
|
if (typeof msg.payload === 'string') {
|
|
101
|
-
lineProtocol = msg.payload;
|
|
102
|
-
|
|
198
|
+
lineProtocol = msg.payload.trim();
|
|
199
|
+
if (!lineProtocol) {
|
|
200
|
+
throw new Error('Line protocol string is empty');
|
|
201
|
+
}
|
|
202
|
+
} else if (msg.payload && typeof msg.payload === 'object' && !Array.isArray(msg.payload)) {
|
|
103
203
|
// Build line protocol from payload object
|
|
104
204
|
const measurement = msg.measurement || node.measurement;
|
|
105
205
|
|
|
@@ -110,7 +210,7 @@ module.exports = function(RED) {
|
|
|
110
210
|
const point = new Point(measurement);
|
|
111
211
|
|
|
112
212
|
// Add tags
|
|
113
|
-
if (msg.payload.tags && typeof msg.payload.tags === 'object') {
|
|
213
|
+
if (msg.payload.tags && typeof msg.payload.tags === 'object' && !Array.isArray(msg.payload.tags)) {
|
|
114
214
|
for (const [key, value] of Object.entries(msg.payload.tags)) {
|
|
115
215
|
if (value !== null && value !== undefined) {
|
|
116
216
|
point.setTag(key, String(value));
|
|
@@ -118,61 +218,59 @@ module.exports = function(RED) {
|
|
|
118
218
|
}
|
|
119
219
|
}
|
|
120
220
|
|
|
221
|
+
// Get list of fields that should be treated as integers
|
|
222
|
+
const integerFields = new Set(msg.payload.integers || []);
|
|
223
|
+
let fieldCount = 0;
|
|
224
|
+
|
|
121
225
|
// Add fields
|
|
122
|
-
if (msg.payload.fields && typeof msg.payload.fields === 'object') {
|
|
226
|
+
if (msg.payload.fields && typeof msg.payload.fields === 'object' && !Array.isArray(msg.payload.fields)) {
|
|
227
|
+
// Explicit fields object
|
|
123
228
|
for (const [key, value] of Object.entries(msg.payload.fields)) {
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
if (Number.isInteger(value)) {
|
|
127
|
-
point.setIntegerField(key, value);
|
|
128
|
-
} else {
|
|
129
|
-
point.setFloatField(key, value);
|
|
130
|
-
}
|
|
131
|
-
} else if (typeof value === 'boolean') {
|
|
132
|
-
point.setBooleanField(key, value);
|
|
133
|
-
} else {
|
|
134
|
-
point.setStringField(key, String(value));
|
|
135
|
-
}
|
|
229
|
+
if (addFieldToPoint(point, key, value, integerFields)) {
|
|
230
|
+
fieldCount++;
|
|
136
231
|
}
|
|
137
232
|
}
|
|
138
233
|
} else {
|
|
139
|
-
//
|
|
234
|
+
// Simplified format: treat all non-reserved properties as fields
|
|
235
|
+
const reservedKeys = new Set(['tags', 'timestamp', 'integers', 'fields']);
|
|
140
236
|
for (const [key, value] of Object.entries(msg.payload)) {
|
|
141
|
-
if (key
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
point.setIntegerField(key, value);
|
|
145
|
-
} else {
|
|
146
|
-
point.setFloatField(key, value);
|
|
147
|
-
}
|
|
148
|
-
} else if (typeof value === 'boolean') {
|
|
149
|
-
point.setBooleanField(key, value);
|
|
150
|
-
} else if (typeof value !== 'object') {
|
|
151
|
-
point.setStringField(key, String(value));
|
|
237
|
+
if (!reservedKeys.has(key)) {
|
|
238
|
+
if (addFieldToPoint(point, key, value, integerFields)) {
|
|
239
|
+
fieldCount++;
|
|
152
240
|
}
|
|
153
241
|
}
|
|
154
242
|
}
|
|
155
243
|
}
|
|
156
244
|
|
|
245
|
+
if (fieldCount === 0) {
|
|
246
|
+
throw new Error('No valid fields to write - at least one field is required');
|
|
247
|
+
}
|
|
248
|
+
|
|
157
249
|
// Add timestamp if provided
|
|
158
250
|
if (msg.payload.timestamp) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
251
|
+
const ts = msg.payload.timestamp;
|
|
252
|
+
if (ts instanceof Date && !isNaN(ts.getTime())) {
|
|
253
|
+
point.setTimestamp(ts);
|
|
254
|
+
} else if (typeof ts === 'number' && isFinite(ts) && ts > 0) {
|
|
255
|
+
point.setTimestamp(new Date(ts));
|
|
256
|
+
} else {
|
|
257
|
+
node.warn(`Invalid timestamp in payload: ${ts}`);
|
|
163
258
|
}
|
|
164
259
|
} else if (msg.timestamp) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
260
|
+
const ts = msg.timestamp;
|
|
261
|
+
if (ts instanceof Date && !isNaN(ts.getTime())) {
|
|
262
|
+
point.setTimestamp(ts);
|
|
263
|
+
} else if (typeof ts === 'number' && isFinite(ts) && ts > 0) {
|
|
264
|
+
point.setTimestamp(new Date(ts));
|
|
265
|
+
} else {
|
|
266
|
+
node.warn(`Invalid timestamp in msg: ${ts}`);
|
|
169
267
|
}
|
|
170
268
|
}
|
|
171
269
|
|
|
172
270
|
lineProtocol = point.toLineProtocol();
|
|
173
271
|
|
|
174
|
-
if (!lineProtocol) {
|
|
175
|
-
throw new Error('
|
|
272
|
+
if (!lineProtocol || lineProtocol.trim() === '') {
|
|
273
|
+
throw new Error('Generated line protocol is empty');
|
|
176
274
|
}
|
|
177
275
|
} else {
|
|
178
276
|
throw new Error('Invalid payload format. Expected string (line protocol) or object with fields');
|
|
@@ -181,27 +279,25 @@ module.exports = function(RED) {
|
|
|
181
279
|
// Write to InfluxDB
|
|
182
280
|
await client.write(lineProtocol, targetDatabase);
|
|
183
281
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Clear status after 3 seconds
|
|
187
|
-
setTimeout(() => {
|
|
188
|
-
node.status({});
|
|
189
|
-
}, 3000);
|
|
282
|
+
setStatus({ fill: 'green', shape: 'dot', text: 'written' }, 3000);
|
|
190
283
|
|
|
191
284
|
send(msg);
|
|
192
285
|
done();
|
|
193
286
|
|
|
194
287
|
} catch (error) {
|
|
195
|
-
|
|
288
|
+
setStatus({ fill: 'red', shape: 'dot', text: 'error' });
|
|
196
289
|
done(error);
|
|
197
290
|
}
|
|
198
291
|
});
|
|
199
292
|
|
|
200
293
|
node.on('close', function() {
|
|
294
|
+
if (statusTimeout) {
|
|
295
|
+
clearTimeout(statusTimeout);
|
|
296
|
+
statusTimeout = null;
|
|
297
|
+
}
|
|
201
298
|
node.status({});
|
|
202
299
|
});
|
|
203
300
|
}
|
|
204
301
|
|
|
205
302
|
RED.nodes.registerType('influxdb3-write', InfluxDB3WriteNode);
|
|
206
303
|
};
|
|
207
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-influxdb3",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Node-RED nodes for InfluxDB v3 integration",
|
|
5
5
|
"main": "influxdb3.js",
|
|
6
6
|
"scripts": {
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
],
|
|
16
16
|
"author": "Stuart B",
|
|
17
17
|
"license": "MIT",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=14.0.0"
|
|
20
|
+
},
|
|
18
21
|
"repository": {
|
|
19
22
|
"type": "git",
|
|
20
23
|
"url": "https://github.com/stuartb55/nodered-influxdb3.git"
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
"url": "https://github.com/stuartb55/nodered-influxdb3/issues"
|
|
25
28
|
},
|
|
26
29
|
"node-red": {
|
|
30
|
+
"version": ">=3.0.0",
|
|
27
31
|
"nodes": {
|
|
28
32
|
"influxdb3": "influxdb3.js"
|
|
29
33
|
}
|
|
@@ -32,7 +36,7 @@
|
|
|
32
36
|
"@influxdata/influxdb3-client": "^1.4.0"
|
|
33
37
|
},
|
|
34
38
|
"peerDependencies": {
|
|
35
|
-
"node-red": ">=
|
|
39
|
+
"node-red": ">=3.0.0"
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|