mqtt-devices-parser 1.0.23 → 1.0.25

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,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.25
4
+
5
+ Sensors template (#9)
6
+
7
+ * new table sensorsTemplate
8
+
9
+ * store topics which match a sensor
10
+ for each mqtt msg received, check if there is a sensor associated to that topic.
11
+ If there is, update current value and insert new log
12
+
13
+ * db: models/sensors
14
+ changes localUnixTs field to remoteUnixTs
15
+ adds value, error
16
+ changes property: allow null
17
+ adds foreign keys to model_id and device_id
18
+
19
+ * handle change db column localUnixTs to remoteUnixTs
20
+
21
+ * fix getSensorsByRef
22
+ ref is not unique so it can have more than 1 row
23
+
24
+ * src/db/data: Implements missing function pathIntoObject
25
+
26
+ * src/aux/parser: adds functions to handle mqtt topics parsing
27
+ index: uses global $.parser as a pointer to class src/aux/parser
28
+ src/device/device: fixes null iterations, sets updateSensor and handleMqttTopic as public functions. Removes local mqtt parser functions. Adapts calls to those functions
29
+
30
+ mqtt: always check if topic is registered
31
+ store values in db
32
+
33
+ ## 1.0.24
34
+ src/device/device: fix log
35
+ src/kafka/consumer: increase initialRetryTime from 100 to 300ms
36
+ src/device/device: parseMqttMessage: get remote configs if not known
37
+ synch configs if they mismatch
38
+ models/devices.models: new column synch
39
+ synchs mqtt topics
40
+ Explanation
41
+ **
42
+ When a message is received, it checks if mqtt topic is associated to the device without "set" if present.
43
+ If topic exists and ends with set, updates localData column. If topic exists and doesn't end with set, updates remoteData column.
44
+ If localData was updated sets topic as unsynched
45
+ If remoteData was received and differs from localData and synch is enabled, tries to synch topic
46
+ **
47
+ adds synch and synched columns to mqtt table
48
+ Adds methods getMqttTopic, updateRemoteTopic, setSynchedTopic
49
+ kafka: adds random number to kafka when program is in dev mode.
50
+ Avoids to read all queue messages since last session
51
+ mqtt templates: preparing mqtt topics synch
52
+ new column synch and localData for mqttTemplates table
53
+ new column mqttTemplate_id for mqtt table
54
+ add property "property" to table sensors
55
+ src/device/device: decode json payload and get property associated
56
+ models/devices: add synched column (for future implementations)
57
+
3
58
  ## 1.0.23
4
59
  src/device/device: fix tech, version and app_version db update
5
60
  npm warn audit Updating websocket-stream to 5.3.0, which is a SemVer major change.
package/index.js CHANGED
@@ -17,6 +17,7 @@ $.db_firmware = require('./src/db/firmware');
17
17
  $.db_sensor = require('./src/db/sensor');
18
18
  $.db_fota = require('./src/db/fota');
19
19
  $.mqtt_client = null;
20
+ $.parser = require('./src/aux/parser')
20
21
 
21
22
  const packageJson = require(__dirname+'/package.json');
22
23
  const packageVersion = packageJson.version;
@@ -174,7 +175,7 @@ function mqtt_connect(){
174
175
  $.mqtt_client.subscribe(project+"/#", (err) => {
175
176
  if(err){
176
177
  console.log("[MQTT] error");
177
- console.err(err);
178
+ console.error(err);
178
179
  }
179
180
  else
180
181
  console.log("[MQTT] subscribed to project:",project);
@@ -230,6 +231,6 @@ function mqtt_connect(){
230
231
 
231
232
  $.mqtt_client.on("error",(error)=>{
232
233
  console.log("[MQTT] error");
233
- console.err(error)
234
+ console.error(error)
234
235
  })
235
236
  }
@@ -71,6 +71,16 @@ module.exports = (sequelize,DataTypes)=>{
71
71
  type: DataTypes.STRING,
72
72
  allowNull: true
73
73
  },
74
+ synch: { // set to 1 to enable auto synch
75
+ type: DataTypes.INTEGER,
76
+ default: 0,
77
+ allowNull: true
78
+ },
79
+ synched: { // handle internally
80
+ type: DataTypes.INTEGER,
81
+ default: 0,
82
+ allowNull: true
83
+ },
74
84
  },
75
85
  {
76
86
  tableName: 'devices',
@@ -30,7 +30,7 @@ module.exports = (sequelize,DataTypes)=>{
30
30
  type: DataTypes.STRING,
31
31
  allowNull: true,
32
32
  },
33
- localUnixTs: {
33
+ remoteUnixTs: {
34
34
  type: DataTypes.BIGINT,
35
35
  allowNull: true,
36
36
  }
@@ -32,6 +32,20 @@ module.exports = (sequelize,DataTypes)=>{
32
32
  template_id: {
33
33
  type: DataTypes.INTEGER,
34
34
  allowNull: true,
35
+ },
36
+ mqttTemplate_id: {
37
+ type: DataTypes.INTEGER,
38
+ allowNull: true,
39
+ },
40
+ synch: {
41
+ type: DataTypes.INTEGER,
42
+ default: 0,
43
+ allowNull: true,
44
+ },
45
+ synched: {
46
+ type: DataTypes.INTEGER,
47
+ default: 0,
48
+ allowNull: true,
35
49
  }
36
50
  },
37
51
  {
@@ -13,10 +13,18 @@ module.exports = (sequelize,DataTypes)=>{
13
13
  type: DataTypes.JSON,
14
14
  allowNull: true
15
15
  },
16
+ localData: {
17
+ type: DataTypes.JSON,
18
+ allowNull: true
19
+ },
16
20
  readInterval: {
17
21
  type: DataTypes.INTEGER,
18
22
  allowNull: true
19
23
  },
24
+ synch: {
25
+ type: DataTypes.INTEGER,
26
+ allowNull: true
27
+ },
20
28
  template_id: {
21
29
  type: DataTypes.INTEGER,
22
30
  allowNull: false
@@ -3,11 +3,19 @@ module.exports = (sequelize,DataTypes)=>{
3
3
  return sequelize.define("sensors", {
4
4
  model_id: { // reference a model
5
5
  type: DataTypes.INTEGER,
6
- allowNull: true,
6
+ allowNull: false,
7
+ references: {
8
+ model: 'models',
9
+ key: 'id'
10
+ }
7
11
  },
8
12
  device_id: { // reference a device id
9
13
  type: DataTypes.INTEGER,
10
- allowNull: true,
14
+ allowNull: false,
15
+ references: {
16
+ model: 'devices',
17
+ key: 'id'
18
+ }
11
19
  },
12
20
  active: {
13
21
  type: DataTypes.BOOLEAN,
@@ -26,6 +34,22 @@ module.exports = (sequelize,DataTypes)=>{
26
34
  type: DataTypes.STRING,
27
35
  allowNull: false,
28
36
  },
37
+ property: {
38
+ type: DataTypes.STRING,
39
+ allowNull: true
40
+ },
41
+ value: {
42
+ type: DataTypes.STRING,
43
+ allowNull: true
44
+ },
45
+ error: {
46
+ type: DataTypes.STRING,
47
+ allowNull: true,
48
+ },
49
+ remoteUnixTs: {
50
+ type: DataTypes.BIGINT,
51
+ allowNull: true,
52
+ },
29
53
  graph: {
30
54
  type: DataTypes.JSON,
31
55
  allowNull: true,
@@ -0,0 +1,43 @@
1
+
2
+ module.exports = (sequelize,DataTypes)=>{
3
+ return sequelize.define("sensorsTemplate", {
4
+ model_id: { // reference a model
5
+ type: DataTypes.INTEGER,
6
+ allowNull: false,
7
+ references: {
8
+ model: 'models',
9
+ key: 'id'
10
+ }
11
+ },
12
+ active: {
13
+ type: DataTypes.BOOLEAN,
14
+ allowNull: false,
15
+ defaultValue: true
16
+ },
17
+ ref: {
18
+ type: DataTypes.STRING,
19
+ allowNull: false,
20
+ },
21
+ name: {
22
+ type: DataTypes.STRING,
23
+ allowNull: false,
24
+ },
25
+ type: {
26
+ type: DataTypes.STRING,
27
+ allowNull: false,
28
+ },
29
+ property: {
30
+ type: DataTypes.STRING,
31
+ allowNull: true
32
+ },
33
+ graph: {
34
+ type: DataTypes.JSON,
35
+ allowNull: true,
36
+ }
37
+ },
38
+ {
39
+ tableName: 'sensorsTemplate',
40
+ freezeTableName: true
41
+ })
42
+ }
43
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mqtt-devices-parser",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/aux/parser.js CHANGED
@@ -16,5 +16,41 @@ module.exports = {
16
16
  }
17
17
  });
18
18
  return obj;
19
+ },
20
+
21
+ getFirstWord : (str)=>{
22
+ const slashIndex = str.indexOf('/');
23
+ if (slashIndex === -1) {
24
+ // No slash found, return the original string
25
+ return str;
26
+ }
27
+ return str.substring(0,slashIndex);
28
+ },
29
+
30
+ getTopicAfterSlash : (str)=>{
31
+ const slashIndex = str.indexOf('/');
32
+ if (slashIndex === -1) {
33
+ // No slash found, return empty string
34
+ return "";
35
+ }
36
+ return str.substring(slashIndex + 1);
37
+ },
38
+
39
+ getWordAfterLastSlash : (str)=>{
40
+ const lastSlashIndex = str.lastIndexOf('/');
41
+ if (lastSlashIndex === -1) {
42
+ // No slash found, return the original string
43
+ return str;
44
+ }
45
+ return str.substring(lastSlashIndex + 1);
46
+ },
47
+
48
+ getWordBeforeLastSlash : (str)=>{
49
+ const lastSlashIndex = str.lastIndexOf('/');
50
+ if (lastSlashIndex === -1) {
51
+ // No slash found, return the original string
52
+ return str;
53
+ }
54
+ return str.substring(0,lastSlashIndex);
19
55
  }
20
56
  }
package/src/db/data.js CHANGED
@@ -372,3 +372,59 @@ var self = module.exports = {
372
372
  });
373
373
  }
374
374
  };
375
+
376
+ /**
377
+ * Build a nested object from a path string and a leaf value.
378
+ *
379
+ * Example:
380
+ * pathIntoObject('foo/bar/baz', 123) -> { foo: { bar: { baz: 123 } } }
381
+ *
382
+ * Notes:
383
+ * - By default, splits on '/'.
384
+ * - Leading/trailing/duplicate delimiters are ignored (e.g., '/a//b/' -> ['a','b']).
385
+ * - Value is used as-is. If you pass JSON.stringify(data), the leaf will be that string.
386
+ *
387
+ * @param {string} path - The path to turn into nested object keys (e.g., 'a/b/c').
388
+ * @param {*} value - The value to place at the final key.
389
+ * @param {Object} [options]
390
+ * @param {string} [options.delimiter='/'] - Path delimiter.
391
+ * @returns {*}
392
+ */
393
+ const parser = {
394
+ pathIntoObject(path, value, options = {}) {
395
+ const delimiter = options.delimiter || '/';
396
+
397
+ if (typeof path !== 'string') {
398
+ throw new TypeError('path must be a string');
399
+ }
400
+
401
+ // Normalize path: remove empty segments caused by leading/trailing or duplicate delimiters
402
+ const segments = path
403
+ .split(delimiter)
404
+ .map(s => s.trim())
405
+ .filter(Boolean);
406
+
407
+ // If no segments, just return the value directly
408
+ if (segments.length === 0) return value;
409
+
410
+ // Build nested object
411
+ let root = {};
412
+ let cursor = root;
413
+
414
+ for (let i = 0; i < segments.length; i++) {
415
+ const key = segments[i];
416
+ const isLast = i === segments.length - 1;
417
+
418
+ if (isLast) {
419
+ cursor[key] = value;
420
+ } else {
421
+ // Create the next level if not present
422
+ cursor[key] = {};
423
+ cursor = cursor[key];
424
+ }
425
+ }
426
+
427
+ return root;
428
+ },
429
+ };
430
+
package/src/db/device.js CHANGED
@@ -194,7 +194,6 @@ var self = module.exports = {
194
194
  });
195
195
  },
196
196
 
197
-
198
197
  addLog : async(id,column,value)=>{
199
198
  return new Promise((resolve,reject) => {
200
199
 
@@ -255,26 +254,26 @@ var self = module.exports = {
255
254
 
256
255
  getLocalSettings : async(deviceId)=>{
257
256
 
258
- return new Promise((resolve,reject) => {
257
+ return new Promise((resolve,reject) => {
259
258
 
260
- let query = "SELECT local_settings FROM ?? where id = ?";
261
- let table = ["devices", deviceId];
262
- query = mysql.format(query,table);
259
+ let query = "SELECT local_settings FROM ?? where id = ?";
260
+ let table = ["devices", deviceId];
261
+ query = mysql.format(query,table);
263
262
 
264
- $.db.queryRow(query)
265
- .then( rows => {
266
- if(rows?.length > 0)
267
- return resolve(rows[0]?.local_settings);
268
- else
269
- return resolve(null);
270
- })
271
- .catch( err => {
272
- return reject(err);
273
- });
263
+ $.db.queryRow(query)
264
+ .then( rows => {
265
+ if(rows?.length > 0)
266
+ return resolve(rows[0]?.local_settings);
267
+ else
268
+ return resolve(null);
269
+ })
270
+ .catch( err => {
271
+ return reject(err);
272
+ });
274
273
  });
275
- },
274
+ },
276
275
 
277
- updateLocalSettings : async(settings, deviceId)=>{
276
+ updateLocalSettings : async(settings, deviceId)=>{
278
277
 
279
278
  return new Promise((resolve,reject) => {
280
279
  let obj = {
@@ -299,7 +298,7 @@ var self = module.exports = {
299
298
  });
300
299
  },
301
300
 
302
- getRemoteSettings : async(deviceId)=>{
301
+ getRemoteSettings : async(deviceId)=>{
303
302
 
304
303
  return new Promise((resolve,reject) => {
305
304
 
@@ -389,13 +388,34 @@ var self = module.exports = {
389
388
  });
390
389
  },
391
390
 
392
- getSensorByRef : async(deviceId, ref)=>{
391
+ getSensorsByRef : async(deviceId, ref)=>{
393
392
  return new Promise((resolve,reject) => {
394
393
 
395
394
  let query = "SELECT * FROM ?? where device_id = ? and ref = ?";
396
395
  let args = ["sensors",deviceId,ref];
397
396
  query = mysql.format(query,args);
398
397
 
398
+ $.db.queryRow(query)
399
+ .then( rows => {
400
+ if(rows.length > 0)
401
+ return resolve(rows);
402
+ else
403
+ return resolve(null);
404
+ })
405
+ .catch( err => {
406
+ console.log(err);
407
+ return resolve(null);
408
+ });
409
+ });
410
+ },
411
+
412
+ getMqttTopic : async(deviceId, topic)=>{
413
+ return new Promise((resolve,reject) => {
414
+
415
+ let query = "SELECT * FROM ?? where device_id = ? and topic = ?";
416
+ let args = ["mqtt",deviceId,topic];
417
+ query = mysql.format(query,args);
418
+
399
419
  $.db.queryRow(query)
400
420
  .then( rows => {
401
421
  if(rows.length > 0)
@@ -408,6 +428,82 @@ var self = module.exports = {
408
428
  return resolve(null);
409
429
  });
410
430
  });
411
- }
431
+ },
432
+
433
+ updateLocalTopic : async(id, data)=>{
434
+
435
+ let jsonData = {
436
+ value : data
437
+ };
438
+
439
+ return new Promise((resolve,reject) => {
440
+ let obj = {
441
+ localData : JSON.stringify(jsonData),
442
+ updatedAt : moment().utc().format('YYYY-MM-DD HH:mm:ss')
443
+ }
444
+
445
+ let filter = {
446
+ id
447
+ };
448
+
449
+ $.db.update("mqtt",obj,filter)
450
+ .then (rows => {
451
+ return resolve(rows[0]);
452
+ })
453
+ .catch(error => {
454
+ return reject(error);
455
+ });
456
+
457
+ });
458
+ },
459
+
460
+ updateRemoteTopic : async(id, data)=>{
412
461
 
462
+ let jsonData = {
463
+ value : data
464
+ };
465
+
466
+ return new Promise((resolve,reject) => {
467
+ let obj = {
468
+ remoteData : JSON.stringify(jsonData),
469
+ updatedAt : moment().utc().format('YYYY-MM-DD HH:mm:ss')
470
+ }
471
+
472
+ let filter = {
473
+ id
474
+ };
475
+
476
+ $.db.update("mqtt",obj,filter)
477
+ .then (rows => {
478
+ return resolve(rows[0]);
479
+ })
480
+ .catch(error => {
481
+ return reject(error);
482
+ });
483
+
484
+ });
485
+ },
486
+
487
+ setSynchedTopic : async(id, data)=>{
488
+
489
+ return new Promise((resolve,reject) => {
490
+ let obj = {
491
+ synched : data ? 1 : 0,
492
+ updatedAt : moment().utc().format('YYYY-MM-DD HH:mm:ss')
493
+ }
494
+
495
+ let filter = {
496
+ id,
497
+ };
498
+
499
+ $.db.update("mqtt",obj,filter)
500
+ .then (rows => {
501
+ return resolve(rows[0]);
502
+ })
503
+ .catch(error => {
504
+ return reject(error);
505
+ });
506
+
507
+ });
508
+ },
413
509
  }
package/src/db/model.js CHANGED
@@ -112,7 +112,7 @@ var self = module.exports = {
112
112
  });
113
113
  },
114
114
 
115
- getSensorByRef : async(modelId, ref)=>{
115
+ getSensorsByRef : async(modelId, ref)=>{
116
116
  return new Promise((resolve,reject) => {
117
117
 
118
118
  let query = "SELECT * FROM ?? where model_id = ? and ref = ?";
@@ -122,7 +122,7 @@ var self = module.exports = {
122
122
  $.db.queryRow(query)
123
123
  .then( rows => {
124
124
  if(rows.length > 0)
125
- return resolve(rows[0]);
125
+ return resolve(rows);
126
126
  else
127
127
  return resolve(null);
128
128
  })
package/src/db/sensor.js CHANGED
@@ -26,6 +26,7 @@ var self = module.exports = {
26
26
  });
27
27
  },
28
28
 
29
+ // not used !!
29
30
  getByRef : async (ref)=>{
30
31
 
31
32
  return new Promise((resolve,reject) => {
@@ -98,7 +99,7 @@ var self = module.exports = {
98
99
  value : payload?.value,
99
100
  error : payload?.error,
100
101
  obj : payload?.object,
101
- localUnixTs : payload?.timestamp, // local timestamp
102
+ remoteUnixTs : payload?.timestamp, // local timestamp
102
103
  createdAt : moment().utc().format('YYYY-MM-DD HH:mm:ss'),
103
104
  updatedAt : moment().utc().format('YYYY-MM-DD HH:mm:ss')
104
105
  }
@@ -113,4 +114,16 @@ var self = module.exports = {
113
114
  });
114
115
  },
115
116
 
117
+ update : async(table,obj,filter)=>{
118
+ return new Promise((resolve,reject) => {
119
+
120
+ $.db.update(table,obj,filter)
121
+ .then (rows => {
122
+ return resolve(rows);
123
+ })
124
+ .catch(error => {
125
+ return reject(error);
126
+ });
127
+ });
128
+ },
116
129
  }
@@ -1,6 +1,8 @@
1
1
  const moment = require('moment');
2
+ const { isDeepStrictEqual } = require('node:util');
2
3
 
3
4
  var _project = [];
5
+ const table = "sensors"
4
6
  const logs_table = "logs_sensor";
5
7
  const semver = require('semver');
6
8
 
@@ -29,8 +31,6 @@ var self = module.exports = {
29
31
  })
30
32
  })
31
33
  });
32
-
33
-
34
34
  },
35
35
 
36
36
  parseMessage : async (client,topic,payload,retain)=>{
@@ -48,22 +48,22 @@ var self = module.exports = {
48
48
  }catch(err){};
49
49
 
50
50
  // --- project ---
51
- project_name = getFirstWord(topic);
51
+ project_name = $.parser.getFirstWord(topic);
52
52
  project = await $.db_project.getByName(project_name);
53
53
  if(project == null)
54
54
  return;
55
55
 
56
56
  project_id = project?.id;
57
- topic = getWordAfterSlash(topic)
57
+ topic = $.parser.getTopicAfterSlash(topic)
58
58
 
59
59
  // check if is from lwm2m gw
60
60
  if(topic.startsWith("responses") || topic.startsWith("requests")){
61
- action = getFirstWord(topic);
62
- topic = getWordAfterSlash(topic);
61
+ action = $.parser.getFirstWord(topic);
62
+ topic = $.parser.getTopicAfterSlash(topic);
63
63
  }
64
64
 
65
65
  // --- uid ---
66
- uid = getFirstWord(topic);
66
+ uid = $.parser.getFirstWord(topic);
67
67
  // check if topic corresponds to a device
68
68
  if(!uid.startsWith(project.uidPrefix) || uid.length > project?.uidLength)
69
69
  return;
@@ -72,14 +72,13 @@ var self = module.exports = {
72
72
  if(!device)
73
73
  return;
74
74
 
75
- topic = getWordAfterSlash(topic);
75
+ topic = $.parser.getTopicAfterSlash(topic);
76
76
 
77
77
  if(device.protocol.toLowerCase() === "lwm2m"){
78
78
  parseLwm2mMessage(client, project_name, device, topic, payload, action);
79
79
  }else if(device.protocol.toLowerCase() === "mqtt"){
80
80
  parseMqttMessage(client, project_name, device, topic, payload, retain);
81
81
  }
82
-
83
82
  },
84
83
 
85
84
  deleteLogs : async()=>{
@@ -104,6 +103,9 @@ var self = module.exports = {
104
103
 
105
104
  const models = await $.db_model.getAll();
106
105
 
106
+ if(!models || !models?.length)
107
+ return;
108
+
107
109
  for (const model of models) {
108
110
 
109
111
  //console.log("model:",model?.name);
@@ -180,6 +182,9 @@ var self = module.exports = {
180
182
 
181
183
  const devices = await $.db_fota.getUpdatable(release);
182
184
 
185
+ if(!devices || !!devices?.length)
186
+ return;
187
+
183
188
  const batchSize = 10;
184
189
  for (let i = 0; i < devices.length; i += batchSize) {
185
190
  const batch = devices.slice(i, i + batchSize);
@@ -246,6 +251,91 @@ var self = module.exports = {
246
251
  }
247
252
  },
248
253
 
254
+
255
+ updateSensor : async (device,ref,payload)=>{
256
+
257
+ let sensors = await $.db_device.getSensorsByRef(device.id,ref)
258
+
259
+ if(!sensors?.length)
260
+ return;
261
+
262
+ object = payload;
263
+ value = null;
264
+ error = null;
265
+ timestamp = null;
266
+
267
+ sensors.map( (sensor,index) =>{
268
+ let value = null;
269
+ let error = null;
270
+ let remoteUnixTs = null;
271
+ if(sensor?.type === "json" && typeof object === 'object'){
272
+ if(object.hasOwnProperty(sensor?.property)){
273
+ value = object[sensor.property];
274
+ }
275
+ }else{
276
+ if (typeof object === 'object') {
277
+ value = object?.value || object?.v;
278
+ error = object?.error || object?.e;
279
+ remoteUnixTs = object?.timestamp || objects?.ts;
280
+ }else{
281
+ value = payload;
282
+ }
283
+ }
284
+ if(value || error){
285
+
286
+ if(!remoteUnixTs)
287
+ remoteUnixTs = moment().unix()
288
+
289
+ const data = {
290
+ value,
291
+ error,
292
+ remoteUnixTs
293
+ }
294
+
295
+ let filter = {
296
+ id : sensor.id
297
+ }
298
+
299
+ $.db_sensor.update(table,data,filter);
300
+ $.db_sensor.insert(logs_table,device.id,sensor.id,data);
301
+ }
302
+ })
303
+ },
304
+
305
+ handleMqttTopic : async(device, dbTopic, payload, set)=>{
306
+ if(set){
307
+ // update local topic
308
+ $.db_device.updateLocalTopic(dbTopic.id,payload)
309
+ .then(()=>{
310
+ // set as not synched
311
+ $.db_device.setSynchedTopic(dbTopic.id,false);
312
+ })
313
+ .catch((error)=>{
314
+ console.error(error);
315
+ })
316
+ }else{
317
+ // update remote topic
318
+ $.db_device.updateRemoteTopic(dbTopic.id,payload)
319
+ .then(async (res)=>{
320
+ // check if topics mismatch
321
+ if(isDeepStrictEqual(dbTopic?.localData, dbTopic?.remoteData)){
322
+ $.db_device.setSynchedTopic(dbTopic.id,true);
323
+ }else{
324
+ // set as not synched
325
+ await $.db_device.setSynchedTopic(dbTopic.id,false);
326
+ if(dbTopic?.synch){ // check if sync is enabled
327
+ // synch topic
328
+ synchMqttTopic(device,dbTopic);
329
+ }
330
+ }
331
+ })
332
+ .catch((error)=>{
333
+ console.error(error);
334
+ })
335
+
336
+ }
337
+ },
338
+
249
339
  }
250
340
 
251
341
  async function parseLwm2mMessage(client, project_name, device, topic, payload, action){
@@ -256,8 +346,8 @@ async function parseLwm2mMessage(client, project_name, device, topic, payload, a
256
346
  payload = JSON.parse(payload);
257
347
  }catch(error){}
258
348
 
259
- let word = getFirstWord(topic);
260
- topic = getWordAfterSlash(topic);
349
+ let word = $.parser.getFirstWord(topic);
350
+ topic = $.parser.getTopicAfterSlash(topic);
261
351
 
262
352
  switch (word) {
263
353
  case "status":
@@ -268,7 +358,7 @@ async function parseLwm2mMessage(client, project_name, device, topic, payload, a
268
358
  return;
269
359
  break;
270
360
  case "sensor":
271
- updateSensor(device,topic,payload)
361
+ self.updateSensor(device,topic,payload)
272
362
  }
273
363
 
274
364
  if(_project[project_name]){
@@ -281,14 +371,15 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
281
371
  if(topic.endsWith("/get"))
282
372
  return;
283
373
 
374
+ const topicBck = topic;
284
375
  //console.log("[MQTT] parse topic: ",topic);
285
376
 
286
377
  try{
287
378
  payload = JSON.parse(payload);
288
379
  }catch(error){}
289
380
 
290
- let word = getFirstWord(topic);
291
- topic = getWordAfterSlash(topic);
381
+ let word = $.parser.getFirstWord(topic);
382
+ topic = $.parser.getTopicAfterSlash(topic);
292
383
 
293
384
  switch (word) {
294
385
  case "status":
@@ -296,7 +387,32 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
296
387
  $.db_device.update(device.id, "status", payload);
297
388
  $.db_device.addLog(device.id,"status",payload);
298
389
  }
299
- return;
390
+
391
+ // list mqtt topics and do calls to readable topics
392
+ if (payload === "online"){
393
+ let mqtt_prefix = `${project_name}/${device.uid}`;
394
+ // check if remote settings are known, if not require it
395
+ if(!device.remote_settings?.log){
396
+ let topic = `${mqtt_prefix}/settings/log/get`
397
+ $.mqtt_client.publish(topic,"",{qos:1,retain:false});
398
+ }
399
+ if(!device.remote_settings?.keepalive){
400
+ let topic = `${mqtt_prefix}/settings/keepalive/get`
401
+ $.mqtt_client.publish(topic,"",{qos:1,retain:false});
402
+ }
403
+ if(!device.remote_settings?.modem){
404
+ let topic = `${mqtt_prefix}/settings/modem/get`
405
+ $.mqtt_client.publish(topic,"",{qos:1,retain:false});
406
+ }
407
+ if(!device.remote_settings?.wifi){
408
+ let topic = `${mqtt_prefix}/settings/wifi/get`
409
+ $.mqtt_client.publish(topic,"",{qos:1,retain:false});
410
+ }
411
+ if(!device.remote_settings?.mqtt){
412
+ let topic = `${mqtt_prefix}/settings/mqtt/get`
413
+ $.mqtt_client.publish(topic,"",{qos:1,retain:false});
414
+ }
415
+ }
300
416
  break;
301
417
  case "model":
302
418
  let res = await $.db_model.getByName(payload);
@@ -305,14 +421,12 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
305
421
  $.db_device.update(device.id, "model_id", model_id);
306
422
  $.db_device.addLog(device.id,"model_id",model_id);
307
423
  }
308
- return;
309
424
  break;
310
425
  case "tech":
311
426
  if (payload != null && payload != device?.tech) {
312
427
  $.db_device.update(device.id, "tech", payload);
313
428
  $.db_device.addLog(device.id,"tech",payload);
314
429
  }
315
- return;
316
430
  break;
317
431
  case "version":
318
432
  if (payload != null && payload != device?.version) {
@@ -320,7 +434,6 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
320
434
  $.db_device.update(device.id, "version", payload);
321
435
  handleFotaSuccess(device.id);
322
436
  }
323
- return;
324
437
  break;
325
438
  case "app_version":
326
439
  if (payload != null && payload != device?.app_version) {
@@ -328,7 +441,6 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
328
441
  $.db_device.update(device.id, "app_version", payload);
329
442
  handleFotaSuccess(device.id);
330
443
  }
331
- return;
332
444
  break;
333
445
  case "fw":
334
446
  if(topic === "fota/update/status"){
@@ -344,7 +456,7 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
344
456
  }else if(typeof payload !== 'object' && payload !== null){
345
457
  // change it to topic.startsWith("fw")
346
458
  try{
347
- const column = getWordAfterLastSlash(topic);
459
+ const column = $.parser.getWordAfterLastSlash(topic);
348
460
  let rows = await $.db_data.update("fw",device.id,column,payload);
349
461
  rows = await $.db_data.addLog("logs_fw",device.id,column,payload);
350
462
  }catch(error){
@@ -352,7 +464,6 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
352
464
  }
353
465
  }
354
466
  }
355
- return;
356
467
  break;
357
468
  case "settings":
358
469
  if(topic.endsWith("/set")){
@@ -360,25 +471,54 @@ async function parseMqttMessage(client, project_name, device, topic, payload, re
360
471
  }else{
361
472
  updateRemoteSettings(device,topic,payload);
362
473
  }
363
- return;
364
- break;
365
- case "sensor":
366
- let ref = getFirstWord(topic)
367
- updateSensor(device,ref,payload)
368
- return;
369
474
  break;
370
475
  // Optional: default case if needed
371
476
  default:
372
- // handle other topics or do nothing
373
477
  break;
374
478
  }
375
479
 
480
+ // check if topic is associated with device
481
+ let findTopic = topicBck;
482
+ let set = false;
483
+ if(topicBck.endsWith("/set")){ // remove set if exists on topic
484
+ set = true;
485
+ findTopic = $.parser.getWordBeforeLastSlash(topicBck);
486
+ }
487
+
488
+ const dbTopic = await $.db_device.getMqttTopic(device.id,findTopic)
489
+ if(dbTopic != null){
490
+ self.handleMqttTopic(device,dbTopic,payload,set);
491
+ }
492
+
493
+ // check if topic is a sensor
494
+ if(!set)
495
+ self.updateSensor(device,topicBck,payload);
496
+
376
497
  if(_project[project_name]){
377
498
  _project[project_name]?.module?.parseMessage(client,project_name,device,`${word}/${topic}`,payload,retain,()=>{});
378
499
  }
379
500
  }
380
501
 
381
- async function updateLocalSettings(device, topic, payload){
502
+ async function synchMqttTopic(device,dbTopic){
503
+
504
+ const project = await $.db_project.getById(device.project_id);
505
+ let mqtt_prefix = `${project.name}/${device.uid}`;
506
+ let topic = `${mqtt_prefix}/${dbTopic.topic}/set`
507
+ let payload = "";
508
+ if(dbTopic?.remoteData && typeof dbTopic?.remoteData === 'object'){
509
+ try{
510
+ payload = JSON.stringify(dbTopic?.remoteData);
511
+ }catch(err){
512
+ console.error(err);
513
+ return;
514
+ }
515
+ }else{
516
+ payload = dbTopic?.remoteData;
517
+ }
518
+ $.mqtt_client.publish(topic,payload,{qos:1,retain:false});
519
+ }
520
+
521
+ async function updateLocalSettings(device,topic,payload){
382
522
 
383
523
  $.db_device.addLog(device.id,"local_settings",JSON.stringify(payload));
384
524
 
@@ -469,45 +609,42 @@ async function updateRemoteSettings(device,topic,payload){
469
609
 
470
610
  try {
471
611
  await $.db_device.updateRemoteSettings(JSON.stringify(settings), device.id);
612
+ if(device?.synch)
613
+ synchSettings(device,route[0]);
472
614
  } catch (err) {
473
615
  console.error("Failed to update local settings:", err);
474
616
  }
475
617
  }
476
618
 
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;
619
+ async function synchSettings(device,key){
488
620
 
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
621
+ console.log(`check topic ${key} from ${device.uid}`);
622
+ // Retrieve existing settings
623
+ let localSettings = await $.db_device.getLocalSettings(device.id);
624
+ console.log(localSettings)
625
+ let remoteSettings = await $.db_device.getRemoteSettings(device.id);
626
+ console.log(remoteSettings)
627
+
628
+ //let keys = await checkSettings(localSettings,remoteSettings);
629
+ if(localSettings?.hasOwnProperty(key) && remoteSettings?.hasOwnProperty(key)){
630
+ if(!isDeepStrictEqual(localSettings?.[key], remoteSettings?.[key])){
631
+ const project = await $.db_project.getById(device.project_id);
632
+ let mqtt_prefix = `${project.name}/${device.uid}`;
633
+ let topic = `${mqtt_prefix}/settings/${key}/set`
634
+ try{
635
+ const payload = JSON.stringify(localSettings[key]);
636
+ console.log(`updating key: ${key} with data:`);
637
+ console.log(payload);
638
+ $.mqtt_client.publish(topic,payload,{qos:1,retain:false});
639
+ }catch(err){
640
+ console.error(localSettings[key])
641
+ console.error(err);
642
+ }
643
+ }
505
644
  }
506
-
507
- $.db_sensor.insert(logs_table,device.id,res.id,data);
508
- return;
509
645
  }
510
646
 
647
+
511
648
  function handleFotaSuccess (deviceId){
512
649
  let object = {
513
650
  "nAttempts" : 0,
@@ -527,29 +664,4 @@ function handleFotaError (deviceId, error){
527
664
  $.db_fota.updateLog(deviceId,object);
528
665
  }
529
666
 
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
667
 
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
- }
@@ -58,9 +58,17 @@ var self = module.exports = {
58
58
 
59
59
  kafka = new Kafka(kafkaConfig);
60
60
 
61
+ let kafkaGroupId = "";
62
+ if(config.env === 'development'){
63
+ const n = Math.floor(Math.random() * 1000) + 1; // 1..1000
64
+ kafkaGroupId = `${config.kafka.groupId}-${String(n)}`;
65
+ }else{
66
+ kafkaGroupId = config.kafka.groupId;
67
+ }
68
+
61
69
  // Create consumer with shared subscription support
62
70
  consumer = kafka.consumer({
63
- groupId: config.kafka.groupId,
71
+ groupId: kafkaGroupId,
64
72
  sessionTimeout: 30000,
65
73
  rebalanceTimeout: 60000,
66
74
  heartbeatInterval: 3000,
@@ -69,7 +77,7 @@ var self = module.exports = {
69
77
  maxBytes: 10485760,
70
78
  maxWaitTimeInMs: 5000,
71
79
  retry: {
72
- initialRetryTime: 100,
80
+ initialRetryTime: 300,
73
81
  retries: 8
74
82
  }
75
83
  });