mqtt-devices-parser 1.0.11 → 1.0.13

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.
@@ -1,6 +1,6 @@
1
1
  const moment = require('moment');
2
2
 
3
- var project = [];
3
+ var _project = [];
4
4
  const logs_table = "sensor_logs";
5
5
  const semver = require('semver');
6
6
 
@@ -14,11 +14,11 @@ var self = module.exports = {
14
14
 
15
15
  projects.map( async (name,counter)=>{
16
16
  console.log("project:",name)
17
- project[name] = {
17
+ _project[name] = {
18
18
  module : null
19
19
  };
20
- project[name].module = require(`${BASE_DIR}/projects/${name}/${name}.js`)
21
- project[name].module?.init();
20
+ _project[name].module = require(`${BASE_DIR}/projects/${name}/${name}.js`)
21
+ _project[name].module?.init();
22
22
  let row = await $.db_project.getByName(name);
23
23
  if(row == null)
24
24
  $.db_project.insert(name,name,"logs_"+name);
@@ -32,6 +32,152 @@ var self = module.exports = {
32
32
 
33
33
  },
34
34
 
35
+ parseMessage : async (client,topic,payload,retain)=>{
36
+
37
+ let topic_bck = topic;
38
+
39
+ //let index = topic.indexOf(MACRO_UID_PREFIX);
40
+ // --- project ---
41
+ let index = topic.indexOf("/");
42
+ if(index == -1)
43
+ return;
44
+
45
+ let project_name = topic.substring(0,index);
46
+ let project = await $.db_project.getByName(project_name);
47
+
48
+ if(project == null)
49
+ return;
50
+
51
+ let project_id = project?.id;
52
+
53
+ // --- uid ---
54
+ topic = topic.substring(index+1);
55
+ index = topic.indexOf("/");
56
+ if(index == -1)
57
+ return;
58
+
59
+ let uid = topic.substring(0,index);
60
+ topic = topic.substring(index+1);
61
+
62
+ // check if topic corresponds to a device
63
+ if(uid.startsWith(project.uidPrefix) && uid.length == project?.uidLength){
64
+
65
+ let device = await $.db_device.get(uid);
66
+
67
+ // Insert device if not exists on db
68
+ if(device == null){
69
+ let obj = {
70
+ uid : uid,
71
+ accept_release : "prod",
72
+ }
73
+ $.db_device.insert(obj)
74
+ .then(async()=>{
75
+ device = await $.db_device.get(uid);
76
+ })
77
+ .catch( (err) => {});
78
+ }
79
+
80
+ // update project id if needed
81
+ if(project_id != null && project_id != device?.project_id){
82
+ $.db_device.update(device.id,"project_id",project_id);
83
+ $.db_device.addLog(device.id,"project_id",project_id);
84
+ }
85
+
86
+ switch (topic) {
87
+ case "status":
88
+ if (payload === "online" || payload === "offline") {
89
+ await $.db_device.update(device.id, "status", payload);
90
+ $.db_device.addLog(device.id,"status",payload);
91
+ }
92
+ break;
93
+ case "model":
94
+ let res = await $.db_model.getByName(payload);
95
+ let model_id = res?.id;
96
+ if (model_id != null && device?.tech != payload) {
97
+ await $.db_device.update(device.id, "model_id", model_id);
98
+ $.db_device.addLog(device.id,"model_id",model_id);
99
+ }
100
+ break;
101
+ case "tech":
102
+ if (device?.tech && payload != device.tech) {
103
+ await $.db_device.update(device.id, "tech", payload);
104
+ $.db_device.addLog(device.id,"tech",payload);
105
+ }
106
+ break;
107
+ case "version":
108
+ if (device?.version && payload != device.version) {
109
+ $.db_device.addLog(device.id,"version",payload);
110
+ await $.db_device.update(device.id, "version", payload);
111
+ await self.handleFotaSuccess(device.id);
112
+ }
113
+ break;
114
+ case "app_version":
115
+ if (device?.app_version && payload != device.app_version) {
116
+ $.db_device.addLog(device.id,"app_version",payload);
117
+ await $.db_device.update(device.id, "app_version", payload);
118
+ await self.handleFotaSuccess(device.id);
119
+ }
120
+ break;
121
+ case "fw/fota/update/status":
122
+ await self.handleFotaError(device.id, payload);
123
+ break;
124
+ // Optional: default case if needed
125
+ default:
126
+ // handle other topics or do nothing
127
+ break;
128
+ }
129
+
130
+ if(topic.startsWith("settings/") && topic.endsWith("/set") && payload != "" ){
131
+ // update local settings
132
+ let index = topic.indexOf("settings/");
133
+ topic = topic.substring(index+9);
134
+ self.updateLocalSettings(device,topic,payload);
135
+ }else if(topic.startsWith("settings/") && !topic.endsWith("/set") && payload != "" ){
136
+ // store remote device settings
137
+ let index = topic.indexOf("settings/");
138
+ topic = topic.substring(index+9);
139
+ self.updateRemoteSettings(device,topic,payload);
140
+ }else if(topic == "fw"){
141
+ try{
142
+ payload = JSON.parse(payload);
143
+ }catch(error){}
144
+ if(typeof payload === 'object' && payload !== null){
145
+ try{
146
+ $.db_data.updateJson("fw",device.id,payload);
147
+ $.db_data.addJsonLog("logs_fw",device.id,payload);
148
+ }catch(error){
149
+ console.error(error)
150
+ }
151
+ }else if(typeof payload !== 'object' && payload !== null){
152
+ // change it to topic.startsWith("fw")
153
+ try{
154
+ const column = getWordAfterLastSlash(topic);
155
+ $.db_data.update("fw",device.id,column,payload);
156
+ $.db_data.addLog("logs_fw",device.id,column,payload);
157
+ }catch(error){
158
+ console.error(error)
159
+ }
160
+ }
161
+ }else if(topic.startsWith("sensor/")){
162
+ updateSensor(device,topic,paylaod)
163
+ index = topic.indexOf("/");
164
+ if(index == -1)
165
+ return;
166
+
167
+ const name = topic.substring(index+1);
168
+ let res = await $.db_sensor.getByRef(topic)
169
+ if(res != null){
170
+ await $.db_sensor.insert(logs_table,device.id,res.id,payload);
171
+ }
172
+ }
173
+
174
+ if(_project[project_name]){
175
+ _project[project_name]?.module?.parseMessage(client,project_name,device,topic,payload,retain,()=>{});
176
+ }
177
+ }
178
+
179
+ },
180
+
35
181
  deleteLogs : async()=>{
36
182
 
37
183
  let tables = await $.db.getTables();
@@ -48,193 +194,269 @@ var self = module.exports = {
48
194
  console.log(`Logs of table ${tableName} deleted in ${time}s`);
49
195
  }
50
196
  }
51
-
52
197
  },
53
198
 
54
- checkFota : async ()=>{
199
+ updateLocalSettings: async (device, topic, payload) => {
55
200
 
56
- const devices = await $.db_device.listOnline();
201
+ $.db_device.addLog(device.id,"local_settings",JSON.stringify(payload));
57
202
 
58
- const batchSize = 10;
59
- for (let i = 0; i < devices.length; i += batchSize) {
60
- const batch = devices.slice(i, i + batchSize);
61
- await Promise.all(batch.map(async (device) => {
203
+ let route = topic.split("/");
62
204
 
63
- if (device.release == null || device.release === "critical")
64
- {
65
- // don't update
66
- return;
67
- }
205
+ if (route == null || route.length == 0) {
206
+ console.warn("updateLocalSettings: topic invalid:", topic);
207
+ return;
208
+ }
68
209
 
69
- if (device.fota_tries > 3)
70
- {
71
- return;
72
- }
210
+ // Retrieve existing settings
211
+ let settings = await $.db_device.getLocalSettings(device.id);
73
212
 
74
- let new_firmware = await $.db_firmware.getLatestFWVersion(device.model_id,device.release);
75
- let new_app = await $.db_firmware.getLatestAppVersion(device.model_id,device.release);
76
- if(new_firmware == null || new_app == null)
77
- return;
78
-
79
- let project_name = "";
80
- let res = await $.db_project.getById(device.project_id);
81
- if(res != null)
82
- project_name = res.name;
83
- else
84
- return;
85
-
86
- let mqtt_prefix = `${project_name}/${device.uid}`;
87
- if(new_app != null && semver.lt(device.app_version, new_app.app_version))
88
- {
89
- console.log(`updating firmware of ${device.uid} to minor version ${new_app.app_version}`);
90
- let link = `${$.config.web.protocol}${$.config.web.domain}${$.config.web.fw_path}${new_app.filename}/download?token=${new_app.token}`;
91
- client.publish(mqtt_prefix+"/fw/fota/update/set",`{"url":"${link}"}`,{qos:2,retain:false});
92
- await $.db_data.update(project_name,device.id,"fota_tries",++device.fota_tries);
93
- }else{
94
- if(new_firmware != null && semver.lt(device.version, new_firmware.fw_version))
95
- {
96
- console.log(`updating firmware of ${device.uid} to major version ${new_firmware.fw_version}`);
97
- let link = `${$.config.web.protocol}${$.config.web.domain}${$.config.web.fw_path}${new_firmware.filename}/download?token=${new_firmware.token}`;
98
- $.mqtt_client.publish(mqtt_prefix+"/fw/fota/update/set",`{"url":"${link}"}`,{qos:2,retain:false});
99
- await $.db_data.update(project_name,device.id,"fota_tries",++device.fota_tries);
100
- }
101
- }
102
- }));
213
+ if (!settings || typeof settings !== 'object') {
214
+ settings = {};
215
+ }
103
216
 
104
- // Wait for 1 minute after processing each batch
105
- if (i + batchSize < devices.length) {
106
- await new Promise(resolve => setTimeout(resolve, 60000));
217
+ let obj = settings;
218
+
219
+ // Traverse route parts to reach the target object
220
+ route.slice(0, -1).forEach(property => {
221
+ if (
222
+ !Object.prototype.hasOwnProperty.call(obj, property) ||
223
+ (obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
224
+ obj[property] === null
225
+ ) {
226
+ obj[property] = {}; // Create nested object if missing or not an object
107
227
  }
228
+ obj = obj[property];
229
+ });
230
+
231
+ // Parse the payload JSON
232
+ let data = {};
233
+ try {
234
+ data = JSON.parse(payload);
235
+ } catch (error) {
236
+ console.error("Failed to parse payload JSON:", error);
237
+ return;
238
+ }
239
+
240
+ // Check if the data is a plain object, then merge
241
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
242
+ Object.assign(obj, data);
243
+ } else {
244
+ console.warn("Payload is not a valid object:", payload);
245
+ return;
108
246
  }
109
247
 
248
+ try {
249
+ // Update the settings in the database
250
+ await $.db_device.updateLocalSettings(JSON.stringify(settings), device.id);
251
+ } catch (err) {
252
+ console.error("Failed to update local settings:", err);
253
+ }
110
254
  },
111
255
 
112
- parseMessage : async (client,topic,payload,retain)=>{
256
+ updateRemoteSettings : async (device,topic,payload)=>{
113
257
 
114
- let topic_bck = topic;
258
+ $.db_device.addLog(device.id,"remote_settings",JSON.stringify(payload));
115
259
 
116
- //let index = topic.indexOf(MACRO_UID_PREFIX);
117
- // --- project ---
118
- let index = topic.indexOf("/");
119
- if(index == -1)
120
- return;
121
- let project_name = topic.substring(0,index);
260
+ let route = topic.split("/");
122
261
 
123
- // --- uid ---
124
- topic = topic.substring(index+1);
125
- index = topic.indexOf("/");
126
- if(index == -1)
127
- return;
128
- let uid = topic.substring(0,index);
262
+ if(route == null){
263
+ console.warn("updateRemoteSettings: topic invalid:",topic);
264
+ return;
265
+ }
266
+ // Parse existing settings
267
+ let settings = await $.db_device.getRemoteSettings(device.id);
129
268
 
130
- topic = topic.substring(index+1);
269
+ if (!settings || typeof settings !== 'object') {
270
+ settings = {};
271
+ }
131
272
 
132
- // check if topic corresponds to a device
133
- if(uid.startsWith(MACRO_UID_PREFIX)){
134
- let device = await $.db_device.get(uid);
273
+ let obj = settings;
135
274
 
136
- if(device == null){
137
- $.db_device.insert(uid)
138
- .then(async()=>{
139
- device = await $.db_device.get(uid);
140
- })
141
- .catch( (err) => {});
275
+ // Traverse route parts
276
+ route.slice(0, route.length).forEach(property => {
277
+ if (
278
+ !Object.prototype.hasOwnProperty.call(obj, property) ||
279
+ (obj.hasOwnProperty(property) && typeof obj[property] !== 'object') ||
280
+ obj[property] === null
281
+ ) {
282
+ obj[property] = {}; // create nested object if missing or not an object
142
283
  }
284
+ obj = obj[property];
285
+ });
143
286
 
144
- // update project id if needed
145
- let res = await $.db_project.getByName(project_name);
146
- let project_id = res?.id;
147
- if(project_id != null && project_id != device?.project_id)
148
- $.db_device.update(device.id,"project_id",project_id);
287
+ // Merge payload into nested object
288
+ let data = {};
289
+ try {
290
+ data = JSON.parse(payload);
291
+ } catch(error) {
292
+ }
149
293
 
150
- if(topic === "status"){
151
- if(payload == "online" || payload == "offline")
152
- await $.db_device.update(device.id,"status",payload);
153
- }else if(topic === "model"){
154
- let res = await $.db_model.getByName(payload);
155
- let model_id = res?.id;
156
-
157
- if(model_id != null){
158
- let res = await $.db_device.update(device.id,"model_id",model_id);
159
- }
160
- }else if(topic === "tech"){
161
- await $.db_device.update(device.id,"tech",payload);
162
- }else if(topic === "version"){
163
- // Update version and app_version on device table..
164
- if(payload != device.version){
165
- await $.db_device.update(device.id,"version",payload);
166
- await $.db_device.update(device.id,"fota_tries",1); // ! value 0 doesn't work..
167
- }
168
- }else if(topic === "app_version"){
169
- if(payload != device.app_version){
170
- await $.db_device.update(device.id,"app_version",payload);
171
- await $.db_device.update(device.id,"fota_tries",1); // ! value 0 doesn't work..
172
- }
294
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
295
+ // Merge data properties
296
+ for (const [key, value] of Object.entries(data)) {
297
+ obj[key] = value;
173
298
  }
299
+ } else {
300
+ // payload isn't an object, replace the nested object
301
+ obj = payload;
302
+ }
174
303
 
175
- if(device.project_id != null && device.model_id != null){
176
- let res = await $.db_sensor.getByRef(topic)
177
- if(res != null){
178
- await $.db_sensor.insert(logs_table,device.id,res.id,payload);
304
+ try {
305
+ await $.db_device.updateRemoteSettings(JSON.stringify(settings), device.id);
306
+ } catch (err) {
307
+ console.error("Failed to update local settings:", err);
308
+ }
309
+ },
310
+
311
+ checkFota : async (release = "dev")=>{
312
+
313
+ const models = await $.db_model.getAll();
314
+
315
+ for (const model of models) {
316
+
317
+ if(!model?.id)
318
+ continue;
319
+
320
+ try{
321
+ const latestVersion = await $.db_firmware.getLatestVersion(model.id,release);
322
+ const latestAppVersion = await $.db_firmware.getLatestAppVersion(model.id,release);
323
+
324
+ const devices = await $.db_device.listByModel(model.id);
325
+
326
+ if(devices == null)
327
+ continue;
328
+
329
+ for (const device of devices) {
330
+
331
+ if(device?.accept_release != release){
332
+ continue;
333
+ }
334
+
335
+ let obj = null;
336
+ // insert filename on fota table for this device or update.
337
+ if(device?.app_version != latestAppVersion.app_version){
338
+ obj = {
339
+ model_id : device.model_id,
340
+ target_version : latestVersion.version,
341
+ target_app_version : latestAppVersion.app_version,
342
+ target_release : device.accept_release,
343
+ firmware_id : latestAppVersion.id,
344
+ nAttempts : 0,
345
+ fUpdate : true,
346
+ };
347
+ }else if(latestVersion?.version && (device?.version != latestVersion.version)){
348
+ obj = {
349
+ model_id : device.model_id,
350
+ target_version : latestVersion.version,
351
+ target_app_version : latestAppVersion.app_version,
352
+ target_release : device.accept_release,
353
+ firmware_id : latestVersion.id,
354
+ nAttempts : 0,
355
+ fUpdate : true,
356
+ };
357
+ }
358
+ if(obj != null){
359
+ try{
360
+ let res = await $.db_fota.getEntry(device.id,obj);
361
+ if(res == null)
362
+ $.db_fota.update(device.id,obj);
363
+ }catch(error){
364
+ console.error(error)
365
+ }
366
+ }
179
367
  }
368
+ }catch(error){
369
+ console.error(error);
370
+ continue;
180
371
  }
372
+ }
373
+ },
181
374
 
182
- // store remote device settings on project table - twin model
183
- if(topic.endsWith("/set") && payload != "" ){
375
+ triggerFota : async (release = "dev")=>{
184
376
 
185
- let route = topic.split("/");
377
+ const devices = await $.db_fota.getUpdatable(release);
186
378
 
187
- if(route == null){
188
- console.log("topic invalid:",topic);
189
- return;
190
- }
191
- // get settings
192
- let settings = await $.db_project.getSettings(project_name,device.id);
193
- let obj = settings;
379
+ const batchSize = 10;
380
+ for (let i = 0; i < devices.length; i += batchSize) {
381
+ const batch = devices.slice(i, i + batchSize);
382
+ await Promise.all(batch.map(async (device) => {
194
383
 
195
- if(obj == null){
196
- console.log("no settings available for deviceId:",device.id)
197
- obj = {};
198
- }
199
- // Traverse through all route parts except the last one
200
- route.slice(0, -1).forEach(property => {
201
- if (!obj.hasOwnProperty(property) ||
202
- ( obj.hasOwnProperty(property) && typeof obj[property] !== 'object'))
203
- {
204
- obj[property] = {}; // create nested object if missing or not an object
384
+ const firmware = await $.db_firmware.getById(device?.firmware_id)
385
+ if(firmware != null){
386
+ const project = await $.db_project.getById(device.project_id);
387
+ const project_name = project?.name;
388
+ const model = await $.db_model.getById(firmware.model_id);
389
+ const model_name = model?.name;
390
+ let topic = "";
391
+ if(model_name == "sniffer"){
392
+ resolve();
393
+ // !! This cannot be handled on this way
394
+
395
+ // get sniffer info associated to device
396
+ const sniffer = await $.db_data.getAssociatedToDevice("sniffer",device.id);
397
+ if(sniffer == null) return;
398
+ // get device_uid from devices table
399
+ const gw = await $.db_device.getById(sniffer?.id)
400
+ if(gw == null) return;
401
+ let mqtt_prefix = `${project_name}/${gw.uid}`;
402
+ if (semver.gt(sniffer.version, "2.0.1"))
403
+ topic = mqtt_prefix+`/app/sniffer/${sniffer.uid}/fota/update/set`;
404
+ else
405
+ topic = mqtt_prefix+`/app/sniffer/fota/update/set`;
205
406
  }
206
- obj = obj[property]; // go deeper
207
- })
208
-
209
- let data = {};
210
- try{
211
- data = JSON.parse(payload)
212
- }catch(error){}
213
-
214
- if (true || (data && typeof payload === 'object' && !Array.isArray(payload) ) ) {
215
- // Assuming payload is an object with properties to process
216
- for (const [key, value] of Object.entries(data)) {
217
- obj[key] = value; // go deeper
218
- };
219
- }else{
220
- obj = payload
221
- }
407
+ else{
408
+ let mqtt_prefix = `${project_name}/${device.uid}`;
409
+ topic = mqtt_prefix+"/fw/fota/update/set";
410
+ }
411
+ let link = `${$.config.web.protocol}${$.config.web.domain}${$.config.web.fw_path}${firmware?.filename}/download?token=${firmware?.token}`;
412
+ console.log(`Requesting firmware update of ${device.uid} to ${firmware?.filename}`);
413
+ $.mqtt_client.publish(topic,`{"url":"${link}"}`,{qos:1,retain:false});
222
414
 
223
- // update on device fw settings
224
- try{
225
- await $.db_project.updateSettings(project_name,JSON.stringify(settings),device.id);
226
- }catch(error){
227
- console.log(error);
415
+ let obj = {
416
+ nAttempts : ++device.nAttempts
417
+ }
418
+
419
+ await $.db_fota.update(device.id,obj);
420
+
421
+ obj = {
422
+ device_id : device.id,
423
+ local_version : device.version,
424
+ local_app_version : device.app_version,
425
+ target_version : firmware.version,
426
+ target_app_version : firmware.app_version,
427
+ target_file : firmware.filename,
428
+ nAttempt : device.nAttempts
429
+ }
430
+ await $.db_fota.newLog(device.id,obj);
228
431
  }
229
- }
432
+
433
+ }));
230
434
 
231
- if(project[project_name]){
232
- project[project_name].module.parseMessage(client,project_name,device,topic,payload,retain,()=>{});
435
+ // Wait for 1 minute after processing each batch
436
+ if (i + batchSize < devices.length) {
437
+ await new Promise(resolve => setTimeout(resolve, 60000));
233
438
  }
234
439
  }
440
+ },
235
441
 
442
+ handleFotaSuccess : async (deviceId) => {
443
+ let object = {
444
+ "nAttempts" : 0,
445
+ "fUpdate" : 0,
446
+ }
447
+ await $.db_fota.update(deviceId,object);
448
+ object = {
449
+ success : 1,
450
+ }
451
+ await $.db_fota.updateLog(deviceId,obj);
236
452
  },
237
453
 
454
+ handleFotaError : async (deviceId, error) => {
455
+ let object = {
456
+ error : error,
457
+ }
458
+ await $.db_fota.updateLog(deviceId,obj);
459
+ }
238
460
  }
239
461
 
240
462