novus-component
Version:
Component framework for home automation
324 lines (272 loc) • 6.85 kB
JavaScript
'use strict';
import { EventEmitter } from 'events';
import * as mqtt from 'mqtt';
import { default as extend } from 'extend';
import { MemoryStore } from 'novus-component-store-memory';
import { Route } from './Route';
/**
* Default component options
**/
const DEFAULT_OPTIONS = {
subscribeWhileConnected: false
};
/**
* Component class
**/
export class Component extends EventEmitter {
/**
* Constructor
**/
constructor(componentId, options = {}) {
super();
this._componentId = componentId;
if(!options.clientId) options.clientId = componentId;
this._store = options.store ? options.store : new MemoryStore();
this._options = extend({}, DEFAULT_OPTIONS, options);
this._mqttClient = null;
this._routes = [];
}
/**
* Convenience method for getting settings variables
**/
get(key, def = null) {
return this._store.get(key, def);
}
/**
* Convenience method for setting settings variables
**/
set(key, value) {
return this._store.set(key, value);
}
/**
* Returns true if the component is connected to an MQTT broker
**/
get connected() {
return this._mqttClient !== null && this._mqttClient.connected;
}
/**
* Returns the component's ID
**/
get componentId() {
return this._componentId;
}
/**
* Start the component and connect to the MQTT broker
**/
start(url = this._options.url || null) {
return new Promise((resolve, reject) => {
const onConnect = (connack) => {
this._mqttClient.connected = true;
this._mqttClient.removeAllListeners();
this._attachListeners()
.then(() => {
if(connack.sessionPresent) return true;
return this._subscribeToRoutes();
})
.then(() => {
return resolve(connack);
})
.catch((err) => {
return reject(err);
});
};
const onError = (err) => {
this._mqttClient.removeAllListeners();
this._mqttClient = null;
return reject(err);
};
this._mqttClient = (url !== null) ? mqtt.connect(url, this._options) : mqtt.connect(this._options);
this._mqttClient.once('connect', onConnect);
this._mqttClient.once('error', onError);
});
}
/**
* Subscribe to one or more topics
**/
subscribe(topic, options = {}) {
return new Promise((resolve, reject) => {
if(!this.connected) {
return reject(new Error('not connected to broker'));
}
options = extend({
qos: 0
}, options);
topic = this._normalizeTopic(topic);
this._mqttClient.subscribe(topic, options, (err, granted) => {
if(err) return reject(err);
return resolve(granted);
});
});
}
/**
* Unsubscribe from one or more topics
**/
unsubscribe(topic, options = {}) {
return new Promise((resolve, reject) => {
if(!this.connected) {
return reject(new Error('not connected to broker'));
}
topic = this._normalizeTopic(topic);
this._mqttClient.unsubscribe(topic, options, () => {
return resolve();
});
});
}
/**
* Publish a message on a topic
**/
publish(topic, message, options = {}) {
return new Promise((resolve, reject) => {
if(!this.connected) {
return reject(new Error('not connected to broker'));
}
options = extend({
qos: 0,
retain: false
}, options);
if(typeof message === 'object') {
if(message.toString) {
message = message.toString();
}
else {
message = JSON.stringify(message);
}
}
topic = this._normalizeTopic(topic);
this._mqttClient.publish(topic, message, options, () => {
return resolve();
});
});
}
/**
* End the connection to the MQTT broker
**/
end(force = false) {
return new Promise((resolve, reject) => {
if(!this.connected) {
return reject(new Error('not connected to broker'));
}
this._mqttClient.end(force, () => {
this._mqttClient.removeAllListeners();
this._mqttClient = null;
return resolve();
});
});
}
//
/**
* Add one or more new routes
**/
route(topic, handler = null, options = {}) {
if(typeof handler === 'function') {
topic = {
topic: topic,
handler: handler,
options: options
};
}
if(!Array.isArray(topic)) {
topic = [ topic ];
}
topic.forEach((item) => {
item.topic = this._normalizeTopic(item.topic);
this._routes.push(new Route(item, this));
if(!this.connected || this.connected && this._options.subscribeWhileConnected) {
this._subscribeToRoutes([ this._routes[this._routes.length-1] ]);
}
});
}
/**
* Attach MQTT listeners
**/
_attachListeners() {
return new Promise((resolve) => {
const onConnect = () => {
this._mqttClient.connected = true;
};
const onOffline = () => {
this._mqttClient.connected = false;
};
const onClose = () => {
onOffline();
};
const onError = (err) => {
// TODO: Better error handling
throw err;
};
const onMessage = (topic, message, packet) => {
for(let route of this._routes) {
let match = route.match(topic);
if(match) {
packet.params = match;
return route.execute(packet);
}
}
return this.emit('message', topic, message, packet);
};
this._mqttClient.on('connect', onConnect);
this._mqttClient.on('offline', onOffline);
this._mqttClient.on('close', onClose);
this._mqttClient.on('error', onError);
this._mqttClient.on('message', onMessage);
return resolve();
});
}
/**
* Subscribe to all registered routes
**/
_subscribeToRoutes(routes = this._routes) {
let i = 0;
let subscriptions = {};
for(let route of routes) {
if(!route.subscribe) continue;
if(subscriptions[route.topic.topic]) continue;
subscriptions[route.topic.topic] = route.qos;
i++;
}
return (i > 0) ? this.subscribe(subscriptions) : true;
}
/**
* Normalize topic names
**/
_normalizeTopic(topic) {
const normalize = (t) => {
t = t.replace('{$componentId}', this._componentId);
return t;
};
// Handle topic arrays
if(Array.isArray(topic)) {
return topic.map(normalize);
}
// Handle topic objects
if(typeof topic === 'object') {
let newObj = {};
for(let t in topic) {
newObj[normalize(t)] = topic[t];
}
return newObj;
}
// Handle strings
return normalize(topic);
}
/**
* Parse JSON or return original value
**/
_tryParseJSON(value) {
try {
value = JSON.parse(value);
}
catch(e) { }
return value;
}
/**
* Stringify JSON or return origial value
**/
_tryStringifyJSON(value) {
try {
value = JSON.stringify(value);
}
catch(e) { }
return value;
}
}