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 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**: 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();
@@ -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.21",
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.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,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
- 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(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
- const data = {
195
- object,
196
- value,
197
- error
198
- }
71
+ device = await $.db_device.get(uid);
72
+ if(!device)
73
+ return;
199
74
 
200
- await $.db_sensor.insert(logs_table,device.id,res.id,data);
201
- }
202
- }
203
- }
75
+ topic = getWordAfterSlash(topic);
204
76
 
205
- if(_project[project_name]){
206
- _project[project_name]?.module?.parseMessage(client,project_name,device,topic,payload,retain,()=>{});
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
- handleFotaSuccess : async (deviceId) => {
489
- let object = {
490
- "nAttempts" : 0,
491
- "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 (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
- await $.db_fota.update(deviceId,object);
494
- object = {
495
- 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
496
456
  }
497
- await $.db_fota.updateLog(deviceId,object);
498
- },
457
+ obj = obj[property];
458
+ });
499
459
 
500
- handleFotaError : async (deviceId, error) => {
501
- let object = {
502
- 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;
503
464
  }
504
- 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);
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
+ }
@@ -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
- { 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);