mqtt-devices-parser 1.0.18 → 1.0.20
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/Changelog.md +10 -0
- package/Readme.md +80 -0
- package/config/index.js +17 -2
- package/example/kafka-example.js +181 -0
- package/index.js +14 -2
- package/models/models.js +3 -3
- package/models/mqtt.models.js +41 -0
- package/models/mqttTemplate.models.js +29 -0
- package/package.json +2 -1
- package/src/device/device.js +4 -0
- package/src/kafka/consumer.js +176 -0
- package/src/unitTest/kafka-integration.test.js +118 -0
- package/src/unitTest/kafka.test.js +72 -0
package/Changelog.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.20
|
|
4
|
+
Adds Kafka consumer support with optional MQTT message parsing (#5)
|
|
5
|
+
supports tables for mqtt templates
|
|
6
|
+
|
|
7
|
+
## 1.0.19
|
|
8
|
+
Removes adding project on launching process
|
|
9
|
+
models/models:
|
|
10
|
+
- seedCommonLwM2MObjects: avoids error on description property not defined on object
|
|
11
|
+
- changes lwm2m seed functions name
|
|
12
|
+
|
|
3
13
|
## 1.0.18
|
|
4
14
|
Fix and improve Sensors, checks uid length
|
|
5
15
|
Adds seedCommonLwM2MObjects and seedCommonLwM2MResources functions
|
package/Readme.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
## NPM Module
|
|
5
5
|
- MQTT parser
|
|
6
6
|
- Parses topics coming from devices and updates data on dB
|
|
7
|
+
- **NEW**: Kafka consumer support for multi-process message parsing
|
|
7
8
|
|
|
8
9
|
## Deploy
|
|
9
10
|
```
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
|
|
26
27
|
- mysql8.0
|
|
27
28
|
- MQTT
|
|
29
|
+
- **NEW**: Apache Kafka (optional)
|
|
28
30
|
|
|
29
31
|
## Front end
|
|
30
32
|
- Node service running the following [project](https://github.com/zimbora/mgmt-iot-web)
|
|
@@ -37,3 +39,81 @@ In your program set the same struct and pass it as argument on the module init c
|
|
|
37
39
|
An mqtt account will be created using the following settings
|
|
38
40
|
mqtt.user - mqtt user login
|
|
39
41
|
mqtt.pwd - mqtt pwd login
|
|
42
|
+
|
|
43
|
+
### Kafka Consumer Configuration (NEW)
|
|
44
|
+
|
|
45
|
+
The module now supports consuming messages from Kafka topics as an alternative or complement to MQTT. This enables multi-process message parsing using Kafka's shared subscription mechanism.
|
|
46
|
+
|
|
47
|
+
#### Environment Variables
|
|
48
|
+
|
|
49
|
+
**Kafka Settings:**
|
|
50
|
+
- `KAFKA_ENABLED` - Set to `'true'` to enable Kafka consumer (default: `false`)
|
|
51
|
+
- `KAFKA_BROKERS` - Comma-separated list of Kafka brokers (default: `'localhost:9092'`)
|
|
52
|
+
- `KAFKA_GROUP_ID` - Consumer group ID for shared subscriptions (default: `'mqtt-devices-parser-group'`)
|
|
53
|
+
- `KAFKA_CLIENT_ID` - Kafka client identifier (default: `'mqtt-devices-parser'`)
|
|
54
|
+
|
|
55
|
+
**Authentication (SASL):**
|
|
56
|
+
- `KAFKA_SASL_MECHANISM` - SASL mechanism: `'plain'`, `'scram-sha-256'`, `'scram-sha-512'` (default: `'plain'`)
|
|
57
|
+
- `KAFKA_SASL_USERNAME` - SASL username (default: empty)
|
|
58
|
+
- `KAFKA_SASL_PASSWORD` - SASL password (default: empty)
|
|
59
|
+
|
|
60
|
+
**Security:**
|
|
61
|
+
- `KAFKA_SSL` - Set to `'true'` to enable SSL (default: `false`)
|
|
62
|
+
|
|
63
|
+
**Connection Settings:**
|
|
64
|
+
- `KAFKA_CONNECTION_TIMEOUT` - Connection timeout in ms (default: `3000`)
|
|
65
|
+
- `KAFKA_REQUEST_TIMEOUT` - Request timeout in ms (default: `30000`)
|
|
66
|
+
|
|
67
|
+
**MQTT Parsing Control:**
|
|
68
|
+
- `MQTT_PARSE_MESSAGES` - Set to `'false'` to disable direct MQTT message parsing (default: `true`)
|
|
69
|
+
|
|
70
|
+
#### Usage Examples
|
|
71
|
+
|
|
72
|
+
**Example 1: Enable Kafka with basic authentication**
|
|
73
|
+
```bash
|
|
74
|
+
export KAFKA_ENABLED=true
|
|
75
|
+
export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
76
|
+
export KAFKA_SASL_USERNAME=myuser
|
|
77
|
+
export KAFKA_SASL_PASSWORD=mypassword
|
|
78
|
+
export KAFKA_SASL_MECHANISM=scram-sha-256
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Example 2: Use Kafka only (disable MQTT parsing)**
|
|
82
|
+
```bash
|
|
83
|
+
export KAFKA_ENABLED=true
|
|
84
|
+
export MQTT_PARSE_MESSAGES=false
|
|
85
|
+
export KAFKA_BROKERS=kafka-cluster:9092
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Example 3: Multi-process deployment**
|
|
89
|
+
```bash
|
|
90
|
+
# Start multiple instances with the same group ID for load balancing
|
|
91
|
+
export KAFKA_ENABLED=true
|
|
92
|
+
export KAFKA_GROUP_ID=my-parser-group
|
|
93
|
+
export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
94
|
+
# Each process will consume different partitions automatically
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Kafka Topic Structure
|
|
98
|
+
|
|
99
|
+
Kafka topics should correspond to project names configured in your `projects` array. Messages are expected to have:
|
|
100
|
+
|
|
101
|
+
- **Topic**: The project name (e.g., `demo`, `freeRTOS2`)
|
|
102
|
+
- **Key**: Device path part (e.g., `uid:123/status`)
|
|
103
|
+
- **Value**: Message payload (same format as MQTT payloads)
|
|
104
|
+
|
|
105
|
+
The consumer will automatically format messages as `{project}/{key}` to match the expected MQTT topic structure.
|
|
106
|
+
|
|
107
|
+
#### Shared Subscriptions
|
|
108
|
+
|
|
109
|
+
When multiple instances use the same `KAFKA_GROUP_ID`, Kafka automatically distributes messages across consumers, enabling:
|
|
110
|
+
|
|
111
|
+
- **Load balancing**: Messages are distributed across multiple parser instances
|
|
112
|
+
- **High availability**: If one instance fails, others continue processing
|
|
113
|
+
- **Scalability**: Add more instances to handle increased load
|
|
114
|
+
|
|
115
|
+
#### Message Flow
|
|
116
|
+
|
|
117
|
+
1. **MQTT Mode** (traditional): MQTT messages → `$.device.parseMessage`
|
|
118
|
+
2. **Kafka Mode**: Kafka messages → Topic formatting → `$.device.parseMessage`
|
|
119
|
+
3. **Hybrid Mode**: Both MQTT and Kafka messages → `$.device.parseMessage`
|
package/config/index.js
CHANGED
|
@@ -13,7 +13,22 @@ module.exports = {
|
|
|
13
13
|
user:process.env.MQTT_USER || 'admin',
|
|
14
14
|
pwd:process.env.MQTT_PWD || 'admin',
|
|
15
15
|
client:process.env.MQTT_CLIENT || 'mqtt-devices-parser',
|
|
16
|
-
logs_path: process.env.MQTT_LOGS || 'uServices'
|
|
16
|
+
logs_path: process.env.MQTT_LOGS || 'uServices',
|
|
17
|
+
parseMessages: process.env.MQTT_PARSE_MESSAGES !== 'false' // Default to true for backward compatibility
|
|
18
|
+
},
|
|
19
|
+
kafka: {
|
|
20
|
+
enabled: process.env.KAFKA_ENABLED === 'true' ? true : true,
|
|
21
|
+
brokers: (process.env.KAFKA_BROKERS || 'localhost:9092').split(','),
|
|
22
|
+
groupId: process.env.KAFKA_GROUP_ID || 'mqtt-devices-parser-group',
|
|
23
|
+
clientId: process.env.KAFKA_CLIENT_ID || 'mqtt-devices-parser',
|
|
24
|
+
sasl: {
|
|
25
|
+
mechanism: process.env.KAFKA_SASL_MECHANISM || 'plain', // plain, scram-sha-256, scram-sha-512
|
|
26
|
+
username: process.env.KAFKA_SASL_USERNAME || '',
|
|
27
|
+
password: process.env.KAFKA_SASL_PASSWORD || ''
|
|
28
|
+
},
|
|
29
|
+
ssl: process.env.KAFKA_SSL === 'true' ? true : false,
|
|
30
|
+
connectionTimeout: parseInt(process.env.KAFKA_CONNECTION_TIMEOUT) || 3000,
|
|
31
|
+
requestTimeout: parseInt(process.env.KAFKA_REQUEST_TIMEOUT) || 30000
|
|
17
32
|
},
|
|
18
33
|
mysqldb: {
|
|
19
34
|
conn_limit: process.env.DB_CONN_LIMIT || 15,
|
|
@@ -27,7 +42,7 @@ module.exports = {
|
|
|
27
42
|
projects: {
|
|
28
43
|
demo : process.env.demo || false,
|
|
29
44
|
},
|
|
30
|
-
demo : {
|
|
45
|
+
demo : { // not need anymore. Can be created on dashboard
|
|
31
46
|
uidPrefix: 'uid:',
|
|
32
47
|
uidLength: 16 // max len for uid
|
|
33
48
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example showing how to use mqtt-devices-parser with Kafka consumer support
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* 1. Traditional MQTT-only mode
|
|
6
|
+
* 2. Kafka-only mode
|
|
7
|
+
* 3. Hybrid mode (both MQTT and Kafka)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const parser = require('../index.js');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
global.BASE_DIR = process.cwd();
|
|
14
|
+
|
|
15
|
+
// Example configuration with Kafka support
|
|
16
|
+
const configWithKafka = {
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
dev: true,
|
|
19
|
+
web: {
|
|
20
|
+
protocol: "http://",
|
|
21
|
+
domain: 'localhost',
|
|
22
|
+
port: 8002,
|
|
23
|
+
},
|
|
24
|
+
mqtt: {
|
|
25
|
+
protocol: 'MQTT',
|
|
26
|
+
host: 'localhost',
|
|
27
|
+
port: '1883',
|
|
28
|
+
user: 'admin',
|
|
29
|
+
pwd: 'admin',
|
|
30
|
+
client: 'mqtt-devices-parser-example',
|
|
31
|
+
logs_path: 'uServices',
|
|
32
|
+
parseMessages: true // Set to false to disable MQTT message parsing
|
|
33
|
+
},
|
|
34
|
+
kafka: {
|
|
35
|
+
enabled: true, // Enable Kafka consumer
|
|
36
|
+
brokers: ['localhost:9092'], // Your Kafka brokers
|
|
37
|
+
groupId: 'mqtt-devices-parser-group',
|
|
38
|
+
clientId: 'mqtt-devices-parser-client',
|
|
39
|
+
sasl: {
|
|
40
|
+
mechanism: 'plain', // or 'scram-sha-256', 'scram-sha-512'
|
|
41
|
+
username: '', // Your Kafka username
|
|
42
|
+
password: '' // Your Kafka password
|
|
43
|
+
},
|
|
44
|
+
ssl: false, // Set to true for SSL/TLS
|
|
45
|
+
connectionTimeout: 3000,
|
|
46
|
+
requestTimeout: 30000
|
|
47
|
+
},
|
|
48
|
+
mysqldb: {
|
|
49
|
+
conn_limit: 15,
|
|
50
|
+
host: 'localhost',
|
|
51
|
+
port: 3306,
|
|
52
|
+
user: 'user',
|
|
53
|
+
pwd: 'user_pwd',
|
|
54
|
+
name: 'mqtt-aedes',
|
|
55
|
+
},
|
|
56
|
+
sync_main_tables: true,
|
|
57
|
+
projects: {
|
|
58
|
+
demo: true,
|
|
59
|
+
// Add your project names here
|
|
60
|
+
// These will become Kafka topic names when Kafka is enabled
|
|
61
|
+
},
|
|
62
|
+
demo: {
|
|
63
|
+
uidPrefix: 'uid:',
|
|
64
|
+
uidLength: 16
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Example: MQTT-only configuration (traditional mode)
|
|
69
|
+
const configMQTTOnly = {
|
|
70
|
+
...configWithKafka,
|
|
71
|
+
kafka: {
|
|
72
|
+
...configWithKafka.kafka,
|
|
73
|
+
enabled: false // Disable Kafka
|
|
74
|
+
},
|
|
75
|
+
mqtt: {
|
|
76
|
+
...configWithKafka.mqtt,
|
|
77
|
+
parseMessages: true // Keep MQTT parsing enabled
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Example: Kafka-only configuration
|
|
82
|
+
const configKafkaOnly = {
|
|
83
|
+
...configWithKafka,
|
|
84
|
+
kafka: {
|
|
85
|
+
...configWithKafka.kafka,
|
|
86
|
+
enabled: true // Enable Kafka
|
|
87
|
+
},
|
|
88
|
+
mqtt: {
|
|
89
|
+
...configWithKafka.mqtt,
|
|
90
|
+
parseMessages: false // Disable MQTT parsing
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Load projects from directories
|
|
95
|
+
var projectsPath = '../projects/';
|
|
96
|
+
var projects = [];
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
fs.readdirSync(projectsPath)
|
|
100
|
+
.filter((file) => {
|
|
101
|
+
return (file.indexOf('.') == -1);
|
|
102
|
+
})
|
|
103
|
+
.forEach((project) => {
|
|
104
|
+
if (configWithKafka.projects[project]) {
|
|
105
|
+
projects.push(project);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.log('Projects directory not found, using default projects');
|
|
110
|
+
projects = ['demo'];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runExample() {
|
|
114
|
+
try {
|
|
115
|
+
console.log('=== MQTT Devices Parser with Kafka Support Example ===\n');
|
|
116
|
+
|
|
117
|
+
// Choose which configuration to use
|
|
118
|
+
const selectedConfig = configWithKafka; // Change this to test different modes
|
|
119
|
+
|
|
120
|
+
console.log('Configuration mode:');
|
|
121
|
+
console.log('- MQTT parsing enabled:', selectedConfig.mqtt.parseMessages);
|
|
122
|
+
console.log('- Kafka consumer enabled:', selectedConfig.kafka.enabled);
|
|
123
|
+
console.log('- Projects:', projects.join(', '));
|
|
124
|
+
console.log();
|
|
125
|
+
|
|
126
|
+
if (selectedConfig.kafka.enabled) {
|
|
127
|
+
console.log('Kafka configuration:');
|
|
128
|
+
console.log('- Brokers:', selectedConfig.kafka.brokers.join(', '));
|
|
129
|
+
console.log('- Group ID:', selectedConfig.kafka.groupId);
|
|
130
|
+
console.log('- SASL enabled:', !!(selectedConfig.kafka.sasl.username && selectedConfig.kafka.sasl.password));
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Initialize the parser
|
|
135
|
+
console.log('Initializing parser...');
|
|
136
|
+
await parser.init(selectedConfig, projects);
|
|
137
|
+
|
|
138
|
+
console.log('Parser initialized successfully!');
|
|
139
|
+
console.log();
|
|
140
|
+
|
|
141
|
+
if (selectedConfig.kafka.enabled) {
|
|
142
|
+
console.log('Kafka consumer is running and listening for messages on topics:', projects.join(', '));
|
|
143
|
+
console.log('Send messages to these Kafka topics with format:');
|
|
144
|
+
console.log('- Topic: <project_name> (e.g., "demo")');
|
|
145
|
+
console.log('- Key: <device_path> (e.g., "uid:123/status")');
|
|
146
|
+
console.log('- Value: <message_payload> (e.g., "online")');
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (selectedConfig.mqtt.parseMessages) {
|
|
151
|
+
console.log('MQTT client is connected and parsing messages');
|
|
152
|
+
console.log('Subscribed to MQTT topics:', projects.map(p => p + '/#').join(', '));
|
|
153
|
+
console.log();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('Press Ctrl+C to stop...');
|
|
157
|
+
|
|
158
|
+
// Keep the process running
|
|
159
|
+
await new Promise(resolve => {
|
|
160
|
+
process.on('SIGINT', () => {
|
|
161
|
+
console.log('\nShutting down gracefully...');
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error running example:', error);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle unhandled rejections
|
|
173
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
174
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
175
|
+
if (configWithKafka.dev) {
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Run the example
|
|
181
|
+
runExample();
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ $.md5 = require('md5');
|
|
|
7
7
|
$.config = require('./config');
|
|
8
8
|
$.models = require('./models/models.js');
|
|
9
9
|
$.device = require('./src/device/device.js');
|
|
10
|
+
$.kafka_consumer = require('./src/kafka/consumer.js');
|
|
10
11
|
$.db = require('./src/db/db');
|
|
11
12
|
$.db_data = require('./src/db/data');
|
|
12
13
|
$.db_project = require('./src/db/project');
|
|
@@ -40,6 +41,12 @@ var self = module.exports = {
|
|
|
40
41
|
//auth.init();
|
|
41
42
|
await $.device.init($.config,projects);
|
|
42
43
|
|
|
44
|
+
// Initialize Kafka consumer if enabled
|
|
45
|
+
if ($.config.kafka.enabled) {
|
|
46
|
+
await $.kafka_consumer.init($.config, projects);
|
|
47
|
+
await $.kafka_consumer.start();
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
console.log("mqtt connecting..")
|
|
44
51
|
mqtt_connect();
|
|
45
52
|
|
|
@@ -107,6 +114,7 @@ var self = module.exports = {
|
|
|
107
114
|
// adds client with credentials admin@admin
|
|
108
115
|
await $.models.insertClient("admin","admin",user_id); // dashboard login
|
|
109
116
|
|
|
117
|
+
/* Add projects using dashboard !!
|
|
110
118
|
for(name of projects){
|
|
111
119
|
project = $.config[name];
|
|
112
120
|
if (project) {
|
|
@@ -115,6 +123,7 @@ var self = module.exports = {
|
|
|
115
123
|
await $.models.insertProject(projectData);
|
|
116
124
|
}
|
|
117
125
|
}
|
|
126
|
+
*/
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
// project tables
|
|
@@ -179,8 +188,11 @@ function mqtt_connect(){
|
|
|
179
188
|
});
|
|
180
189
|
|
|
181
190
|
$.mqtt_client.on("message", (topic, payload, packet) => {
|
|
182
|
-
//
|
|
183
|
-
|
|
191
|
+
// Only parse MQTT messages if enabled in configuration
|
|
192
|
+
if ($.config.mqtt.parseMessages) {
|
|
193
|
+
// payload is Buffer
|
|
194
|
+
$.device.parseMessage($.mqtt_client,topic.toString(),payload.toString(),packet.retain);
|
|
195
|
+
}
|
|
184
196
|
});
|
|
185
197
|
|
|
186
198
|
$.mqtt_client.on("reconnect",()=>{
|
package/models/models.js
CHANGED
|
@@ -253,7 +253,7 @@ var self = module.exports = {
|
|
|
253
253
|
});
|
|
254
254
|
},
|
|
255
255
|
|
|
256
|
-
|
|
256
|
+
seedLwM2MObjects: async(objects) => {
|
|
257
257
|
try {
|
|
258
258
|
const results = await Promise.all(
|
|
259
259
|
objects.map(async (object) => {
|
|
@@ -261,7 +261,7 @@ var self = module.exports = {
|
|
|
261
261
|
{
|
|
262
262
|
objectId: object.objectId,
|
|
263
263
|
name: object.name,
|
|
264
|
-
description: object
|
|
264
|
+
description: object?.description,
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
267
|
where: { objectId: object.objectId }, // Ensure the record exists based on objectId
|
|
@@ -317,7 +317,7 @@ var self = module.exports = {
|
|
|
317
317
|
* @param {import('sequelize').Sequelize} sequelize - An initialized Sequelize instance.
|
|
318
318
|
* @returns {Promise<Array<{ objectId:number, resourceId:number, created:boolean, updated:boolean }>>}
|
|
319
319
|
*/
|
|
320
|
-
|
|
320
|
+
seedLwM2MResources: async (resources) => {
|
|
321
321
|
if (!sequelize || !sequelize.models || !sequelize.models['lwm2mResources']) {
|
|
322
322
|
throw new Error("sequelize instance with model 'lwm2mResources' is required");
|
|
323
323
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
module.exports = (sequelize,DataTypes)=>{
|
|
3
|
+
return sequelize.define("mqtt", {
|
|
4
|
+
device_id: {
|
|
5
|
+
type: DataTypes.INTEGER,
|
|
6
|
+
allowNull: true
|
|
7
|
+
},
|
|
8
|
+
topic: {
|
|
9
|
+
type: DataTypes.STRING,
|
|
10
|
+
allowNull: false
|
|
11
|
+
},
|
|
12
|
+
description: {
|
|
13
|
+
type: DataTypes.JSON,
|
|
14
|
+
allowNull: false
|
|
15
|
+
},
|
|
16
|
+
defaultData: {
|
|
17
|
+
type: DataTypes.JSON,
|
|
18
|
+
allowNull: true
|
|
19
|
+
},
|
|
20
|
+
remoteData: {
|
|
21
|
+
type: DataTypes.JSON,
|
|
22
|
+
allowNull: true
|
|
23
|
+
},
|
|
24
|
+
localData: {
|
|
25
|
+
type: DataTypes.JSON,
|
|
26
|
+
allowNull: true
|
|
27
|
+
},
|
|
28
|
+
readInterval: {
|
|
29
|
+
type: DataTypes.INTEGER,
|
|
30
|
+
allowNull: true
|
|
31
|
+
},
|
|
32
|
+
template_id: {
|
|
33
|
+
type: DataTypes.INTEGER,
|
|
34
|
+
allowNull: true,
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
tableName: 'mqtt',
|
|
39
|
+
freezeTableName: true
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
module.exports = (sequelize,DataTypes)=>{
|
|
3
|
+
return sequelize.define("mqttTemplate", {
|
|
4
|
+
topic: {
|
|
5
|
+
type: DataTypes.STRING,
|
|
6
|
+
allowNull: false
|
|
7
|
+
},
|
|
8
|
+
description: {
|
|
9
|
+
type: DataTypes.JSON,
|
|
10
|
+
allowNull: false
|
|
11
|
+
},
|
|
12
|
+
defaultData: {
|
|
13
|
+
type: DataTypes.JSON,
|
|
14
|
+
allowNull: true
|
|
15
|
+
},
|
|
16
|
+
readInterval: {
|
|
17
|
+
type: DataTypes.INTEGER,
|
|
18
|
+
allowNull: true
|
|
19
|
+
},
|
|
20
|
+
template_id: {
|
|
21
|
+
type: DataTypes.INTEGER,
|
|
22
|
+
allowNull: false
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
tableName: 'mqttTemplate',
|
|
27
|
+
freezeTableName: true
|
|
28
|
+
})
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mqtt-devices-parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"async": "^3.2.4",
|
|
14
|
+
"kafkajs": "^2.2.4",
|
|
14
15
|
"lodash": "^4.17.21",
|
|
15
16
|
"md5": "^2.3.0",
|
|
16
17
|
"moment": "^2.29.4",
|
package/src/device/device.js
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const { Kafka, logLevel } = require('kafkajs');
|
|
2
|
+
|
|
3
|
+
var kafka = null;
|
|
4
|
+
var consumer = null;
|
|
5
|
+
var running = false;
|
|
6
|
+
|
|
7
|
+
var self = module.exports = {
|
|
8
|
+
|
|
9
|
+
init: async (config, projects) => {
|
|
10
|
+
|
|
11
|
+
return new Promise(async (resolve, reject) => {
|
|
12
|
+
try {
|
|
13
|
+
if (!config.kafka.enabled) {
|
|
14
|
+
console.log("Kafka consumer is disabled");
|
|
15
|
+
return resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log("Initializing Kafka consumer...");
|
|
19
|
+
|
|
20
|
+
// Build Kafka configuration
|
|
21
|
+
const kafkaConfig = {
|
|
22
|
+
clientId: config.kafka.clientId,
|
|
23
|
+
brokers: config.kafka.brokers,
|
|
24
|
+
connectionTimeout: config.kafka.connectionTimeout,
|
|
25
|
+
requestTimeout: config.kafka.requestTimeout,
|
|
26
|
+
logLevel: logLevel.INFO
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Add SASL authentication if credentials provided
|
|
30
|
+
if (config.kafka.sasl.username && config.kafka.sasl.password) {
|
|
31
|
+
kafkaConfig.sasl = {
|
|
32
|
+
mechanism: config.kafka.sasl.mechanism,
|
|
33
|
+
username: config.kafka.sasl.username,
|
|
34
|
+
password: config.kafka.sasl.password
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add SSL configuration if enabled
|
|
39
|
+
if (config.kafka.ssl) {
|
|
40
|
+
kafkaConfig.ssl = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
kafka = new Kafka(kafkaConfig);
|
|
44
|
+
|
|
45
|
+
// Create consumer with shared subscription support
|
|
46
|
+
consumer = kafka.consumer({
|
|
47
|
+
groupId: config.kafka.groupId,
|
|
48
|
+
sessionTimeout: 30000,
|
|
49
|
+
rebalanceTimeout: 60000,
|
|
50
|
+
heartbeatInterval: 3000,
|
|
51
|
+
maxBytesPerPartition: 1048576,
|
|
52
|
+
minBytes: 1,
|
|
53
|
+
maxBytes: 10485760,
|
|
54
|
+
maxWaitTimeInMs: 5000,
|
|
55
|
+
retry: {
|
|
56
|
+
initialRetryTime: 100,
|
|
57
|
+
retries: 8
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Subscribe to project topics
|
|
62
|
+
const topics = projects.map(project => project.toString());
|
|
63
|
+
|
|
64
|
+
if (topics.length === 0) {
|
|
65
|
+
console.log("No Kafka topics to subscribe to");
|
|
66
|
+
return resolve();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await consumer.subscribe({
|
|
70
|
+
topics: topics,
|
|
71
|
+
fromBeginning: false
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`Kafka consumer subscribed to topics: ${topics.join(', ')}`);
|
|
75
|
+
|
|
76
|
+
return resolve();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("Failed to initialize Kafka consumer:", error);
|
|
79
|
+
return reject(error);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
start: async () => {
|
|
85
|
+
if (!consumer || running) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
console.log("Starting Kafka consumer...");
|
|
91
|
+
running = true;
|
|
92
|
+
|
|
93
|
+
await consumer.run({
|
|
94
|
+
eachMessage: async ({ topic, partition, message }) => {
|
|
95
|
+
try {
|
|
96
|
+
let payload = message.value ? message.value.toString() : '';
|
|
97
|
+
const kafkaTopic = topic.toString();
|
|
98
|
+
|
|
99
|
+
// Format message to look like MQTT format for compatibility
|
|
100
|
+
// Kafka topic becomes the project name, message key becomes the device path
|
|
101
|
+
const messageKey = message.key ? message.key.toString() : '';
|
|
102
|
+
|
|
103
|
+
let formattedTopic;
|
|
104
|
+
if (messageKey) {
|
|
105
|
+
// If message has a key, use it as the device path: project/device_path
|
|
106
|
+
formattedTopic = `${kafkaTopic}/${messageKey}`;
|
|
107
|
+
} else {
|
|
108
|
+
// If no key, try to extract from headers or use a default format
|
|
109
|
+
const headers = message.headers || {};
|
|
110
|
+
const devicePath = headers.device_path ? headers.device_path.toString() : 'unknown';
|
|
111
|
+
formattedTopic = `${kafkaTopic}/${devicePath}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try{
|
|
115
|
+
payload = JSON.parse(payload);
|
|
116
|
+
}catch(error){}
|
|
117
|
+
|
|
118
|
+
// Call the device parser with formatted topic
|
|
119
|
+
await $.device.parseMessage(payload.client.id, formattedTopic, payload.mqtt.payload, payload.mqtt.retain);
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('[Kafka] Error processing message:', {
|
|
123
|
+
topic: topic.toString(),
|
|
124
|
+
partition,
|
|
125
|
+
offset: message.offset ? message.offset.toString() : 'unknown',
|
|
126
|
+
error: error.message
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log("Kafka consumer started successfully");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("Failed to start Kafka consumer:", error);
|
|
135
|
+
running = false;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
stop: async () => {
|
|
141
|
+
if (!consumer || !running) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
console.log("Stopping Kafka consumer...");
|
|
147
|
+
running = false;
|
|
148
|
+
await consumer.stop();
|
|
149
|
+
await consumer.disconnect();
|
|
150
|
+
console.log("Kafka consumer stopped successfully");
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error("Error stopping Kafka consumer:", error);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
isRunning: () => {
|
|
157
|
+
return running;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Graceful shutdown handling
|
|
162
|
+
process.on('SIGINT', async () => {
|
|
163
|
+
if (running) {
|
|
164
|
+
console.log('Received SIGINT, gracefully shutting down Kafka consumer...');
|
|
165
|
+
await self.stop();
|
|
166
|
+
}
|
|
167
|
+
process.exit(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
process.on('SIGTERM', async () => {
|
|
171
|
+
if (running) {
|
|
172
|
+
console.log('Received SIGTERM, gracefully shutting down Kafka consumer...');
|
|
173
|
+
await self.stop();
|
|
174
|
+
}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
$ = {};
|
|
4
|
+
var path = "/../..";
|
|
5
|
+
$.config = require(__dirname + path + '/config');
|
|
6
|
+
|
|
7
|
+
// Mock device module to prevent actual parsing
|
|
8
|
+
$.device = {
|
|
9
|
+
parseMessage: async (client, topic, payload, retain) => {
|
|
10
|
+
console.log('Mock parseMessage called:', { topic, payload, retain });
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function testConfigurationValues() {
|
|
16
|
+
console.log('\n=== Testing Configuration Values ===');
|
|
17
|
+
|
|
18
|
+
// Test default values
|
|
19
|
+
console.log('Default MQTT parseMessages:', $.config.mqtt.parseMessages);
|
|
20
|
+
console.log('Default Kafka enabled:', $.config.kafka.enabled);
|
|
21
|
+
console.log('Default Kafka brokers:', $.config.kafka.brokers);
|
|
22
|
+
console.log('Default Kafka groupId:', $.config.kafka.groupId);
|
|
23
|
+
console.log('Default Kafka SASL mechanism:', $.config.kafka.sasl.mechanism);
|
|
24
|
+
|
|
25
|
+
// Test with environment variables
|
|
26
|
+
process.env.MQTT_PARSE_MESSAGES = 'false';
|
|
27
|
+
process.env.KAFKA_ENABLED = 'true';
|
|
28
|
+
process.env.KAFKA_BROKERS = 'broker1:9092,broker2:9092';
|
|
29
|
+
process.env.KAFKA_GROUP_ID = 'test-group';
|
|
30
|
+
process.env.KAFKA_SASL_MECHANISM = 'scram-sha-256';
|
|
31
|
+
process.env.KAFKA_SASL_USERNAME = 'testuser';
|
|
32
|
+
process.env.KAFKA_SASL_PASSWORD = 'testpass';
|
|
33
|
+
|
|
34
|
+
// Reload config to pick up env variables
|
|
35
|
+
delete require.cache[require.resolve(__dirname + path + '/config')];
|
|
36
|
+
const newConfig = require(__dirname + path + '/config');
|
|
37
|
+
|
|
38
|
+
console.log('\nWith environment variables:');
|
|
39
|
+
console.log('MQTT parseMessages:', newConfig.mqtt.parseMessages);
|
|
40
|
+
console.log('Kafka enabled:', newConfig.kafka.enabled);
|
|
41
|
+
console.log('Kafka brokers:', newConfig.kafka.brokers);
|
|
42
|
+
console.log('Kafka groupId:', newConfig.kafka.groupId);
|
|
43
|
+
console.log('Kafka SASL mechanism:', newConfig.kafka.sasl.mechanism);
|
|
44
|
+
console.log('Kafka SASL username:', newConfig.kafka.sasl.username);
|
|
45
|
+
console.log('Kafka SASL password:', newConfig.kafka.sasl.password ? '[SET]' : '[NOT SET]');
|
|
46
|
+
|
|
47
|
+
console.log('✓ Configuration values test passed!');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function testKafkaConsumerModule() {
|
|
51
|
+
console.log('\n=== Testing Kafka Consumer Module Structure ===');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const kafkaConsumer = require(__dirname + path + '/src/kafka/consumer.js');
|
|
55
|
+
|
|
56
|
+
// Test module exports
|
|
57
|
+
console.log('Module has init method:', typeof kafkaConsumer.init === 'function');
|
|
58
|
+
console.log('Module has start method:', typeof kafkaConsumer.start === 'function');
|
|
59
|
+
console.log('Module has stop method:', typeof kafkaConsumer.stop === 'function');
|
|
60
|
+
console.log('Module has isRunning method:', typeof kafkaConsumer.isRunning === 'function');
|
|
61
|
+
|
|
62
|
+
// Test initial state
|
|
63
|
+
console.log('Initial running state:', kafkaConsumer.isRunning());
|
|
64
|
+
|
|
65
|
+
// Test with disabled Kafka
|
|
66
|
+
const configDisabled = { kafka: { enabled: false } };
|
|
67
|
+
await kafkaConsumer.init(configDisabled, ['test']);
|
|
68
|
+
console.log('Init with disabled config succeeded');
|
|
69
|
+
|
|
70
|
+
console.log('✓ Kafka consumer module structure test passed!');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('✗ Kafka consumer module test failed:', error.message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function testMQTTMessageParsing() {
|
|
77
|
+
console.log('\n=== Testing MQTT Message Parsing Logic ===');
|
|
78
|
+
|
|
79
|
+
// Test the logic that would be used in index.js
|
|
80
|
+
const testConfigs = [
|
|
81
|
+
{ mqtt: { parseMessages: true } },
|
|
82
|
+
{ mqtt: { parseMessages: false } }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const config of testConfigs) {
|
|
86
|
+
console.log(`\nTesting with parseMessages: ${config.mqtt.parseMessages}`);
|
|
87
|
+
|
|
88
|
+
// Simulate the MQTT message handler logic
|
|
89
|
+
const mockTopic = "test/uid:123/status";
|
|
90
|
+
const mockPayload = "online";
|
|
91
|
+
const mockPacket = { retain: false };
|
|
92
|
+
|
|
93
|
+
if (config.mqtt.parseMessages) {
|
|
94
|
+
console.log('Would call device.parseMessage');
|
|
95
|
+
await $.device.parseMessage(null, mockTopic, mockPayload, mockPacket.retain);
|
|
96
|
+
} else {
|
|
97
|
+
console.log('Would skip device.parseMessage');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('✓ MQTT message parsing logic test passed!');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runTests() {
|
|
105
|
+
console.log('Starting Kafka Integration Tests...');
|
|
106
|
+
|
|
107
|
+
await testConfigurationValues();
|
|
108
|
+
await testKafkaConsumerModule();
|
|
109
|
+
await testMQTTMessageParsing();
|
|
110
|
+
|
|
111
|
+
console.log('\n=== All Tests Completed Successfully ===');
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
runTests().catch(error => {
|
|
116
|
+
console.error('Test suite failed:', error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
$ = {};
|
|
4
|
+
var path = "/../..";
|
|
5
|
+
$.config = require(__dirname + path + '/config');
|
|
6
|
+
$.kafka_consumer = require(__dirname + path + '/src/kafka/consumer.js');
|
|
7
|
+
$.device = {
|
|
8
|
+
parseMessage: async (client, topic, payload, retain) => {
|
|
9
|
+
console.log('Mock parseMessage called:', { topic, payload, retain });
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function testKafkaConsumerInit() {
|
|
14
|
+
console.log('\n=== Testing Kafka Consumer Initialization ===');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Test with Kafka disabled
|
|
18
|
+
console.log('1. Testing with Kafka disabled...');
|
|
19
|
+
const configDisabled = { ...$.config, kafka: { ...$.config.kafka, enabled: false } };
|
|
20
|
+
await $.kafka_consumer.init(configDisabled, ['test-project']);
|
|
21
|
+
console.log('✓ Kafka consumer init with disabled config passed');
|
|
22
|
+
|
|
23
|
+
// Test with Kafka enabled but no authentication
|
|
24
|
+
console.log('2. Testing with Kafka enabled (no auth)...');
|
|
25
|
+
const configEnabled = {
|
|
26
|
+
...$.config,
|
|
27
|
+
kafka: {
|
|
28
|
+
...$.config.kafka,
|
|
29
|
+
enabled: true,
|
|
30
|
+
sasl: { username: '', password: '' }
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
await $.kafka_consumer.init(configEnabled, ['test-project']);
|
|
34
|
+
console.log('✓ Kafka consumer init with enabled config passed');
|
|
35
|
+
|
|
36
|
+
// Test isRunning method
|
|
37
|
+
console.log('3. Testing isRunning method...');
|
|
38
|
+
console.log('Is running:', $.kafka_consumer.isRunning());
|
|
39
|
+
console.log('✓ isRunning method works');
|
|
40
|
+
|
|
41
|
+
console.log('\n✓ All Kafka consumer tests passed!');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('✗ Kafka consumer test failed:', error.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function testConfiguration() {
|
|
48
|
+
console.log('\n=== Testing Configuration ===');
|
|
49
|
+
|
|
50
|
+
console.log('MQTT parseMessages:', $.config.mqtt.parseMessages);
|
|
51
|
+
console.log('Kafka enabled:', $.config.kafka.enabled);
|
|
52
|
+
console.log('Kafka brokers:', $.config.kafka.brokers);
|
|
53
|
+
console.log('Kafka groupId:', $.config.kafka.groupId);
|
|
54
|
+
console.log('Kafka SASL mechanism:', $.config.kafka.sasl.mechanism);
|
|
55
|
+
|
|
56
|
+
console.log('✓ Configuration test passed!');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function runTests() {
|
|
60
|
+
console.log('Starting Kafka Consumer Tests...');
|
|
61
|
+
|
|
62
|
+
await testConfiguration();
|
|
63
|
+
await testKafkaConsumerInit();
|
|
64
|
+
|
|
65
|
+
console.log('\n=== All Tests Completed ===');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
runTests().catch(error => {
|
|
70
|
+
console.error('Test suite failed:', error);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|