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 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**: Apache Kafka (optional)
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
- - `KAFKA_SSL` - Set to `'true'` to enable SSL (default: `false`)
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: Enable Kafka with basic authentication**
72
+ **Example 1: Use Kafka with SSL and SCRAM-SHA-256 authentication - Most Secure**
73
73
  ```bash
74
- export KAFKA_ENABLED=true
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 2: Use Kafka only (disable MQTT parsing)**
104
+ **Example 4: Use KAFKA without authentication. Not secure at all**
82
105
  ```bash
83
106
  export KAFKA_ENABLED=true
84
- export MQTT_PARSE_MESSAGES=false
85
- export KAFKA_BROKERS=kafka-cluster:9092
107
+ export KAFKA_SSL_ENABLED=false
108
+ export KAFKA_SASL_USERNAME=''
109
+ export KAFKA_SASL_PASSWORD=''
86
110
  ```
87
111
 
88
- **Example 3: Multi-process deployment**
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
  },
@@ -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.mqtt.parseMessages) {
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 to: ${$.config.mqtt.host}:${$.config.mqtt.port}`);
169
- projects.map( project=>{
170
- console.log("subscribing project:",project);
171
- $.mqtt_client.subscribe(project+"/#")
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 ($.config.mqtt.parseMessages) {
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("reconnect")
212
+ console.log("[MQTT] reconnected")
200
213
  });
201
214
 
202
215
  $.mqtt_client.on("close",()=>{
203
- console.log("close")
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(packet)
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
  }
@@ -30,6 +30,10 @@ module.exports = (sequelize,DataTypes)=>{
30
30
  type: DataTypes.STRING,
31
31
  allowNull: true,
32
32
  },
33
+ localUnixTs: {
34
+ type: DataTypes.BIGINT,
35
+ allowNull: true,
36
+ }
33
37
  },
34
38
  {
35
39
  tableName: 'logs_sensor',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mqtt-devices-parser",
3
- "version": "1.0.20",
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.5.2"
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
  }
@@ -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
- let index = topic.indexOf("/");
47
- if(index == -1)
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
- // --- uid ---
59
- topic = topic.substring(index+1);
60
- index = topic.indexOf("/");
61
- if(index == -1)
62
- return;
56
+ project_id = project?.id;
57
+ topic = getWordAfterSlash(topic)
63
58
 
64
- let uid = topic.substring(0,index);
65
- topic = topic.substring(index+1);
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) && uid.length <= project?.uidLength){
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
- const data = {
194
- object,
195
- value,
196
- error
197
- }
71
+ device = await $.db_device.get(uid);
72
+ if(!device)
73
+ return;
198
74
 
199
- await $.db_sensor.insert(logs_table,device.id,res.id,data);
200
- }
201
- }
202
- }
75
+ topic = getWordAfterSlash(topic);
203
76
 
204
- if(_project[project_name]){
205
- _project[project_name]?.module?.parseMessage(client,project_name,device,topic,payload,retain,()=>{});
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
- handleFotaSuccess : async (deviceId) => {
488
- let object = {
489
- "nAttempts" : 0,
490
- "fUpdate" : 0,
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
- await $.db_fota.update(deviceId,object);
493
- object = {
494
- success : 1,
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
- await $.db_fota.updateLog(deviceId,object);
497
- },
457
+ obj = obj[property];
458
+ });
498
459
 
499
- handleFotaError : async (deviceId, error) => {
500
- let object = {
501
- error : error,
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
- await $.db_fota.updateLog(deviceId,object);
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
+ }
@@ -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 Kafka consumer...");
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 Kafka topics to subscribe to");
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 Kafka consumer:", error);
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 Kafka consumer...");
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 Kafka consumer:", error);
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 Kafka consumer...");
168
+ console.log("[KAFKA] Stopping consumer...");
147
169
  running = false;
148
170
  await consumer.stop();
149
171
  await consumer.disconnect();
150
- console.log("Kafka consumer stopped successfully");
172
+ console.log("[KAFKA] consumer stopped successfully");
151
173
  } catch (error) {
152
- console.error("Error stopping Kafka consumer:", error);
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
- { mqtt: { parseMessages: true } },
82
- { mqtt: { parseMessages: false } }
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.mqtt.parseMessages}`);
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.mqtt.parseMessages) {
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);