mqtt-devices-parser 1.0.21 → 1.0.23
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/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 -281
- package/src/kafka/consumer.js +22 -6
- 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.23
|
|
4
|
+
src/device/device: fix tech, version and app_version db update
|
|
5
|
+
npm warn audit Updating websocket-stream to 5.3.0, which is a SemVer major change.
|
|
6
|
+
|
|
7
|
+
## 1.0.22
|
|
8
|
+
Adds SSL and SASL support for kafka
|
|
9
|
+
mqtt.parseMessages removed
|
|
10
|
+
src/kafka/consumer: adds random number to kafka client Id
|
|
11
|
+
Parse mqtt lwm2m (#6)
|
|
12
|
+
* in progress: splits parsing messages base on protocol
|
|
13
|
+
* supports local unix timestamp, fix sensor data parse
|
|
14
|
+
models/logs_sensor.models: add localUnixTs
|
|
15
|
+
src/devices/device: fix object parse
|
|
16
|
+
* fix: mqtt parser
|
|
17
|
+
Re-structs parser
|
|
18
|
+
|
|
3
19
|
## 1.0.21
|
|
4
20
|
src/kafka/consumer: fixes parseMessage call
|
|
5
21
|
improves: logs
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mqtt-devices-parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
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,176 +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(payload)
|
|
182
|
-
console.log(err);
|
|
183
|
-
}
|
|
184
|
-
if (typeof object === 'object' && object !== null) {
|
|
185
|
-
value = object?.value;
|
|
186
|
-
error = object?.error;
|
|
187
|
-
}else{
|
|
188
|
-
value = payload;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if(value || error)
|
|
192
|
-
object = null;
|
|
68
|
+
if(!uid.startsWith(project.uidPrefix) || uid.length > project?.uidLength)
|
|
69
|
+
return;
|
|
193
70
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
error
|
|
198
|
-
}
|
|
71
|
+
device = await $.db_device.get(uid);
|
|
72
|
+
if(!device)
|
|
73
|
+
return;
|
|
199
74
|
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
75
|
+
topic = getWordAfterSlash(topic);
|
|
204
76
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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);
|
|
208
81
|
}
|
|
209
82
|
|
|
210
83
|
},
|
|
@@ -227,118 +100,6 @@ var self = module.exports = {
|
|
|
227
100
|
}
|
|
228
101
|
},
|
|
229
102
|
|
|
230
|
-
updateLocalSettings: async (device, topic, payload) => {
|
|
231
|
-
|
|
232
|
-
$.db_device.addLog(device.id,"local_settings",JSON.stringify(payload));
|
|
233
|
-
|
|
234
|
-
let route = topic.split("/");
|
|
235
|
-
|
|
236
|
-
if (route == null || route.length == 0) {
|
|
237
|
-
console.warn("updateLocalSettings: topic invalid:", topic);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Retrieve existing settings
|
|
242
|
-
let settings = await $.db_device.getLocalSettings(device.id);
|
|
243
|
-
|
|
244
|
-
if (!settings || typeof settings !== 'object') {
|
|
245
|
-
settings = {};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
let obj = settings;
|
|
249
|
-
|
|
250
|
-
// Traverse route parts to reach the target object
|
|
251
|
-
route.slice(0, -1).forEach(property => {
|
|
252
|
-
if (
|
|
253
|
-
!Object.prototype.hasOwnProperty.call(obj, property) ||
|
|
254
|
-
(obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
|
|
255
|
-
obj[property] === null
|
|
256
|
-
) {
|
|
257
|
-
obj[property] = {}; // Create nested object if missing or not an object
|
|
258
|
-
}
|
|
259
|
-
obj = obj[property];
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// Parse the payload JSON
|
|
263
|
-
let data = {};
|
|
264
|
-
try {
|
|
265
|
-
data = JSON.parse(payload);
|
|
266
|
-
} catch (error) {
|
|
267
|
-
console.error("Failed to parse payload JSON:", error);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Check if the data is a plain object, then merge
|
|
272
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
273
|
-
Object.assign(obj, data);
|
|
274
|
-
} else {
|
|
275
|
-
console.warn("Payload is not a valid object:", payload);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
// Update the settings in the database
|
|
281
|
-
await $.db_device.updateLocalSettings(JSON.stringify(settings), device.id);
|
|
282
|
-
} catch (err) {
|
|
283
|
-
console.error("Failed to update local settings:", err);
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
|
|
287
|
-
updateRemoteSettings : async (device,topic,payload)=>{
|
|
288
|
-
|
|
289
|
-
$.db_device.addLog(device.id,"remote_settings",JSON.stringify(payload));
|
|
290
|
-
|
|
291
|
-
let route = topic.split("/");
|
|
292
|
-
|
|
293
|
-
if(route == null){
|
|
294
|
-
console.warn("updateRemoteSettings: topic invalid:",topic);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
// Parse existing settings
|
|
298
|
-
let settings = await $.db_device.getRemoteSettings(device.id);
|
|
299
|
-
|
|
300
|
-
if (!settings || typeof settings !== 'object') {
|
|
301
|
-
settings = {};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
let obj = settings;
|
|
305
|
-
|
|
306
|
-
// Traverse route parts
|
|
307
|
-
route.slice(0, route.length).forEach(property => {
|
|
308
|
-
if (
|
|
309
|
-
!Object.prototype.hasOwnProperty.call(obj, property) ||
|
|
310
|
-
(obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
|
|
311
|
-
obj[property] === null
|
|
312
|
-
) {
|
|
313
|
-
obj[property] = {}; // create nested object if missing or not an object
|
|
314
|
-
}
|
|
315
|
-
obj = obj[property];
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Merge payload into nested object
|
|
319
|
-
let data = {};
|
|
320
|
-
try {
|
|
321
|
-
data = JSON.parse(payload);
|
|
322
|
-
} catch(error) {
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
326
|
-
// Merge data properties
|
|
327
|
-
for (const [key, value] of Object.entries(data)) {
|
|
328
|
-
obj[key] = value;
|
|
329
|
-
}
|
|
330
|
-
} else {
|
|
331
|
-
// payload isn't an object, replace the nested object
|
|
332
|
-
obj = payload;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
await $.db_device.updateRemoteSettings(JSON.stringify(settings), device.id);
|
|
337
|
-
} catch (err) {
|
|
338
|
-
console.error("Failed to update local settings:", err);
|
|
339
|
-
}
|
|
340
|
-
},
|
|
341
|
-
|
|
342
103
|
checkFota : async (release = "dev")=>{
|
|
343
104
|
|
|
344
105
|
const models = await $.db_model.getAll();
|
|
@@ -485,25 +246,310 @@ var self = module.exports = {
|
|
|
485
246
|
}
|
|
486
247
|
},
|
|
487
248
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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 (payload != null && 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 (payload != null && 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 (payload != null && 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
|
|
492
409
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
496
456
|
}
|
|
497
|
-
|
|
498
|
-
}
|
|
457
|
+
obj = obj[property];
|
|
458
|
+
});
|
|
499
459
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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;
|
|
503
464
|
}
|
|
504
|
-
|
|
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);
|
|
505
474
|
}
|
|
506
475
|
}
|
|
507
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
|
+
}
|
|
508
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
|
+
}
|
|
509
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;
|
|
@@ -17,17 +18,37 @@ var self = module.exports = {
|
|
|
17
18
|
|
|
18
19
|
console.log("[Kafka] Initializing consumer...");
|
|
19
20
|
|
|
21
|
+
// Generate a random 6-digit number
|
|
22
|
+
const randomNumber = Math.floor(100000 + Math.random() * 900000);
|
|
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
|
|
@@ -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);
|