mqtt-devices-parser 1.0.0

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 ADDED
@@ -0,0 +1,3 @@
1
+
2
+ # 1.0.0
3
+ - Module to parse mqtt messages from devices
package/Readme.md ADDED
@@ -0,0 +1,77 @@
1
+
2
+ # mqtt-devices-parser
3
+
4
+ MQTT parser
5
+ Parses topics received from devices and updates data on dB
6
+
7
+ ## Setup
8
+
9
+ ### Command
10
+ >> node deploy
11
+
12
+ 1. build/sync all tables inside models folder
13
+ 2. registers users and clients if not registered already
14
+
15
+ 3. build/sync all tables inside projects folder
16
+ 4. registers projects and models/apps if not registered already
17
+
18
+ ## Working mode
19
+
20
+ ### Command
21
+ >> node index
22
+
23
+ 1. Checks all projects inside src/projects folder and enable it if is activated in config/index.js
24
+
25
+ 2. Connects to defined MQTT broker
26
+
27
+ 3. Parse MQTT messages starting with :project/MACRO_UID_PREFIX+uid/
28
+
29
+ 3.1 register device if not exists
30
+ 3.2 Associate to a project if project is registered
31
+ 3.3 Associate to a model/app if model is registered
32
+
33
+ 4. Call project parseMessage function
34
+
35
+ ## Instructions
36
+
37
+ Projects folders must be kept
38
+
39
+ The following functions must exist inside a file with the same name of the project
40
+
41
+ ```
42
+ module.exports = {
43
+ sync_db : async()=>{
44
+ },
45
+ init : async ()=>{
46
+ },
47
+ parseMessage : async (client, project, device, topic, payload, retain, cb)=>{
48
+ },
49
+ }
50
+ ```
51
+
52
+ Ex: If project name is freeRTOS2, so must exist a file in projects/freeRTOS2/freeRTOS2.js.
53
+
54
+ Calling the following methods will write data in a ${table} with the name of the project, and also
55
+ in other table called "logs_"+${table}. Data will be written if there is a field in the respective table
56
+ with the same name as the name in topic.
57
+
58
+ ```
59
+ $.db_data.update(project,device?.id,topic,payload);
60
+ $.db_data.addLog("logs_"+project,device.id,topic,payload);
61
+ ```
62
+
63
+ ## UnitTest
64
+
65
+
66
+ ## Dependencies
67
+
68
+ - mysql8.0
69
+ - MQTT
70
+
71
+ ## Front end
72
+ - Node service running the following [project](https://github.com/zimbora/mgmt-iot-web)
73
+
74
+ ## Configuration
75
+
76
+ The default configuration uses ./config/index.js file\
77
+ In your program set the same struct and pass it as argument on the module init call.
@@ -0,0 +1,29 @@
1
+ module.exports = {
2
+ version : "0.0.0",
3
+ dev : process.env.dev || "true",
4
+ web:{
5
+ protocol : process.env.HTTP_PROTOCOL || "http://",
6
+ domain: process.env.DOMAIN || '192.168.1.1',
7
+ },
8
+ mqtt: {
9
+ protocol:process.env.MQTT_PROTOCOL || 'MQTT',
10
+ host:process.env.MQTT_HOST || 'localhost',
11
+ port:process.env.MQTT_PORT || '1883',
12
+ user:process.env.MQTT_USER || 'admin',
13
+ pwd:process.env.MQTT_PWD || 'admin',
14
+ client:process.env.MQTT_CLIENT || 'mqtt-devices-parser',
15
+ logs_path: process.env.MQTT_LOGS || 'uServices'
16
+ },
17
+ mysqldb: {
18
+ conn_limit: process.env.DB_CONN_LIMIT || 15,
19
+ host: process.env.DB_HOST || 'localhost',
20
+ port: process.env.DB_PORT || 3306,
21
+ user: process.env.DB_USER || 'user',
22
+ pwd: process.env.DB_PWD || 'user_pwd',
23
+ name: process.env.DB_NAME || 'mqtt-aedes',
24
+ },
25
+ sync_main_tables : process.env.sync_main_tables || true,
26
+ projects: {
27
+ demo : process.env.demo || false,
28
+ }
29
+ }
package/deploy.js ADDED
@@ -0,0 +1,70 @@
1
+ const fs = require('fs').promises;
2
+
3
+ $ = {};
4
+ $.config = require('./config');
5
+ $.models = require('./models/models.js');
6
+ var projectsPath = './src/projects/';
7
+ var project = [];
8
+
9
+ async function readModelsInsideProjects(){
10
+
11
+ return new Promise( async (resolve,reject) => {
12
+
13
+ // load projects dbs
14
+ var files = await fs.readdir(projectsPath)
15
+
16
+ files = files.filter((file) => {
17
+ // Exclude any extension file
18
+ return (file.indexOf('.') == -1);
19
+ })
20
+
21
+ let counter_help = 0;
22
+ files.forEach( async(name,counter) => {
23
+ if($.config.projects[name]){
24
+ project[name] = {
25
+ module : null
26
+ };
27
+ project[name].module = require('./src/projects/'+name+'/'+name+".js")
28
+ await project[name].module.sync_db();
29
+ counter_help++;
30
+ if(files.length == counter_help) return resolve();
31
+ }else{
32
+ counter_help++;
33
+ if(files.length == counter_help) return resolve();
34
+ }
35
+
36
+ });
37
+
38
+ });
39
+
40
+ }
41
+
42
+
43
+ async function init(){
44
+
45
+ await $.models.init();
46
+ await $.models.connect();
47
+
48
+ //main tables
49
+ if($.config.sync_main_tables){
50
+ await $.models.load(__dirname+"/models");
51
+ await $.models.dropTableIndexes();
52
+ await $.models.sync();
53
+
54
+ let user = await $.models.insertUser("admin","admin",5); // ads user
55
+ //console.log(user);
56
+ let user_id = user.dataValues.id;
57
+ await $.models.insertUser("device","device",3);
58
+ await $.models.insertUser("client","client_pwd",3);
59
+ await $.models.insertClient("admin","admin",user_id); // ads client with credentials admin@admin
60
+ }
61
+
62
+ // project tables
63
+ await readModelsInsideProjects();
64
+ await $.models.dropTableIndexes();
65
+ await $.models.sync();
66
+
67
+ }
68
+
69
+
70
+ init();
package/index.js ADDED
@@ -0,0 +1,113 @@
1
+ const mqtt = require("mqtt");
2
+
3
+ $ = {};
4
+ $.md5 = require('md5');
5
+
6
+ $.config = require('./config');
7
+ $.models = require('./models/models.js');
8
+ $.device = require('./src/device/device.js');
9
+ $.db = require('./src/db/db');
10
+ $.db_data = require('./src/db/data');
11
+ $.db_project = require('./src/db/project');
12
+ $.db_device = require('./src/db/device');
13
+ $.db_model = require('./src/db/model');
14
+ $.db_firmware = require('./src/db/firmware');
15
+ $.db_sensor = require('./src/db/sensor');
16
+
17
+ const packageJson = require(__dirname+'/package.json');
18
+ const packageVersion = packageJson.version;
19
+
20
+ var projects = [];
21
+
22
+ var self = module.exports = {
23
+
24
+ init : async(_config, _projects)=>{
25
+
26
+ return new Promise(async (resolve,reject) => {
27
+
28
+ if(_config != null)
29
+ $.config = _config;
30
+ if(_projects != null)
31
+ projects = _projects;
32
+
33
+ await $.models.init();
34
+ await $.models.connect();
35
+
36
+ //auth.init();
37
+ await $.device.init(projects);
38
+
39
+ console.log("mqtt connecting..")
40
+ mqtt_connect();
41
+
42
+ return resolve();
43
+ })
44
+ }
45
+ }
46
+
47
+ function mqtt_connect(){
48
+
49
+ const mqtt_prefix = $.config.mqtt.logs_path+"/"+$.config.mqtt.client;
50
+ const client = mqtt.connect({
51
+ host : $.config.mqtt.host,
52
+ port : $.config.mqtt.port,
53
+ username : $.config.mqtt.user,
54
+ password : $.config.mqtt.pwd,
55
+ clientId : $.config.mqtt.client,
56
+ protocolId : $.config.mqtt.protocol,
57
+ will : {
58
+ topic: mqtt_prefix+"/status",
59
+ payload: "offline",
60
+ qos: 2,
61
+ retain: true
62
+ }
63
+ });
64
+
65
+ client.on("connect", () => {
66
+
67
+ client.publish(mqtt_prefix+"/status","online",{qos:2,retain:true});
68
+ client.publish(mqtt_prefix+"/version",packageVersion,{qos:2,retain:true});
69
+
70
+ projects.map( (project)=>{
71
+ client.subscribe(project+"/#", (err) => {});
72
+
73
+ for(let project in $.config.projects){
74
+ if($.config.projects[project])
75
+ client.publish(mqtt_prefix+"/"+project,"active");
76
+ else
77
+ client.publish(mqtt_prefix+"/"+project,"deactive");
78
+ };
79
+ })
80
+ console.log(`MQTT connected to: ${$.config.mqtt.host}:${$.config.mqtt.port}`);
81
+ projects.map( project=>{
82
+ console.log("subscribing project:",project);
83
+ client.subscribe(project+"/#")
84
+ })
85
+ });
86
+
87
+ client.on("message", (topic, payload, packet) => {
88
+ // payload is Buffer
89
+ $.device.parseMessage(client,topic.toString(),payload.toString(),packet.retain);
90
+ });
91
+
92
+ client.on("reconnect",()=>{
93
+ console.log("reconnect")
94
+ });
95
+
96
+ client.on("close",()=>{
97
+ console.log("close")
98
+ });
99
+
100
+ client.on("offline",()=>{
101
+ console.log("offline")
102
+ });
103
+
104
+ client.on("disconnect",(packet)=>{
105
+ console.log(packet)
106
+ })
107
+
108
+ client.on("error",(error)=>{
109
+ console.log(error)
110
+ })
111
+ }
112
+
113
+
@@ -0,0 +1,41 @@
1
+
2
+ module.exports = (sequelize,DataTypes)=>{
3
+ return sequelize.define("clients", {
4
+ user_id: {
5
+ type: DataTypes.INTEGER,
6
+ references : {
7
+ model: 'users',
8
+ key: 'id'
9
+ }
10
+ },
11
+ nick: {
12
+ type: DataTypes.STRING(64),
13
+ unique: true
14
+ },
15
+ token: {
16
+ type: DataTypes.STRING,
17
+ allowNull: true
18
+ },
19
+ api_token: {
20
+ type: DataTypes.STRING,
21
+ allowNull: true
22
+ },
23
+ gmail: {
24
+ type: DataTypes.STRING(64),
25
+ //unique: true,
26
+ allowNull: true
27
+ },
28
+ name: {
29
+ type: DataTypes.STRING,
30
+ allowNull: true
31
+ },
32
+ avatar: {
33
+ type: DataTypes.STRING,
34
+ allowNull: true
35
+ },
36
+ },
37
+ {
38
+ tableName: 'clients',
39
+ freezeTableName: true
40
+ })
41
+ }
@@ -0,0 +1,39 @@
1
+
2
+ module.exports = (sequelize,DataTypes)=>{
3
+ return sequelize.define("devices", {
4
+ uid: {
5
+ type: DataTypes.STRING,
6
+ unique: true
7
+ },
8
+ name: {
9
+ type: DataTypes.STRING,
10
+ allowNull: true
11
+ },
12
+ project: { // deprecated
13
+ type: DataTypes.STRING,
14
+ allowNull: true
15
+ },
16
+ status: {
17
+ type: DataTypes.STRING,
18
+ allowNull: true
19
+ },
20
+ project_id: {
21
+ type: DataTypes.INTEGER,
22
+ references: {
23
+ model: 'projects',
24
+ key: 'id'
25
+ }
26
+ },
27
+ model_id: {
28
+ type: DataTypes.INTEGER,
29
+ references: {
30
+ model: 'models',
31
+ key: 'id'
32
+ }
33
+ },
34
+ },
35
+ {
36
+ tableName: 'devices',
37
+ freezeTableName: true
38
+ })
39
+ }
@@ -0,0 +1,41 @@
1
+
2
+ module.exports = (sequelize,DataTypes)=>{
3
+ return sequelize.define("firmwares", {
4
+ filename: {
5
+ type: DataTypes.STRING,
6
+ allowNull: true
7
+ },
8
+ originalname: {
9
+ type: DataTypes.STRING,
10
+ allowNull: true,
11
+ unique: true
12
+ },
13
+ fw_version: {
14
+ type: DataTypes.STRING,
15
+ allowNull: true
16
+ },
17
+ app_version: {
18
+ type: DataTypes.STRING,
19
+ allowNull: true
20
+ },
21
+ fw_release: {
22
+ type: DataTypes.STRING,
23
+ allowNull: true
24
+ },
25
+ model_id: { // deprecated
26
+ type: DataTypes.INTEGER,
27
+ references: {
28
+ model: 'models',
29
+ key: 'id'
30
+ },
31
+ },
32
+ token: {
33
+ type: DataTypes.STRING,
34
+ allowNull: true
35
+ },
36
+ },
37
+ {
38
+ tableName: 'firmwares',
39
+ freezeTableName: true
40
+ })
41
+ }
@@ -0,0 +1,23 @@
1
+
2
+ module.exports = (sequelize,DataTypes)=>{
3
+ return sequelize.define("modelPermissions", {
4
+ client_id: {
5
+ type: DataTypes.INTEGER,
6
+ references: {
7
+ model: 'clients',
8
+ key: 'id'
9
+ }
10
+ },
11
+ model_id: {
12
+ type: DataTypes.INTEGER,
13
+ references: {
14
+ model: 'models',
15
+ key: 'id'
16
+ }
17
+ }
18
+ },
19
+ {
20
+ tableName: 'modelPermissions',
21
+ freezeTableName: true
22
+ })
23
+ }
@@ -0,0 +1,226 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const {Sequelize, DataTypes} = require("sequelize");
4
+ var SqlString = require('sequelize/lib/sql-string')
5
+ var mysql = require('mysql2');
6
+ var sequelize;
7
+
8
+ //var modelsPath = './models/';
9
+ var config = require('../config');
10
+
11
+ var self = module.exports = {
12
+
13
+ init : async ()=>{
14
+
15
+ return new Promise((resolve,reject) => {
16
+ sequelize = new Sequelize(
17
+ config.mysqldb.name,
18
+ config.mysqldb.user,
19
+ config.mysqldb.pwd,
20
+ {
21
+ host: config.mysqldb.host,
22
+ dialect: 'mysql'
23
+ },
24
+ );
25
+ return resolve(sequelize);
26
+ })
27
+ },
28
+
29
+ connect : async ()=>{
30
+
31
+ return new Promise((resolve,reject) => {
32
+ sequelize.authenticate().then(() => {
33
+ console.log('Connection has been established successfully.');
34
+ return resolve();
35
+ }).catch((error) => {
36
+ console.error('Unable to connect to the database: ', error);
37
+ return reject();
38
+ });
39
+ });
40
+ },
41
+
42
+ load : async (modelsPath)=>{
43
+
44
+ return new Promise(async(resolve,reject) => {
45
+ var files = await fs.readdir(modelsPath)
46
+ files = files.filter((file) => {
47
+ // Exclude models.js and any non-js files
48
+ return (file.indexOf('.') !== 0) && (file !== 'models.js') && (file.slice(-9) === 'models.js');
49
+ })
50
+ files.forEach((file,counter) => {
51
+ const model = require(path.join(modelsPath, file));
52
+ model(sequelize, DataTypes);
53
+ if(counter == files.length-1){
54
+ return resolve();
55
+ }
56
+ });
57
+ });
58
+ },
59
+
60
+ sync : async ()=>{
61
+
62
+ return new Promise((resolve,reject) => {
63
+ //sequelize.sync({alter:true,force:true}).then(() => {
64
+ sequelize.sync({alter:true}).then( async () => {
65
+ console.log('All tables were synced!');
66
+ return resolve();
67
+ }).catch((error) => {
68
+ console.error('Unable to create table : ', error);
69
+ return reject();
70
+ });
71
+ });
72
+ },
73
+
74
+ dropTableIndexes : async()=>{
75
+
76
+ return new Promise((resolve,reject) => {
77
+ let tables = Object.keys(sequelize.models);
78
+
79
+ console.log(tables);
80
+ if(!tables?.length)
81
+ return resolve();
82
+
83
+ tables.forEach(async(tableName,counter)=>{
84
+ self.dropAllIndexes(tableName)
85
+ .then(res =>{
86
+ if(counter == tables.length-1)
87
+ return resolve();
88
+ })
89
+ .catch(error => {
90
+ if(counter == tables.length-1)
91
+ return resolve();
92
+ });
93
+
94
+ });
95
+ });
96
+ },
97
+
98
+ dropAllIndexes : async (tableName)=>{
99
+
100
+ return new Promise( async(resolve,reject) => {
101
+ // Fetch all indexes from the table
102
+ let query = `SHOW INDEXES FROM ??`
103
+ query = mysql.format(query,[tableName]);
104
+
105
+ var indexes;
106
+ sequelize.query(query)
107
+ .then(res=>{
108
+ indexes = res;
109
+
110
+ // Filter out primary key index and unique indexes if you don't want to drop them
111
+ const filteredIndexes = indexes[0].filter(index => index.Key_name !== 'PRIMARY' && !index.Non_unique);
112
+
113
+ if(!filteredIndexes?.length)
114
+ return resolve();
115
+
116
+ // Drop each index
117
+ filteredIndexes.forEach( async(index,counter)=>{
118
+ let query = `DROP INDEX ?? ON ??`
119
+ query = mysql.format(query,[index.Key_name,tableName]);
120
+ sequelize.query(query)
121
+ .then(res=>{
122
+ console.log(`Dropped index ${index.Key_name}`);
123
+ if(counter == filteredIndexes.length-1)
124
+ return resolve();
125
+ })
126
+ .catch(error=>{
127
+ if(counter == filteredIndexes.length-1)
128
+ return resolve();
129
+ })
130
+ })
131
+ })
132
+ .catch(error=>{
133
+ return resolve();
134
+ })
135
+ });
136
+ },
137
+
138
+ get: (modelName)=>{
139
+
140
+ if(modelName != ""){
141
+ if(sequelize.models[modelName]){
142
+ const model = sequelize.models[modelName];
143
+ const keys = Object.keys(model.rawAttributes);
144
+ let obj = {}
145
+ keys.map((key)=>{
146
+ obj[key] = null;
147
+ })
148
+ return obj;
149
+ }else return;
150
+ }else{
151
+ return models = Object.keys(sequelize.models).map(modelName => {
152
+ const model = sequelize.models[modelName];
153
+ const keys = Object.keys(model.rawAttributes);
154
+ return {
155
+ modelName,
156
+ keys
157
+ };
158
+ });
159
+ }
160
+ },
161
+
162
+ getUser : async (type)=>{
163
+
164
+ return new Promise( async (resolve,reject) => {
165
+ const user = await sequelize.models['users'].findOne({
166
+ where: {
167
+ type: type
168
+ }
169
+ });
170
+ return resolve(user);
171
+ });
172
+ },
173
+
174
+ insertUser: async (type, password,level)=>{
175
+
176
+ return new Promise( async (resolve,reject) => {
177
+ // Insert a record if it doesn't exist
178
+ const res = await sequelize.models['users'].findOrCreate({
179
+ where: { type: type },
180
+ defaults: {
181
+ password: password,
182
+ level: level
183
+ }
184
+ })
185
+ .then(([user, created]) => {
186
+ if (created) {
187
+ console.log('New user created:', type);
188
+ } else {
189
+ console.log('User already exists:', type);
190
+ }
191
+ return resolve(user);
192
+ })
193
+ .catch(error => {
194
+ console.error('Error:', error);
195
+ return reject(error);
196
+ });
197
+ });
198
+ },
199
+
200
+ insertClient: async (nick, token, user_id)=>{
201
+
202
+ return new Promise( async (resolve,reject) => {
203
+ // Insert a record if it doesn't exist
204
+ const res = await sequelize.models['clients'].findOrCreate({
205
+ where: { nick: nick },
206
+ defaults: {
207
+ user_id: user_id,
208
+ token: token
209
+ }
210
+ })
211
+ .then(([client, created]) => {
212
+ if (created) {
213
+ console.log('New client created:', nick);
214
+ } else {
215
+ console.log('Client already exists:', nick);
216
+ }
217
+ return resolve(client);
218
+ })
219
+ .catch(error => {
220
+ console.error('Error:', error);
221
+ });
222
+ });
223
+ }
224
+
225
+ }
226
+
@@ -0,0 +1,40 @@
1
+
2
+ module.exports = (sequelize,DataTypes)=>{
3
+ return sequelize.define("models", {
4
+ name: {
5
+ type: DataTypes.STRING,
6
+ unique: true
7
+ },
8
+ description: {
9
+ type: DataTypes.STRING
10
+ },
11
+ model_table: { // deprecated
12
+ type: DataTypes.STRING,
13
+ allowNull: true
14
+ },
15
+ logs_table: { // deprecated
16
+ type: DataTypes.STRING,
17
+ allowNull: true
18
+ },
19
+ fw_enabled: {
20
+ type: DataTypes.BOOLEAN,
21
+ defaultValue: false
22
+ },
23
+ ar_enabled: {
24
+ type: DataTypes.BOOLEAN,
25
+ defaultValue: false
26
+ },
27
+ alarms_enabled: {
28
+ type: DataTypes.BOOLEAN,
29
+ defaultValue: false
30
+ },
31
+ js_code_enabled: {
32
+ type: DataTypes.BOOLEAN,
33
+ defaultValue: false
34
+ }
35
+ },
36
+ {
37
+ tableName: 'models',
38
+ freezeTableName: true
39
+ })
40
+ }