homebridge-mqtt
Version:
MQTT Plugin for Homebridge
337 lines (274 loc) • 11.8 kB
JavaScript
'use strict';
var mqtt = require('mqtt');
var Utils = require('./utils.js').Utils;
var fs = require('fs');
var plugin_name, topic_type, topic_prefix, Characteristic, api;
var addAccessory, addService, removeAccessory, removeService, setValue, getAccessories, getCharacteristic, updateReachability, setAccessoryInformation;
var set_timeout, client;
module.exports = {
Model: Model
}
function Model(params) {
this.config = params.config;
this.log = params.log;
plugin_name = params.plugin_name;
Characteristic = params.Characteristic;
api = params.api;
addAccessory = params.addAccessory;
addService = params.addService;
removeAccessory = params.removeAccessory;
removeService = params.removeService;
setValue = params.setValue;
getAccessories = params.getAccessories;
getCharacteristic = params.getCharacteristic;
updateReachability = params.updateReachability;
setAccessoryInformation = params.setAccessoryInformation;
}
Model.prototype.start = function() {
var url = this.config.url;
var options = {};
// experimental
this.publish_options = {};
topic_type = this.config.topic_type || "multiple";
topic_prefix = this.config.topic_prefix || "homebridge";
options.username = this.config.username || null;
options.password = this.config.password || null;
options.port = this.config.port || 1883;
this.publish_options = {retain: this.config.retain || false, qos: this.config.qos || 0};
//this.log.debug("connect options %s", JSON.stringify(this.publish_options));
if(this.config.cert != null) {
options.cert = fs.readFileSync(this.config.cert);
}
if(this.config.key != null) {
options.key = fs.readFileSync(this.config.key);
}
if(this.config.ca != null) {
options.ca = fs.readFileSync(this.config.ca);
}
//options default values
//options.protocolId = 'MQTT'; // 'MQIsdp';
//options.protocolVersion = 4; // 3;
//options.reconnectPeriod = 5000;
//options.keepalive = 60;
//options.clean = true;
options.clientId = this.config.client_id || 'homebridge-mqtt_' + Math.random().toString(16).substr(2, 8);
this.log("clientId = %s", options.clientId);
this.log("Connecting..");
client = mqtt.connect(url, options);
// Graceful shutdown cleanup for Child Bridge / Docker
if (api && typeof api.on === 'function') {
api.on('shutdown', function () {
if (!client) {
this.log.debug && this.log.debug('[mqtt] shutdown: no client to close');
return;
}
try {
this.log.debug && this.log.debug('[mqtt] shutdown: closing MQTT connection');
if (typeof client.end === 'function') {
client.end(true);
this.log.info && this.log.info('[mqtt] MQTT connection closed gracefully');
}
} catch (e) {
this.log.error && this.log.error('[mqtt] MQTT shutdown error', e);
}
}.bind(this));
}
// todo client.end();
// note: the plugig doesn't get the signal, because homebridge/lib/cli.js catchs the signal first.
/*
var signals = { 'SIGINT': 2, 'SIGTERM': 15 };
Object.keys(signals).forEach(function (signal) {
process.on(signal, function () {
this.log("Got %s, closing mqtt-client...", signal);
client.end();
}.bind(this));
}.bind(this));
*/
var timeout = setTimeout(function() {
if (!client.connected) {
this.log.error("connect error! (url = %s)", url);
}
}.bind(this), 5000);
client.on('connect', function () {
this.log("connected (url = %s)", url);
if (this.config.username) this.log.debug("on.connect %s %s", this.config.username, this.config.password);
var topic = topic_prefix + '/to/#';
client.subscribe(topic);
this.log.debug("on.connect subscribe %s", topic);
var plugin_version = Utils.readPluginVersion();
var msg = plugin_name + " v" + plugin_version + " started";
this.log.debug("on.connect %s", msg);
client.publish(topic_prefix + '/from/connected', msg, this.publish_options);
}.bind(this));
client.on('message', function (topic, buffer) {
var payload = buffer.toString();
var message, accessory;
var result, isValid;
if (typeof topic === "undefined" || payload.length === 0) {
message = "topic or payload invalid";
this.log.debug("on.message %s", message);
this.sendAck(false, message, 0);
} else {
//this.log.debug("on.message topic %s payload %s", topic, payload);
try {
accessory = JSON.parse(payload);
if (typeof accessory.request_id === "undefined") {
//this.log("added request_id=0");
accessory.request_id = 0;
} else {
//this.log("request_id %s", accessory.request_id);
}
if (typeof accessory.subtype !== "undefined") {
message = "Please replace 'subtype' by 'service_name'";
this.log.debug("on.message %s", message);
this.sendAck(false, message, accessory.request_id);
isValid = false;
} else {
isValid = true;
}
} catch(e) {
message = "invalid JSON format";
this.log.debug("on.message %s (%s)", message, e.message);
this.sendAck(false, message, 0);
isValid = false;
}
if (isValid) {
switch (topic) {
case topic_prefix + "/to/add":
case topic_prefix + "/to/add/accessory":
this.log.debug("on.message add \n%s", JSON.stringify(accessory, null, 2));
result = addAccessory(accessory);
this.handle(result, accessory.name, accessory.request_id);
break;
case topic_prefix + "/to/add/service":
case topic_prefix + "/to/add/services":
this.log.debug("on.message add/service \n%s", JSON.stringify(accessory, null, 2));
result = addService(accessory);
this.handle(result, accessory.name, accessory.request_id);
break;
case topic_prefix + "/to/set/reachability":
case topic_prefix + "/to/set/reachable":
if (typeof accessory.reachable === "boolean") {
result = updateReachability(accessory);
this.handle(result, accessory.name, accessory.request_id);
} else {
message = "accessory '" + accessory.name + "' reachable not boolean.";
this.log.warn("on.message %s", message);
this.sendAck(false, message, accessory.request_id);
}
break;
case topic_prefix + "/to/set/accessoryinformation":
case topic_prefix + "/to/set/information":
result = setAccessoryInformation(accessory);
this.handle(result, accessory.name, accessory.request_id);
break;
case topic_prefix + "/to/remove":
case topic_prefix + "/to/remove/accessory":
result = removeAccessory(accessory.name);
this.handle(result, accessory.name, accessory.request_id);
break;
case topic_prefix + "/to/remove/service":
result = removeService(accessory);
this.handle(result, accessory.name, accessory.request_id);
break;
case topic_prefix + "/to/set":
result = setValue(accessory);
if (!result.ack) {
this.handle(result, accessory.name, accessory.request_id);
}
break;
case topic_prefix + "/to/get":
result = getAccessories(accessory);
if (result.ack) {
this.sendAccessories(result.accessories, accessory.name, accessory.request_id);
} else {
this.handle(result, accessory.name, accessory.request_id);
}
break;
case topic_prefix + "/to/get/characteristic":
this.log("/to/get/characteristic: %s", JSON.stringify(accessory));
result = getCharacteristic(accessory);
if (result.ack) {
this.sendCharacteristic(result.characteristic, accessory.name, accessory.request_id);
} else {
this.handle(result, accessory.name, accessory.request_id);
}
break;
default:
message = "topic '" + topic + "' unknown.";
this.log.warn("on.message default %s", message);
this.sendAck(false, message, accessory.request_id);
}
}
}
}.bind(this));
client.on('close', function () {
this.log.warn("on.close <to analyze>");
// todo
//this.log("mqtt-client closed, shutting down Homebridge...");
//process.exit();
}.bind(this));
client.on('error', function (error) {
this.log.error("on.error %s", error);
}.bind(this));
client.on('reconnect', function () {
this.log.warn("on.reconnect <to analyze>");
}.bind(this));
client.on('offline', function () {
this.log.warn("on.offline <to analyze>");
}.bind(this));
}
Model.prototype.get = function(name, service_name, service_type, c, value, callback) {
//this.log.debug("get '%s' '%s' '%s' '%s'", name, service_name, c, value);
var msg = {"name": name, "service_name": service_name, "service_type": service_type, "characteristic": c, "cachedValue": value};
var topic = this.buildTopic('/from/get', name);
client.publish(topic, JSON.stringify(msg), this.publish_options);
// callback(null, null); // not used
}
Model.prototype.set = function(name, service_name, service_type, c, value, callback) {
//this.log.debug("set '%s' '%s' '%s' %s", name, service_name, c, value);
var msg = {"name": name, "service_name": service_name, "service_type": service_type, "characteristic": c, "value": value};
var topic = this.buildTopic('/from/set', name);
client.publish(topic, JSON.stringify(msg), this.publish_options);
callback();
}
Model.prototype.identify = function (name, manufacturer, model, serialnumber, firmwarerevision) {
var msg = {"name": name, "manufacturer": manufacturer, "model": model, "serialnumber": serialnumber, "firmwarerevision": firmwarerevision};
//this.log.debug("identify %s", JSON.stringify(msg));
var topic = this.buildTopic('/from/identify', name);
client.publish(topic, JSON.stringify(msg), this.publish_options);
}
Model.prototype.sendAccessories = function (accessories, name, request_id) {
var msg = accessories;
msg.request_id = request_id;
this.log.debug("sendAccessories \n%s", JSON.stringify(msg, null, 2));
var topic = this.buildTopic('/from/response', name);
client.publish(topic, JSON.stringify(msg), this.publish_options);
}
Model.prototype.sendCharacteristic = function (characteristic, name, request_id) {
var msg = characteristic;
msg.request_id = request_id;
this.log.debug("sendCharacteristic \n%s", JSON.stringify(msg, null, 2));
var topic = this.buildTopic('/from/response', name);
client.publish(topic, JSON.stringify(msg), this.publish_options);
}
Model.prototype.handle = function (result, name, request_id) {
this.sendAck(result.ack, result.message, request_id, name);
this.log("%s %s, %s [%s]", result.topic, result.ack, result.message, request_id);
}
Model.prototype.sendAck = function (ack, message, request_id, name) {
var msg = {"ack": ack, "message": message, "request_id": request_id};
//this.log.debug("sendAck %s", JSON.stringify(msg));
var topic = this.buildTopic('/from/response', name);
client.publish(topic, JSON.stringify(msg), this.publish_options);
}
Model.prototype.buildTopic = function(topic_section, name) {
var topic;
if (topic_type == "single") {
topic = topic_prefix + topic_section + '/' + name;
} else {
topic = topic_prefix + topic_section;
}
this.log.debug("buildTopic %s", topic);
return (topic);
}