mqtt-devices-parser 1.0.20 → 1.0.22
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 +16 -0
- package/Readme.md +37 -8
- package/config/index.js +5 -2
- package/example/kafka-example.js +1 -6
- package/index.js +27 -12
- package/models/logs_sensor.models.js +4 -0
- package/package.json +2 -2
- package/src/db/sensor.js +1 -0
- package/src/device/device.js +327 -280
- package/src/kafka/consumer.js +41 -19
- package/src/unitTest/kafka-integration.test.js +4 -6
- package/src/unitTest/kafka.test.js +0 -1
package/Changelog.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.22
|
|
4
|
+
Adds SSL and SASL support for kafka
|
|
5
|
+
mqtt.parseMessages removed
|
|
6
|
+
src/kafka/consumer: adds random number to kafka client Id
|
|
7
|
+
Parse mqtt lwm2m (#6)
|
|
8
|
+
* in progress: splits parsing messages base on protocol
|
|
9
|
+
* supports local unix timestamp, fix sensor data parse
|
|
10
|
+
models/logs_sensor.models: add localUnixTs
|
|
11
|
+
src/devices/device: fix object parse
|
|
12
|
+
* fix: mqtt parser
|
|
13
|
+
Re-structs parser
|
|
14
|
+
|
|
15
|
+
## 1.0.21
|
|
16
|
+
src/kafka/consumer: fixes parseMessage call
|
|
17
|
+
improves: logs
|
|
18
|
+
|
|
3
19
|
## 1.0.20
|
|
4
20
|
Adds Kafka consumer support with optional MQTT message parsing (#5)
|
|
5
21
|
supports tables for mqtt templates
|
package/Readme.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
- mysql8.0
|
|
28
28
|
- MQTT
|
|
29
|
-
- **NEW**:
|
|
29
|
+
- **NEW**: Confluentic Kafka (optional)
|
|
30
30
|
|
|
31
31
|
## Front end
|
|
32
32
|
- Node service running the following [project](https://github.com/zimbora/mgmt-iot-web)
|
|
@@ -58,7 +58,7 @@ The module now supports consuming messages from Kafka topics as an alternative o
|
|
|
58
58
|
- `KAFKA_SASL_PASSWORD` - SASL password (default: empty)
|
|
59
59
|
|
|
60
60
|
**Security:**
|
|
61
|
-
- `
|
|
61
|
+
- `KAFKA_SSL_ENABLED` - Set to `'true'` to enable SSL (default: `false`)
|
|
62
62
|
|
|
63
63
|
**Connection Settings:**
|
|
64
64
|
- `KAFKA_CONNECTION_TIMEOUT` - Connection timeout in ms (default: `3000`)
|
|
@@ -69,23 +69,47 @@ The module now supports consuming messages from Kafka topics as an alternative o
|
|
|
69
69
|
|
|
70
70
|
#### Usage Examples
|
|
71
71
|
|
|
72
|
-
**Example 1:
|
|
72
|
+
**Example 1: Use Kafka with SSL and SCRAM-SHA-256 authentication - Most Secure**
|
|
73
73
|
```bash
|
|
74
|
-
export
|
|
74
|
+
export KAFKA_SSL_ENABLED=true
|
|
75
|
+
export KAFKA_SSL_CERTFILE='./certs/ca.cert'
|
|
76
|
+
export KAFKA_SSL_REJECT_UNAUTHORIZED=true
|
|
77
|
+
export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
78
|
+
export KAFKA_SASL_USERNAME=myuser
|
|
79
|
+
export KAFKA_SASL_PASSWORD=mypassword
|
|
80
|
+
export KAFKA_SASL_MECHANISM=scram-sha-256
|
|
81
|
+
```
|
|
82
|
+
**Example 2: Use Kafka with SSL No trust and SCRAM-SHA-256 authentication.
|
|
83
|
+
Prone to MITM attacks**
|
|
84
|
+
```bash
|
|
85
|
+
export KAFKA_SSL_ENABLED=true
|
|
86
|
+
export KAFKA_SSL_CERTFILE=''
|
|
87
|
+
export KAFKA_SSL_REJECT_UNAUTHORIZED=false
|
|
88
|
+
export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
89
|
+
export KAFKA_SASL_USERNAME=myuser
|
|
90
|
+
export KAFKA_SASL_PASSWORD=mypassword
|
|
91
|
+
export KAFKA_SASL_MECHANISM=scram-sha-256
|
|
92
|
+
```
|
|
93
|
+
**Example 3: Use Kafka without SSL and SCRAM-SHA-256 authentication. Basic authentication with no encryption. Credentials are not shared**
|
|
94
|
+
```bash
|
|
95
|
+
export KAFKA_SSL_ENABLED=false
|
|
96
|
+
export KAFKA_SSL_CERTFILE=''
|
|
97
|
+
export KAFKA_SSL_REJECT_UNAUTHORIZED=true
|
|
75
98
|
export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
76
99
|
export KAFKA_SASL_USERNAME=myuser
|
|
77
100
|
export KAFKA_SASL_PASSWORD=mypassword
|
|
78
101
|
export KAFKA_SASL_MECHANISM=scram-sha-256
|
|
79
102
|
```
|
|
80
103
|
|
|
81
|
-
**Example
|
|
104
|
+
**Example 4: Use KAFKA without authentication. Not secure at all**
|
|
82
105
|
```bash
|
|
83
106
|
export KAFKA_ENABLED=true
|
|
84
|
-
export
|
|
85
|
-
export
|
|
107
|
+
export KAFKA_SSL_ENABLED=false
|
|
108
|
+
export KAFKA_SASL_USERNAME=''
|
|
109
|
+
export KAFKA_SASL_PASSWORD=''
|
|
86
110
|
```
|
|
87
111
|
|
|
88
|
-
**Example
|
|
112
|
+
**Example 5: KAFKA Multi-process deployment**
|
|
89
113
|
```bash
|
|
90
114
|
# Start multiple instances with the same group ID for load balancing
|
|
91
115
|
export KAFKA_ENABLED=true
|
|
@@ -94,6 +118,11 @@ export KAFKA_BROKERS=kafka1:9092,kafka2:9092
|
|
|
94
118
|
# Each process will consume different partitions automatically
|
|
95
119
|
```
|
|
96
120
|
|
|
121
|
+
**Example 6: Use MQTT only (disable KAFKA parsing)**
|
|
122
|
+
```bash
|
|
123
|
+
export KAFKA_ENABLED=false
|
|
124
|
+
```
|
|
125
|
+
|
|
97
126
|
#### Kafka Topic Structure
|
|
98
127
|
|
|
99
128
|
Kafka topics should correspond to project names configured in your `projects` array. Messages are expected to have:
|
package/config/index.js
CHANGED
|
@@ -14,19 +14,22 @@ module.exports = {
|
|
|
14
14
|
pwd:process.env.MQTT_PWD || 'admin',
|
|
15
15
|
client:process.env.MQTT_CLIENT || 'mqtt-devices-parser',
|
|
16
16
|
logs_path: process.env.MQTT_LOGS || 'uServices',
|
|
17
|
-
parseMessages: process.env.MQTT_PARSE_MESSAGES !== 'false' // Default to true for backward compatibility
|
|
18
17
|
},
|
|
19
18
|
kafka: {
|
|
20
19
|
enabled: process.env.KAFKA_ENABLED === 'true' ? true : true,
|
|
21
20
|
brokers: (process.env.KAFKA_BROKERS || 'localhost:9092').split(','),
|
|
22
21
|
groupId: process.env.KAFKA_GROUP_ID || 'mqtt-devices-parser-group',
|
|
23
22
|
clientId: process.env.KAFKA_CLIENT_ID || 'mqtt-devices-parser',
|
|
23
|
+
ssl: {
|
|
24
|
+
enabled : process.env.KAFKA_SSL_ENABLED || false,
|
|
25
|
+
file: process.env.KAFKA_SSL_CERTFILE || './certs/kafka.crt',
|
|
26
|
+
rejectUnauthorized: process.env.KAFKA_SSL_REJECT_UNAUTHORIZED || true, // set to false to accept unstrusted certs
|
|
27
|
+
},
|
|
24
28
|
sasl: {
|
|
25
29
|
mechanism: process.env.KAFKA_SASL_MECHANISM || 'plain', // plain, scram-sha-256, scram-sha-512
|
|
26
30
|
username: process.env.KAFKA_SASL_USERNAME || '',
|
|
27
31
|
password: process.env.KAFKA_SASL_PASSWORD || ''
|
|
28
32
|
},
|
|
29
|
-
ssl: process.env.KAFKA_SSL === 'true' ? true : false,
|
|
30
33
|
connectionTimeout: parseInt(process.env.KAFKA_CONNECTION_TIMEOUT) || 3000,
|
|
31
34
|
requestTimeout: parseInt(process.env.KAFKA_REQUEST_TIMEOUT) || 30000
|
|
32
35
|
},
|
package/example/kafka-example.js
CHANGED
|
@@ -28,8 +28,6 @@ const configWithKafka = {
|
|
|
28
28
|
user: 'admin',
|
|
29
29
|
pwd: 'admin',
|
|
30
30
|
client: 'mqtt-devices-parser-example',
|
|
31
|
-
logs_path: 'uServices',
|
|
32
|
-
parseMessages: true // Set to false to disable MQTT message parsing
|
|
33
31
|
},
|
|
34
32
|
kafka: {
|
|
35
33
|
enabled: true, // Enable Kafka consumer
|
|
@@ -74,7 +72,6 @@ const configMQTTOnly = {
|
|
|
74
72
|
},
|
|
75
73
|
mqtt: {
|
|
76
74
|
...configWithKafka.mqtt,
|
|
77
|
-
parseMessages: true // Keep MQTT parsing enabled
|
|
78
75
|
}
|
|
79
76
|
};
|
|
80
77
|
|
|
@@ -87,7 +84,6 @@ const configKafkaOnly = {
|
|
|
87
84
|
},
|
|
88
85
|
mqtt: {
|
|
89
86
|
...configWithKafka.mqtt,
|
|
90
|
-
parseMessages: false // Disable MQTT parsing
|
|
91
87
|
}
|
|
92
88
|
};
|
|
93
89
|
|
|
@@ -118,7 +114,6 @@ async function runExample() {
|
|
|
118
114
|
const selectedConfig = configWithKafka; // Change this to test different modes
|
|
119
115
|
|
|
120
116
|
console.log('Configuration mode:');
|
|
121
|
-
console.log('- MQTT parsing enabled:', selectedConfig.mqtt.parseMessages);
|
|
122
117
|
console.log('- Kafka consumer enabled:', selectedConfig.kafka.enabled);
|
|
123
118
|
console.log('- Projects:', projects.join(', '));
|
|
124
119
|
console.log();
|
|
@@ -147,7 +142,7 @@ async function runExample() {
|
|
|
147
142
|
console.log();
|
|
148
143
|
}
|
|
149
144
|
|
|
150
|
-
if (selectedConfig.
|
|
145
|
+
if (!selectedConfig.kafka.enabled) {
|
|
151
146
|
console.log('MQTT client is connected and parsing messages');
|
|
152
147
|
console.log('Subscribed to MQTT topics:', projects.map(p => p + '/#').join(', '));
|
|
153
148
|
console.log();
|
package/index.js
CHANGED
|
@@ -47,7 +47,6 @@ var self = module.exports = {
|
|
|
47
47
|
await $.kafka_consumer.start();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
console.log("mqtt connecting..")
|
|
51
50
|
mqtt_connect();
|
|
52
51
|
|
|
53
52
|
setInterval(async ()=>{
|
|
@@ -133,6 +132,8 @@ var self = module.exports = {
|
|
|
133
132
|
|
|
134
133
|
function mqtt_connect(){
|
|
135
134
|
|
|
135
|
+
console.log(`[MQTT] connecting to ${$.config.mqtt.host}:${$.config.mqtt.port}..`)
|
|
136
|
+
|
|
136
137
|
const mqtt_prefix = $.config.mqtt.logs_path+"/"+$.config.mqtt.client;
|
|
137
138
|
let checkFota = null;
|
|
138
139
|
$.mqtt_client = mqtt.connect({
|
|
@@ -165,11 +166,21 @@ function mqtt_connect(){
|
|
|
165
166
|
$.mqtt_client.publish(mqtt_prefix+"/"+project,"deactive",{qos:2,retain:true});
|
|
166
167
|
};
|
|
167
168
|
})
|
|
168
|
-
console.log(`MQTT connected
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
console.log(`[MQTT] connected`);
|
|
170
|
+
|
|
171
|
+
if (!$.config.kafka.enabled) {
|
|
172
|
+
projects.map( project=>{
|
|
173
|
+
console.log("[MQTT] subscribing project:",project);
|
|
174
|
+
$.mqtt_client.subscribe(project+"/#", (err) => {
|
|
175
|
+
if(err){
|
|
176
|
+
console.log("[MQTT] error");
|
|
177
|
+
console.err(err);
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
console.log("[MQTT] subscribed to project:",project);
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|
|
173
184
|
|
|
174
185
|
// Update 'dev' devices at each 5 min
|
|
175
186
|
checkFota = setInterval(async ()=>{
|
|
@@ -189,32 +200,36 @@ function mqtt_connect(){
|
|
|
189
200
|
|
|
190
201
|
$.mqtt_client.on("message", (topic, payload, packet) => {
|
|
191
202
|
// Only parse MQTT messages if enabled in configuration
|
|
192
|
-
if (
|
|
203
|
+
if (!$.config.kafka.enabled) {
|
|
204
|
+
|
|
205
|
+
console.log(`[MQTT] rx: ${topic.toString()}`);
|
|
193
206
|
// payload is Buffer
|
|
194
207
|
$.device.parseMessage($.mqtt_client,topic.toString(),payload.toString(),packet.retain);
|
|
195
208
|
}
|
|
196
209
|
});
|
|
197
210
|
|
|
198
211
|
$.mqtt_client.on("reconnect",()=>{
|
|
199
|
-
console.log("
|
|
212
|
+
console.log("[MQTT] reconnected")
|
|
200
213
|
});
|
|
201
214
|
|
|
202
215
|
$.mqtt_client.on("close",()=>{
|
|
203
|
-
console.log("
|
|
216
|
+
console.log("[MQTT] closed")
|
|
204
217
|
checkFota = null;
|
|
205
218
|
});
|
|
206
219
|
|
|
207
220
|
$.mqtt_client.on("offline",()=>{
|
|
208
|
-
console.log("offline")
|
|
221
|
+
console.log("[MQTT] offline")
|
|
209
222
|
checkFota = null;
|
|
210
223
|
});
|
|
211
224
|
|
|
212
225
|
$.mqtt_client.on("disconnect",(packet)=>{
|
|
213
|
-
console.log(
|
|
226
|
+
console.log(`[MQTT] disconnected`);
|
|
227
|
+
console.log(packet);
|
|
214
228
|
checkFota = null;
|
|
215
229
|
})
|
|
216
230
|
|
|
217
231
|
$.mqtt_client.on("error",(error)=>{
|
|
218
|
-
console.log(error)
|
|
232
|
+
console.log("[MQTT] error");
|
|
233
|
+
console.err(error)
|
|
219
234
|
})
|
|
220
235
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mqtt-devices-parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
"path": "^0.12.7",
|
|
22
22
|
"semver": "^7.3.8",
|
|
23
23
|
"sequelize": "^6.32.1",
|
|
24
|
-
"websocket-stream": "^5.
|
|
24
|
+
"websocket-stream": "^5.3.0"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/db/sensor.js
CHANGED
|
@@ -98,6 +98,7 @@ var self = module.exports = {
|
|
|
98
98
|
value : payload?.value,
|
|
99
99
|
error : payload?.error,
|
|
100
100
|
obj : payload?.object,
|
|
101
|
+
localUnixTs : payload?.timestamp, // local timestamp
|
|
101
102
|
createdAt : moment().utc().format('YYYY-MM-DD HH:mm:ss'),
|
|
102
103
|
updatedAt : moment().utc().format('YYYY-MM-DD HH:mm:ss')
|
|
103
104
|
}
|
package/src/device/device.js
CHANGED
|
@@ -35,175 +35,49 @@ var self = module.exports = {
|
|
|
35
35
|
|
|
36
36
|
parseMessage : async (client,topic,payload,retain)=>{
|
|
37
37
|
|
|
38
|
+
let topic_bck = topic;
|
|
39
|
+
let project_name = null;
|
|
40
|
+
let project = null;
|
|
41
|
+
let project_id = null;
|
|
42
|
+
let action = null;
|
|
43
|
+
let uid = null;
|
|
44
|
+
let device = null;
|
|
45
|
+
|
|
38
46
|
try{
|
|
39
47
|
payload = JSON.parse(payload)
|
|
40
48
|
}catch(err){};
|
|
41
49
|
|
|
42
|
-
let topic_bck = topic;
|
|
43
|
-
|
|
44
|
-
//let index = topic.indexOf(MACRO_UID_PREFIX);
|
|
45
50
|
// --- project ---
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return;
|
|
49
|
-
|
|
50
|
-
let project_name = topic.substring(0,index);
|
|
51
|
-
let project = await $.db_project.getByName(project_name);
|
|
52
|
-
|
|
51
|
+
project_name = getFirstWord(topic);
|
|
52
|
+
project = await $.db_project.getByName(project_name);
|
|
53
53
|
if(project == null)
|
|
54
54
|
return;
|
|
55
|
-
|
|
56
|
-
let project_id = project?.id;
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
topic = topic
|
|
60
|
-
index = topic.indexOf("/");
|
|
61
|
-
if(index == -1)
|
|
62
|
-
return;
|
|
56
|
+
project_id = project?.id;
|
|
57
|
+
topic = getWordAfterSlash(topic)
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
topic
|
|
59
|
+
// check if is from lwm2m gw
|
|
60
|
+
if(topic.startsWith("responses") || topic.startsWith("requests")){
|
|
61
|
+
action = getFirstWord(topic);
|
|
62
|
+
topic = getWordAfterSlash(topic);
|
|
63
|
+
}
|
|
66
64
|
|
|
65
|
+
// --- uid ---
|
|
66
|
+
uid = getFirstWord(topic);
|
|
67
67
|
// check if topic corresponds to a device
|
|
68
|
-
if(uid.startsWith(project.uidPrefix)
|
|
69
|
-
|
|
70
|
-
let device = await $.db_device.get(uid);
|
|
71
|
-
|
|
72
|
-
// Insert device if not exists on db
|
|
73
|
-
if(device == null){
|
|
74
|
-
let obj = {
|
|
75
|
-
uid : uid,
|
|
76
|
-
accept_release : "prod",
|
|
77
|
-
}
|
|
78
|
-
$.db_device.insert(obj)
|
|
79
|
-
.then(async()=>{
|
|
80
|
-
device = await $.db_device.get(uid);
|
|
81
|
-
})
|
|
82
|
-
.catch( (err) => {});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// update project id if needed
|
|
86
|
-
if(project_id != null && project_id != device?.project_id){
|
|
87
|
-
$.db_device.update(device.id,"project_id",project_id);
|
|
88
|
-
$.db_device.addLog(device.id,"project_id",project_id);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
switch (topic) {
|
|
92
|
-
case "status":
|
|
93
|
-
if (payload === "online" || payload === "offline") {
|
|
94
|
-
await $.db_device.update(device.id, "status", payload);
|
|
95
|
-
$.db_device.addLog(device.id,"status",payload);
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
case "model":
|
|
99
|
-
let res = await $.db_model.getByName(payload);
|
|
100
|
-
let model_id = res?.id;
|
|
101
|
-
if (model_id != null && device?.tech != payload) {
|
|
102
|
-
await $.db_device.update(device.id, "model_id", model_id);
|
|
103
|
-
$.db_device.addLog(device.id,"model_id",model_id);
|
|
104
|
-
}
|
|
105
|
-
break;
|
|
106
|
-
case "tech":
|
|
107
|
-
if (device?.tech && payload != device.tech) {
|
|
108
|
-
await $.db_device.update(device.id, "tech", payload);
|
|
109
|
-
$.db_device.addLog(device.id,"tech",payload);
|
|
110
|
-
}
|
|
111
|
-
break;
|
|
112
|
-
case "version":
|
|
113
|
-
if (device?.version && payload != device.version) {
|
|
114
|
-
$.db_device.addLog(device.id,"version",payload);
|
|
115
|
-
await $.db_device.update(device.id, "version", payload);
|
|
116
|
-
await self.handleFotaSuccess(device.id);
|
|
117
|
-
}
|
|
118
|
-
break;
|
|
119
|
-
case "app_version":
|
|
120
|
-
if (device?.app_version && payload != device.app_version) {
|
|
121
|
-
$.db_device.addLog(device.id,"app_version",payload);
|
|
122
|
-
await $.db_device.update(device.id, "app_version", payload);
|
|
123
|
-
await self.handleFotaSuccess(device.id);
|
|
124
|
-
}
|
|
125
|
-
break;
|
|
126
|
-
case "fw/fota/update/status":
|
|
127
|
-
await self.handleFotaError(device.id, payload);
|
|
128
|
-
break;
|
|
129
|
-
// Optional: default case if needed
|
|
130
|
-
default:
|
|
131
|
-
// handle other topics or do nothing
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if(topic.startsWith("settings/") && topic.endsWith("/set") && payload != "" ){
|
|
136
|
-
// update local settings
|
|
137
|
-
let index = topic.indexOf("settings/");
|
|
138
|
-
topic = topic.substring(index+9);
|
|
139
|
-
self.updateLocalSettings(device,topic,payload);
|
|
140
|
-
}else if(topic.startsWith("settings/") && !topic.endsWith("/set") && payload != "" ){
|
|
141
|
-
// store remote device settings
|
|
142
|
-
let index = topic.indexOf("settings/");
|
|
143
|
-
topic = topic.substring(index+9);
|
|
144
|
-
self.updateRemoteSettings(device,topic,payload);
|
|
145
|
-
}else if(topic == "fw"){
|
|
146
|
-
try{
|
|
147
|
-
payload = JSON.parse(payload);
|
|
148
|
-
}catch(error){}
|
|
149
|
-
if(typeof payload === 'object' && payload !== null){
|
|
150
|
-
try{
|
|
151
|
-
$.db_data.updateJson("fw",device.id,payload);
|
|
152
|
-
$.db_data.addJsonLog("logs_fw",device.id,payload);
|
|
153
|
-
}catch(error){
|
|
154
|
-
console.error(error)
|
|
155
|
-
}
|
|
156
|
-
}else if(typeof payload !== 'object' && payload !== null){
|
|
157
|
-
// change it to topic.startsWith("fw")
|
|
158
|
-
try{
|
|
159
|
-
const column = getWordAfterLastSlash(topic);
|
|
160
|
-
$.db_data.update("fw",device.id,column,payload);
|
|
161
|
-
$.db_data.addLog("logs_fw",device.id,column,payload);
|
|
162
|
-
}catch(error){
|
|
163
|
-
console.error(error)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}else if(topic.startsWith("sensor/")){
|
|
167
|
-
//updateSensor(device,topic,payload)
|
|
168
|
-
index = topic.indexOf("/");
|
|
169
|
-
if(index != -1){
|
|
170
|
-
const ref = topic.substring(index+1);
|
|
171
|
-
let res = await $.db_device.getSensorByRef(device.id,ref)
|
|
172
|
-
if(res == null)
|
|
173
|
-
res = await $.db_model.getSensorByRef(device.model_id,ref)
|
|
174
|
-
if(res != null){
|
|
175
|
-
object = null;
|
|
176
|
-
value = null;
|
|
177
|
-
error = null;
|
|
178
|
-
try{
|
|
179
|
-
object = JSON.parse(payload);
|
|
180
|
-
}catch(err){
|
|
181
|
-
console.log(err);
|
|
182
|
-
}
|
|
183
|
-
if (typeof object === 'object' && object !== null) {
|
|
184
|
-
value = object?.value;
|
|
185
|
-
error = object?.error;
|
|
186
|
-
}else{
|
|
187
|
-
value = payload;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if(value || error)
|
|
191
|
-
object = null;
|
|
68
|
+
if(!uid.startsWith(project.uidPrefix) || uid.length > project?.uidLength)
|
|
69
|
+
return;
|
|
192
70
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
error
|
|
197
|
-
}
|
|
71
|
+
device = await $.db_device.get(uid);
|
|
72
|
+
if(!device)
|
|
73
|
+
return;
|
|
198
74
|
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
75
|
+
topic = getWordAfterSlash(topic);
|
|
203
76
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
77
|
+
if(device.protocol.toLowerCase() === "lwm2m"){
|
|
78
|
+
parseLwm2mMessage(client, project_name, device, topic, payload, action);
|
|
79
|
+
}else if(device.protocol.toLowerCase() === "mqtt"){
|
|
80
|
+
parseMqttMessage(client, project_name, device, topic, payload, retain);
|
|
207
81
|
}
|
|
208
82
|
|
|
209
83
|
},
|
|
@@ -226,118 +100,6 @@ var self = module.exports = {
|
|
|
226
100
|
}
|
|
227
101
|
},
|
|
228
102
|
|
|
229
|
-
updateLocalSettings: async (device, topic, payload) => {
|
|
230
|
-
|
|
231
|
-
$.db_device.addLog(device.id,"local_settings",JSON.stringify(payload));
|
|
232
|
-
|
|
233
|
-
let route = topic.split("/");
|
|
234
|
-
|
|
235
|
-
if (route == null || route.length == 0) {
|
|
236
|
-
console.warn("updateLocalSettings: topic invalid:", topic);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Retrieve existing settings
|
|
241
|
-
let settings = await $.db_device.getLocalSettings(device.id);
|
|
242
|
-
|
|
243
|
-
if (!settings || typeof settings !== 'object') {
|
|
244
|
-
settings = {};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let obj = settings;
|
|
248
|
-
|
|
249
|
-
// Traverse route parts to reach the target object
|
|
250
|
-
route.slice(0, -1).forEach(property => {
|
|
251
|
-
if (
|
|
252
|
-
!Object.prototype.hasOwnProperty.call(obj, property) ||
|
|
253
|
-
(obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
|
|
254
|
-
obj[property] === null
|
|
255
|
-
) {
|
|
256
|
-
obj[property] = {}; // Create nested object if missing or not an object
|
|
257
|
-
}
|
|
258
|
-
obj = obj[property];
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Parse the payload JSON
|
|
262
|
-
let data = {};
|
|
263
|
-
try {
|
|
264
|
-
data = JSON.parse(payload);
|
|
265
|
-
} catch (error) {
|
|
266
|
-
console.error("Failed to parse payload JSON:", error);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Check if the data is a plain object, then merge
|
|
271
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
272
|
-
Object.assign(obj, data);
|
|
273
|
-
} else {
|
|
274
|
-
console.warn("Payload is not a valid object:", payload);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
// Update the settings in the database
|
|
280
|
-
await $.db_device.updateLocalSettings(JSON.stringify(settings), device.id);
|
|
281
|
-
} catch (err) {
|
|
282
|
-
console.error("Failed to update local settings:", err);
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
updateRemoteSettings : async (device,topic,payload)=>{
|
|
287
|
-
|
|
288
|
-
$.db_device.addLog(device.id,"remote_settings",JSON.stringify(payload));
|
|
289
|
-
|
|
290
|
-
let route = topic.split("/");
|
|
291
|
-
|
|
292
|
-
if(route == null){
|
|
293
|
-
console.warn("updateRemoteSettings: topic invalid:",topic);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
// Parse existing settings
|
|
297
|
-
let settings = await $.db_device.getRemoteSettings(device.id);
|
|
298
|
-
|
|
299
|
-
if (!settings || typeof settings !== 'object') {
|
|
300
|
-
settings = {};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
let obj = settings;
|
|
304
|
-
|
|
305
|
-
// Traverse route parts
|
|
306
|
-
route.slice(0, route.length).forEach(property => {
|
|
307
|
-
if (
|
|
308
|
-
!Object.prototype.hasOwnProperty.call(obj, property) ||
|
|
309
|
-
(obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
|
|
310
|
-
obj[property] === null
|
|
311
|
-
) {
|
|
312
|
-
obj[property] = {}; // create nested object if missing or not an object
|
|
313
|
-
}
|
|
314
|
-
obj = obj[property];
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
// Merge payload into nested object
|
|
318
|
-
let data = {};
|
|
319
|
-
try {
|
|
320
|
-
data = JSON.parse(payload);
|
|
321
|
-
} catch(error) {
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
325
|
-
// Merge data properties
|
|
326
|
-
for (const [key, value] of Object.entries(data)) {
|
|
327
|
-
obj[key] = value;
|
|
328
|
-
}
|
|
329
|
-
} else {
|
|
330
|
-
// payload isn't an object, replace the nested object
|
|
331
|
-
obj = payload;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
await $.db_device.updateRemoteSettings(JSON.stringify(settings), device.id);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
console.error("Failed to update local settings:", err);
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
|
|
341
103
|
checkFota : async (release = "dev")=>{
|
|
342
104
|
|
|
343
105
|
const models = await $.db_model.getAll();
|
|
@@ -484,25 +246,310 @@ var self = module.exports = {
|
|
|
484
246
|
}
|
|
485
247
|
},
|
|
486
248
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function parseLwm2mMessage(client, project_name, device, topic, payload, action){
|
|
252
|
+
|
|
253
|
+
console.log("parse lwm2m message: ",topic);
|
|
254
|
+
|
|
255
|
+
try{
|
|
256
|
+
payload = JSON.parse(payload);
|
|
257
|
+
}catch(error){}
|
|
258
|
+
|
|
259
|
+
let word = getFirstWord(topic);
|
|
260
|
+
topic = getWordAfterSlash(topic);
|
|
261
|
+
|
|
262
|
+
switch (word) {
|
|
263
|
+
case "status":
|
|
264
|
+
if (payload === "online" || payload === "offline") {
|
|
265
|
+
$.db_device.update(device.id, "status", payload);
|
|
266
|
+
$.db_device.addLog(device.id,"status",payload);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
break;
|
|
270
|
+
case "sensor":
|
|
271
|
+
updateSensor(device,topic,payload)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if(_project[project_name]){
|
|
275
|
+
_project[project_name]?.module?.parseMessage(client,project_name,device,`${word}/${topic}`,payload,action,()=>{});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function parseMqttMessage(client, project_name, device, topic, payload, retain){
|
|
280
|
+
|
|
281
|
+
if(topic.endsWith("/get"))
|
|
282
|
+
return;
|
|
283
|
+
|
|
284
|
+
//console.log("[MQTT] parse topic: ",topic);
|
|
285
|
+
|
|
286
|
+
try{
|
|
287
|
+
payload = JSON.parse(payload);
|
|
288
|
+
}catch(error){}
|
|
289
|
+
|
|
290
|
+
let word = getFirstWord(topic);
|
|
291
|
+
topic = getWordAfterSlash(topic);
|
|
292
|
+
|
|
293
|
+
switch (word) {
|
|
294
|
+
case "status":
|
|
295
|
+
if (payload === "online" || payload === "offline") {
|
|
296
|
+
$.db_device.update(device.id, "status", payload);
|
|
297
|
+
$.db_device.addLog(device.id,"status",payload);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
break;
|
|
301
|
+
case "model":
|
|
302
|
+
let res = await $.db_model.getByName(payload);
|
|
303
|
+
let model_id = res?.id;
|
|
304
|
+
if (model_id != null && device?.tech != payload) {
|
|
305
|
+
$.db_device.update(device.id, "model_id", model_id);
|
|
306
|
+
$.db_device.addLog(device.id,"model_id",model_id);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
break;
|
|
310
|
+
case "tech":
|
|
311
|
+
if (device?.tech && payload != device.tech) {
|
|
312
|
+
$.db_device.update(device.id, "tech", payload);
|
|
313
|
+
$.db_device.addLog(device.id,"tech",payload);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
break;
|
|
317
|
+
case "version":
|
|
318
|
+
if (device?.version && payload != device.version) {
|
|
319
|
+
$.db_device.addLog(device.id,"version",payload);
|
|
320
|
+
$.db_device.update(device.id, "version", payload);
|
|
321
|
+
handleFotaSuccess(device.id);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
break;
|
|
325
|
+
case "app_version":
|
|
326
|
+
if (device?.app_version && payload != device.app_version) {
|
|
327
|
+
$.db_device.addLog(device.id,"app_version",payload);
|
|
328
|
+
$.db_device.update(device.id, "app_version", payload);
|
|
329
|
+
handleFotaSuccess(device.id);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
break;
|
|
333
|
+
case "fw":
|
|
334
|
+
if(topic === "fota/update/status"){
|
|
335
|
+
handleFotaError(device.id, payload);
|
|
336
|
+
}else{
|
|
337
|
+
if(typeof payload === 'object' && payload !== null){
|
|
338
|
+
try{
|
|
339
|
+
let rows = await $.db_data.updateJson("fw",device.id,payload);
|
|
340
|
+
rows = await $.db_data.addJsonLog("logs_fw",device.id,payload);
|
|
341
|
+
}catch(error){
|
|
342
|
+
console.error(error)
|
|
343
|
+
}
|
|
344
|
+
}else if(typeof payload !== 'object' && payload !== null){
|
|
345
|
+
// change it to topic.startsWith("fw")
|
|
346
|
+
try{
|
|
347
|
+
const column = getWordAfterLastSlash(topic);
|
|
348
|
+
let rows = await $.db_data.update("fw",device.id,column,payload);
|
|
349
|
+
rows = await $.db_data.addLog("logs_fw",device.id,column,payload);
|
|
350
|
+
}catch(error){
|
|
351
|
+
console.error(error)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
break;
|
|
357
|
+
case "settings":
|
|
358
|
+
if(topic.endsWith("/set")){
|
|
359
|
+
updateLocalSettings(device,topic,payload);
|
|
360
|
+
}else{
|
|
361
|
+
updateRemoteSettings(device,topic,payload);
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
break;
|
|
365
|
+
case "sensor":
|
|
366
|
+
let ref = getFirstWord(topic)
|
|
367
|
+
updateSensor(device,ref,payload)
|
|
368
|
+
return;
|
|
369
|
+
break;
|
|
370
|
+
// Optional: default case if needed
|
|
371
|
+
default:
|
|
372
|
+
// handle other topics or do nothing
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if(_project[project_name]){
|
|
377
|
+
_project[project_name]?.module?.parseMessage(client,project_name,device,`${word}/${topic}`,payload,retain,()=>{});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function updateLocalSettings(device, topic, payload){
|
|
382
|
+
|
|
383
|
+
$.db_device.addLog(device.id,"local_settings",JSON.stringify(payload));
|
|
384
|
+
|
|
385
|
+
let route = topic.split("/");
|
|
386
|
+
|
|
387
|
+
if (route == null || route.length == 0) {
|
|
388
|
+
console.warn("updateLocalSettings: topic invalid:", topic);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Retrieve existing settings
|
|
393
|
+
let settings = await $.db_device.getLocalSettings(device.id);
|
|
394
|
+
|
|
395
|
+
if (!settings || typeof settings !== 'object') {
|
|
396
|
+
settings = {};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let obj = settings;
|
|
400
|
+
|
|
401
|
+
// Traverse route parts to reach the target object
|
|
402
|
+
route.slice(0, -1).forEach(property => {
|
|
403
|
+
if (
|
|
404
|
+
!Object.prototype.hasOwnProperty.call(obj, property) ||
|
|
405
|
+
(obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
|
|
406
|
+
obj[property] === null
|
|
407
|
+
) {
|
|
408
|
+
obj[property] = {}; // Create nested object if missing or not an object
|
|
491
409
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
410
|
+
obj = obj[property];
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Check if the data is a plain object, then merge
|
|
414
|
+
if (typeof payload === 'object' && !Array.isArray(payload)) {
|
|
415
|
+
Object.assign(obj, payload);
|
|
416
|
+
} else {
|
|
417
|
+
console.warn("Payload is not a valid object:", payload);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
// Update the settings in the database
|
|
423
|
+
await $.db_device.updateLocalSettings(JSON.stringify(settings), device.id);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error("Failed to update local settings:", err);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function updateRemoteSettings(device,topic,payload){
|
|
430
|
+
|
|
431
|
+
$.db_device.addLog(device.id,"remote_settings",JSON.stringify(payload));
|
|
432
|
+
|
|
433
|
+
let route = topic.split("/");
|
|
434
|
+
|
|
435
|
+
if(route == null){
|
|
436
|
+
console.warn("updateRemoteSettings: topic invalid:",topic);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// Parse existing settings
|
|
440
|
+
let settings = await $.db_device.getRemoteSettings(device.id);
|
|
441
|
+
|
|
442
|
+
if (!settings || typeof settings !== 'object') {
|
|
443
|
+
settings = {};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let obj = settings;
|
|
447
|
+
|
|
448
|
+
// Traverse route parts
|
|
449
|
+
route.slice(0, route.length).forEach(property => {
|
|
450
|
+
if (
|
|
451
|
+
!Object.prototype.hasOwnProperty.call(obj, property) ||
|
|
452
|
+
(obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
|
|
453
|
+
obj[property] === null
|
|
454
|
+
) {
|
|
455
|
+
obj[property] = {}; // create nested object if missing or not an object
|
|
495
456
|
}
|
|
496
|
-
|
|
497
|
-
}
|
|
457
|
+
obj = obj[property];
|
|
458
|
+
});
|
|
498
459
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
460
|
+
if (typeof payload === 'object' && !Array.isArray(payload)) {
|
|
461
|
+
// Merge payload properties
|
|
462
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
463
|
+
obj[key] = value;
|
|
502
464
|
}
|
|
503
|
-
|
|
465
|
+
} else {
|
|
466
|
+
// payload isn't an object, replace the nested object
|
|
467
|
+
obj = payload;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
await $.db_device.updateRemoteSettings(JSON.stringify(settings), device.id);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.error("Failed to update local settings:", err);
|
|
504
474
|
}
|
|
505
475
|
}
|
|
506
476
|
|
|
477
|
+
async function updateSensor(device,ref,payload){
|
|
478
|
+
let res = await $.db_device.getSensorByRef(device.id,ref)
|
|
479
|
+
if(res == null)
|
|
480
|
+
res = await $.db_model.getSensorByRef(device.model_id,ref)
|
|
481
|
+
if(res == null)
|
|
482
|
+
return;
|
|
483
|
+
|
|
484
|
+
object = payload;
|
|
485
|
+
value = null;
|
|
486
|
+
error = null;
|
|
487
|
+
timestamp = null;
|
|
488
|
+
|
|
489
|
+
if (typeof object === 'object' && object !== null) {
|
|
490
|
+
value = object?.value || object?.v;
|
|
491
|
+
error = object?.error || object?.e;
|
|
492
|
+
timestamp = object?.timestamp || objects?.ts;
|
|
493
|
+
}else{
|
|
494
|
+
value = payload;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if(value || error)
|
|
498
|
+
object = null;
|
|
499
|
+
|
|
500
|
+
const data = {
|
|
501
|
+
object,
|
|
502
|
+
value,
|
|
503
|
+
error,
|
|
504
|
+
timestamp
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
$.db_sensor.insert(logs_table,device.id,res.id,data);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
507
510
|
|
|
511
|
+
function handleFotaSuccess (deviceId){
|
|
512
|
+
let object = {
|
|
513
|
+
"nAttempts" : 0,
|
|
514
|
+
"fUpdate" : 0,
|
|
515
|
+
}
|
|
516
|
+
$.db_fota.update(deviceId,object);
|
|
517
|
+
object = {
|
|
518
|
+
success : 1,
|
|
519
|
+
}
|
|
520
|
+
$.db_fota.updateLog(deviceId,object);
|
|
521
|
+
}
|
|
508
522
|
|
|
523
|
+
function handleFotaError (deviceId, error){
|
|
524
|
+
let object = {
|
|
525
|
+
error : error,
|
|
526
|
+
}
|
|
527
|
+
$.db_fota.updateLog(deviceId,object);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function getFirstWord(str){
|
|
531
|
+
const slashIndex = str.indexOf('/');
|
|
532
|
+
if (slashIndex === -1) {
|
|
533
|
+
// No slash found, return the original string
|
|
534
|
+
return str;
|
|
535
|
+
}
|
|
536
|
+
return str.substring(0,slashIndex);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function getWordAfterSlash(str){
|
|
540
|
+
const slashIndex = str.indexOf('/');
|
|
541
|
+
if (slashIndex === -1) {
|
|
542
|
+
// No slash found, return empty string
|
|
543
|
+
return "";
|
|
544
|
+
}
|
|
545
|
+
return str.substring(slashIndex + 1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function getWordAfterLastSlash(str){
|
|
549
|
+
const lastSlashIndex = str.lastIndexOf('/');
|
|
550
|
+
if (lastSlashIndex === -1) {
|
|
551
|
+
// No slash found, return the original string
|
|
552
|
+
return str;
|
|
553
|
+
}
|
|
554
|
+
return str.substring(lastSlashIndex + 1);
|
|
555
|
+
}
|
package/src/kafka/consumer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { Kafka, logLevel } = require('kafkajs');
|
|
2
|
+
const fs = require('fs');
|
|
2
3
|
|
|
3
4
|
var kafka = null;
|
|
4
5
|
var consumer = null;
|
|
@@ -11,23 +12,43 @@ var self = module.exports = {
|
|
|
11
12
|
return new Promise(async (resolve, reject) => {
|
|
12
13
|
try {
|
|
13
14
|
if (!config.kafka.enabled) {
|
|
14
|
-
console.log("Kafka consumer is disabled");
|
|
15
|
+
console.log("[Kafka] consumer is disabled");
|
|
15
16
|
return resolve();
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
console.log("Initializing
|
|
19
|
+
console.log("[Kafka] Initializing consumer...");
|
|
20
|
+
|
|
21
|
+
// Generate a random 6-digit number
|
|
22
|
+
const randomNumber = Math.floor(100000 + Math.random() * 900000);
|
|
19
23
|
|
|
20
24
|
// Build Kafka configuration
|
|
21
25
|
const kafkaConfig = {
|
|
22
|
-
clientId: config.kafka.clientId
|
|
26
|
+
clientId: `${config.kafka.clientId}-${randomNumber}`,
|
|
23
27
|
brokers: config.kafka.brokers,
|
|
24
28
|
connectionTimeout: config.kafka.connectionTimeout,
|
|
25
29
|
requestTimeout: config.kafka.requestTimeout,
|
|
26
30
|
logLevel: logLevel.INFO
|
|
27
31
|
};
|
|
28
32
|
|
|
33
|
+
if(config.kafka.ssl.enabled === true){
|
|
34
|
+
console.log("[Kafka] Try to establish SSL connection..");
|
|
35
|
+
if(config.kafka.ssl.rejectUnauthorized){
|
|
36
|
+
// check certificate. Most secure
|
|
37
|
+
kafkaConfig.ssl = {
|
|
38
|
+
ca: [fs.readFileSync(config.kafka.ssl.file, 'utf8')],
|
|
39
|
+
rejectUnauthorized: config.kafka.ssl.rejectUnauthorized
|
|
40
|
+
}
|
|
41
|
+
}else{
|
|
42
|
+
// trust certificate. Prone to MITM attacks
|
|
43
|
+
kafkaConfig.ssl = {
|
|
44
|
+
rejectUnauthorized: config.kafka.ssl.rejectUnauthorized
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
// Add SASL authentication if credentials provided
|
|
30
50
|
if (config.kafka.sasl.username && config.kafka.sasl.password) {
|
|
51
|
+
console.log("[Kafka] SASL: username and password are defined");
|
|
31
52
|
kafkaConfig.sasl = {
|
|
32
53
|
mechanism: config.kafka.sasl.mechanism,
|
|
33
54
|
username: config.kafka.sasl.username,
|
|
@@ -35,11 +56,6 @@ var self = module.exports = {
|
|
|
35
56
|
};
|
|
36
57
|
}
|
|
37
58
|
|
|
38
|
-
// Add SSL configuration if enabled
|
|
39
|
-
if (config.kafka.ssl) {
|
|
40
|
-
kafkaConfig.ssl = true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
59
|
kafka = new Kafka(kafkaConfig);
|
|
44
60
|
|
|
45
61
|
// Create consumer with shared subscription support
|
|
@@ -62,7 +78,7 @@ var self = module.exports = {
|
|
|
62
78
|
const topics = projects.map(project => project.toString());
|
|
63
79
|
|
|
64
80
|
if (topics.length === 0) {
|
|
65
|
-
console.log("No
|
|
81
|
+
console.log("[Kafka] No topics to subscribe to");
|
|
66
82
|
return resolve();
|
|
67
83
|
}
|
|
68
84
|
|
|
@@ -71,11 +87,11 @@ var self = module.exports = {
|
|
|
71
87
|
fromBeginning: false
|
|
72
88
|
});
|
|
73
89
|
|
|
74
|
-
console.log(`Kafka consumer subscribed to topics: ${topics.join(', ')}`);
|
|
90
|
+
console.log(`[Kafka] consumer subscribed to topics: ${topics.join(', ')}`);
|
|
75
91
|
|
|
76
92
|
return resolve();
|
|
77
93
|
} catch (error) {
|
|
78
|
-
console.error("Failed to initialize
|
|
94
|
+
console.error("[Kafka] Failed to initialize consumer:", error);
|
|
79
95
|
return reject(error);
|
|
80
96
|
}
|
|
81
97
|
});
|
|
@@ -87,11 +103,14 @@ var self = module.exports = {
|
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
try {
|
|
90
|
-
console.log("Starting
|
|
106
|
+
console.log("[Kafka] Starting consumer...");
|
|
91
107
|
running = true;
|
|
92
108
|
|
|
93
109
|
await consumer.run({
|
|
94
110
|
eachMessage: async ({ topic, partition, message }) => {
|
|
111
|
+
|
|
112
|
+
console.log(`[KAFKA] rx: ${message.key.toString()}`);
|
|
113
|
+
|
|
95
114
|
try {
|
|
96
115
|
let payload = message.value ? message.value.toString() : '';
|
|
97
116
|
const kafkaTopic = topic.toString();
|
|
@@ -113,10 +132,13 @@ var self = module.exports = {
|
|
|
113
132
|
|
|
114
133
|
try{
|
|
115
134
|
payload = JSON.parse(payload);
|
|
116
|
-
}catch(error){
|
|
135
|
+
}catch(error){
|
|
136
|
+
console.log(`[KAFKA] error: ${payload}`);
|
|
137
|
+
console.log(error)
|
|
138
|
+
}
|
|
117
139
|
|
|
118
140
|
// Call the device parser with formatted topic
|
|
119
|
-
await $.device.parseMessage(payload.client.id, formattedTopic, payload.mqtt.payload, payload.mqtt.retain);
|
|
141
|
+
await $.device.parseMessage(payload.client.id, formattedTopic, JSON.stringify(payload.mqtt.payload), payload.mqtt.retain);
|
|
120
142
|
|
|
121
143
|
} catch (error) {
|
|
122
144
|
console.error('[Kafka] Error processing message:', {
|
|
@@ -129,9 +151,9 @@ var self = module.exports = {
|
|
|
129
151
|
},
|
|
130
152
|
});
|
|
131
153
|
|
|
132
|
-
console.log("Kafka consumer started successfully");
|
|
154
|
+
console.log("[Kafka] consumer started successfully");
|
|
133
155
|
} catch (error) {
|
|
134
|
-
console.error("Failed to start
|
|
156
|
+
console.error("[KAFKA] Failed to start consumer:", error);
|
|
135
157
|
running = false;
|
|
136
158
|
throw error;
|
|
137
159
|
}
|
|
@@ -143,13 +165,13 @@ var self = module.exports = {
|
|
|
143
165
|
}
|
|
144
166
|
|
|
145
167
|
try {
|
|
146
|
-
console.log("Stopping
|
|
168
|
+
console.log("[KAFKA] Stopping consumer...");
|
|
147
169
|
running = false;
|
|
148
170
|
await consumer.stop();
|
|
149
171
|
await consumer.disconnect();
|
|
150
|
-
console.log("
|
|
172
|
+
console.log("[KAFKA] consumer stopped successfully");
|
|
151
173
|
} catch (error) {
|
|
152
|
-
console.error("Error stopping
|
|
174
|
+
console.error("[KAFKA] Error stopping consumer:", error);
|
|
153
175
|
}
|
|
154
176
|
},
|
|
155
177
|
|
|
@@ -16,7 +16,6 @@ async function testConfigurationValues() {
|
|
|
16
16
|
console.log('\n=== Testing Configuration Values ===');
|
|
17
17
|
|
|
18
18
|
// Test default values
|
|
19
|
-
console.log('Default MQTT parseMessages:', $.config.mqtt.parseMessages);
|
|
20
19
|
console.log('Default Kafka enabled:', $.config.kafka.enabled);
|
|
21
20
|
console.log('Default Kafka brokers:', $.config.kafka.brokers);
|
|
22
21
|
console.log('Default Kafka groupId:', $.config.kafka.groupId);
|
|
@@ -36,7 +35,6 @@ async function testConfigurationValues() {
|
|
|
36
35
|
const newConfig = require(__dirname + path + '/config');
|
|
37
36
|
|
|
38
37
|
console.log('\nWith environment variables:');
|
|
39
|
-
console.log('MQTT parseMessages:', newConfig.mqtt.parseMessages);
|
|
40
38
|
console.log('Kafka enabled:', newConfig.kafka.enabled);
|
|
41
39
|
console.log('Kafka brokers:', newConfig.kafka.brokers);
|
|
42
40
|
console.log('Kafka groupId:', newConfig.kafka.groupId);
|
|
@@ -78,19 +76,19 @@ async function testMQTTMessageParsing() {
|
|
|
78
76
|
|
|
79
77
|
// Test the logic that would be used in index.js
|
|
80
78
|
const testConfigs = [
|
|
81
|
-
{
|
|
82
|
-
{
|
|
79
|
+
{ kafka: { enabled: true } },
|
|
80
|
+
{ kafka: { enabled: false } }
|
|
83
81
|
];
|
|
84
82
|
|
|
85
83
|
for (const config of testConfigs) {
|
|
86
|
-
console.log(`\nTesting with parseMessages: ${config.
|
|
84
|
+
console.log(`\nTesting with kafka parseMessages: ${config.kafka.enabled}`);
|
|
87
85
|
|
|
88
86
|
// Simulate the MQTT message handler logic
|
|
89
87
|
const mockTopic = "test/uid:123/status";
|
|
90
88
|
const mockPayload = "online";
|
|
91
89
|
const mockPacket = { retain: false };
|
|
92
90
|
|
|
93
|
-
if (config.
|
|
91
|
+
if (!config.kafka.enabled) {
|
|
94
92
|
console.log('Would call device.parseMessage');
|
|
95
93
|
await $.device.parseMessage(null, mockTopic, mockPayload, mockPacket.retain);
|
|
96
94
|
} else {
|
|
@@ -47,7 +47,6 @@ async function testKafkaConsumerInit() {
|
|
|
47
47
|
async function testConfiguration() {
|
|
48
48
|
console.log('\n=== Testing Configuration ===');
|
|
49
49
|
|
|
50
|
-
console.log('MQTT parseMessages:', $.config.mqtt.parseMessages);
|
|
51
50
|
console.log('Kafka enabled:', $.config.kafka.enabled);
|
|
52
51
|
console.log('Kafka brokers:', $.config.kafka.brokers);
|
|
53
52
|
console.log('Kafka groupId:', $.config.kafka.groupId);
|