2018-02-28 20:24:45 -08:00
import { connect } from 'async-mqtt'
import merge from 'lodash.merge'
import btc from 'better-try-catch'
import union from 'lodash.union'
import xor from 'lodash.xor'
2019-02-14 14:01:11 -08:00
import logger from '@uci-utils/logger'
2018-05-24 12:31:47 -07:00
import isPlainObject from 'is-plain-object'
2018-02-28 20:24:45 -08:00
let log = { }
2019-01-01 19:20:51 -08:00
/ * *
* MQTT - Client - An mqtt client that supports UCI packets
* it extends the { @ link https : //github.com/mqttjs/async-mqtt| async version this mqtt client } albiet in a clunky way with a private extend method because it was not written as an extendable class
* @ extends MQTT . js
* /
class MQTTClient {
/ * *
* constructor - Description
*
* @ param { object } [ opts = { } ] Description
*
* /
constructor ( opts = { } ) {
this . id = opts . id || opts . name || 'mqtt:' + new Date ( ) . getTime ( )
log = logger ( {
file : 'src/client.js' ,
class : 'Client' ,
name : 'mqtt' ,
id : this . id
} )
2018-05-24 12:31:47 -07:00
this . url = opts . url || ''
2018-02-28 20:24:45 -08:00
// subscription topics can be string of commna delimited or array of strings see object see mqtt.js docs
2019-01-01 19:20:51 -08:00
this . topics = Array . isArray ( opts . topics )
? opts . topics
: opts . topics
? opts . topics . split ( ',' )
: [ this . id ]
this . opts = opts . connect || { } // see options for new mqtt.Client
2018-02-28 20:24:45 -08:00
// self bindings
this . connect = this . connect . bind ( this )
2018-05-24 12:31:47 -07:00
this . push = this . send
2018-02-28 20:24:45 -08:00
}
2019-01-01 19:20:51 -08:00
/ * *
* connect - Description
*
* @ returns { type } Description
* /
async connect ( ) {
return new Promise ( ( resolve , reject ) => {
let mqtt = connect (
this . url ,
this . opts
)
this . _extend ( mqtt , 'subscribe,unsubscribe' ) // merge mqtt client into class extend with given functions
2018-05-24 12:31:47 -07:00
let timeout = this . opts . connectTimeout || 5000
2019-01-01 19:20:51 -08:00
setTimeout ( ( ) => {
2019-02-14 14:01:11 -08:00
reject ( { msg : 'ending mqtt connection attempt, no broker' , url : this . url , opts : this . opts } )
2019-01-01 19:20:51 -08:00
} , timeout )
2018-02-28 20:24:45 -08:00
this . once ( 'connect' , ( ) => {
2018-05-24 12:31:47 -07:00
this . _log ( )
this . _listen ( )
2018-02-28 20:24:45 -08:00
2018-05-24 12:31:47 -07:00
this . on ( 'reconnect' , ( ) => {
2019-01-01 19:20:51 -08:00
log . info ( 'mqtt client reconnected to broker' )
2018-05-24 12:31:47 -07:00
this . subscribe ( this . topics )
} )
this . on ( 'error' , err => {
2019-01-01 19:20:51 -08:00
log . fatal ( { err : err } , 'connection error to broker' )
2018-05-24 12:31:47 -07:00
} )
this . subscribe ( this . topics )
2019-01-01 19:20:51 -08:00
log . info (
{ options : this . _client . options } ,
` mqtt client connected to broker at ${
this . _client . options . hostname
} : $ { this . _client . options . port } `
)
resolve (
` mqtt client connected to broker at ${
this . _client . options . hostname
} : $ { this . _client . options . port } `
)
2018-02-28 20:24:45 -08:00
} )
} ) //end promise
}
2019-01-01 19:20:51 -08:00
async subscribe ( topic , options = { } ) {
2018-02-28 20:24:45 -08:00
if ( options . pub ) {
2019-01-01 19:20:51 -08:00
if ( typeof topic === 'string' ) topic = topic . split ( ',' )
this . topics = union ( this . topics , topic )
2018-02-28 20:24:45 -08:00
}
2018-05-24 12:31:47 -07:00
log . info ( ` subscription for topic ${ topic } added ` )
2019-01-01 19:20:51 -08:00
this . _subscribe ( topic , options )
2018-02-28 20:24:45 -08:00
}
2019-01-01 19:20:51 -08:00
async unsubscribe ( topic , options = { } ) {
2018-02-28 20:24:45 -08:00
if ( ! options . pub ) {
2019-01-01 19:20:51 -08:00
if ( typeof topic === 'string' ) topic = topic . split ( ',' )
this . topics = xor ( this . topics , topic )
2018-02-28 20:24:45 -08:00
}
this . _unsubscribe ( topic )
}
2019-01-01 19:20:51 -08:00
async send ( packet , topics , options ) {
2019-02-14 14:01:11 -08:00
if ( ! this . hasOwnProperty ( 'publish' ) ) {
log . warn ( { url : this . url , opts : this . opts , msg : 'connect method never called, initialization needed, aborting send' } )
return
}
2019-01-01 19:20:51 -08:00
if ( typeof topics !== 'string' && ! Array . isArray ( topics ) ) {
2018-05-24 12:31:47 -07:00
options = topics
2018-02-28 20:24:45 -08:00
topics = this . topics
2019-01-01 19:20:51 -08:00
} else {
if ( typeof topics === 'string' ) topics = topics . split ( ',' )
}
2018-05-24 12:31:47 -07:00
let payload = { }
if ( typeof packet !== 'string' ) {
payload = this . _serialize ( packet )
2019-01-01 19:20:51 -08:00
if ( ! payload )
payload = this . _serialize ( { err : 'could not serialze packet' } )
2018-05-24 12:31:47 -07:00
topics = [ packet . cmd ] || topics // if payload has a cmd use that as topic
2019-01-01 19:20:51 -08:00
} else payload = this . _serialize ( { data : packet } )
2018-05-24 12:31:47 -07:00
let pubs = [ ]
2019-01-01 19:20:51 -08:00
topics . forEach ( async topic => {
2018-05-24 12:31:47 -07:00
log . info ( ` sending ${ payload } to topic ${ topic } with options ${ options } ` )
2019-01-01 19:20:51 -08:00
pubs . push ( this . publish ( topic , payload , options ) )
2018-05-24 12:31:47 -07:00
} )
return await Promise . all ( pubs )
2018-02-28 20:24:45 -08:00
}
2019-01-01 19:20:51 -08:00
registerPacketProcessor ( func ) {
2018-02-28 20:24:45 -08:00
this . _packetProcess = func
}
_serialize ( json ) {
2019-01-01 19:20:51 -08:00
let [ err , payload ] = btc ( JSON . stringify ) ( json )
2018-05-24 12:31:47 -07:00
if ( err ) {
log . warn ( ` error unable to stringify json: ${ json } ` )
2018-02-28 20:24:45 -08:00
return null
}
return payload
}
2019-01-01 19:20:51 -08:00
_listen ( ) {
log . info (
` listening for incoming packets from broker on topics ${ this . topics } `
)
this . on ( 'message' , messageProcess . bind ( this ) )
2018-02-28 20:24:45 -08:00
2019-01-01 19:20:51 -08:00
async function messageProcess ( topic , payload ) {
2018-05-24 12:31:47 -07:00
log . info ( 'incoming message on topic' , topic )
2018-02-28 20:24:45 -08:00
let packet = this . _handlePayload ( payload )
2019-01-01 19:20:51 -08:00
if ( packet ) packet . cmd = packet . cmd || topic
// payload had no command that use topic as cmd
else packet = { cmd : topic }
let res = ( await this . _packetProcess ( packet , topic ) ) || { }
2018-05-24 12:31:47 -07:00
this . send ( res )
2018-02-28 20:24:45 -08:00
}
}
2019-01-01 19:20:51 -08:00
_handlePayload ( payload ) {
let [ err , packet ] = btc ( JSON . parse ) ( payload . toString ( ) )
2018-02-28 20:24:45 -08:00
if ( err ) {
2018-05-24 12:31:47 -07:00
log . info ( 'payload not json returning as prop data:<payload>' )
2018-02-28 20:24:45 -08:00
}
2019-01-01 19:20:51 -08:00
if ( ! isPlainObject ( packet ) ) packet = { data : payload . toString ( ) }
2018-02-28 20:24:45 -08:00
return packet
}
// default packet process just a simple console logger. ignores any cmd: prop
2019-01-01 19:20:51 -08:00
_packetProcess ( packet , topic ) {
2018-02-28 20:24:45 -08:00
console . log ( '=========================' )
2019-01-01 19:20:51 -08:00
console . log (
'default consumer processor\npacket from broker on topic:' ,
topic
)
2018-02-28 20:24:45 -08:00
console . dir ( packet )
console . log ( packet . status )
console . log ( '========================' )
}
async _log ( ) {
2019-01-01 19:20:51 -08:00
log = logger ( {
file : 'src/client.js' ,
class : 'Client' ,
2019-02-14 14:01:11 -08:00
package : '@uci/mqtt' ,
2019-01-01 19:20:51 -08:00
id : this . id
} )
2018-02-28 20:24:45 -08:00
this . on ( 'close' , ( ) => {
log . info ( 'connection to broker was closed' )
} )
this . on ( 'offline' , ( ) => {
log . info ( 'this client has gone offline from broker' )
} )
this . on ( 'packetsend' , packet => {
2019-01-01 19:20:51 -08:00
log . info ( { packet : packet } , 'outgoing packet to mqtt broker' )
2018-02-28 20:24:45 -08:00
} )
this . on ( 'packetreceive' , packet => {
2019-01-01 19:20:51 -08:00
log . info ( { packet : packet } , 'incoming packet from mqtt broker' )
2018-02-28 20:24:45 -08:00
} )
}
2019-01-01 19:20:51 -08:00
_extend ( obj , funcs ) {
2018-02-28 20:24:45 -08:00
let temp = { }
funcs = funcs . split ( ',' )
funcs . forEach ( func => {
temp [ func ] = this [ func ]
} )
2019-01-01 19:20:51 -08:00
merge ( this , obj )
2018-02-28 20:24:45 -08:00
funcs . forEach ( func => {
2019-01-01 19:20:51 -08:00
this [ '_' + func ] = this [ func ]
2018-02-28 20:24:45 -08:00
this [ func ] = temp [ func ]
} )
}
2019-01-01 19:20:51 -08:00
} // end mqtt client class
2018-02-28 20:24:45 -08:00
2019-01-01 19:20:51 -08:00
export default MQTTClient