New Plugin API

- Homebridge calls single exported initializer function and passes an
API object
  - No more require() for HAP classes (doesn't play well with plugin
structure)
This commit is contained in:
Nick Farina
2015-10-18 22:20:06 -07:00
parent a3c0df1c7c
commit 27b39cbfa0
8 changed files with 155 additions and 102 deletions
+4
View File
@@ -6,3 +6,7 @@
node_modules/ node_modules/
npm-debug.log npm-debug.log
.node-version .node-version
# Ignore any extra plugins in the example directory that aren't in Git already
# (this is a sandbox for the user)
example-plugins
@@ -1,11 +1,11 @@
var Service = require("hap-nodejs").Service;
var Characteristic = require("hap-nodejs").Characteristic;
var request = require("request"); var request = require("request");
var Service, Characteristic;
module.exports = { module.exports = function(homebridge) {
accessories: { Service = homebridge.hap.Service;
Lockitron: LockitronAccessory Characteristic = homebridge.hap.Characteristic;
}
homebridge.registerAccessory("Lockitron", LockitronAccessory);
} }
function LockitronAccessory(log, config) { function LockitronAccessory(log, config) {
+55
View File
@@ -0,0 +1,55 @@
var hap = require("hap-nodejs");
var hapLegacyTypes = require("hap-nodejs/accessories/types.js");
var log = require("./logger")._system;
// The official homebridge API is the object we feed the plugin's exported initializer function.
module.exports = {
API: API
}
function API() {
this._accessories = {}; // this._accessories[name] = accessory constructor
this._platforms = {}; // this._platforms[name] = platform constructor
// expose HAP-NodeJS in its entirely for plugins to use instead of making Plugins
// require() it as a dependency - it's a heavy dependency so we don't want it in
// every single plugin.
this.hap = hap;
// we also need to "bolt on" the legacy "types" constants for older accessories/platforms
// still using the "object literal" style JSON.
this.hapLegacyTypes = hapLegacyTypes;
}
API.prototype.accessory = function(name) {
if (!this._accessories[name])
throw new Error("The requested accessory '" + name + "' was not registered by any plugin.");
return this._accessories[name];
}
API.prototype.registerAccessory = function(name, constructor) {
if (this._accessories[name])
throw new Error("Attempting to register an accessory '" + name + "' which has already been registered!");
log.info("Registering accessory '%s'", name);
this._accessories[name] = constructor;
}
API.prototype.platform = function(name) {
if (!this._platforms[name])
throw new Error("The requested platform '" + name + "' was not registered by any plugin.");
return this._platforms[name];
}
API.prototype.registerPlatform = function(name, constructor) {
if (this._platforms[name])
throw new Error("Attempting to register a platform '" + name + "' which has already been registered!");
log.info("Registering platform '%s'", name);
this._platforms[name] = constructor;
}
+7 -6
View File
@@ -4,17 +4,18 @@ var version = require('./version');
var Server = require('./server').Server; var Server = require('./server').Server;
var Plugin = require('./plugin').Plugin; var Plugin = require('./plugin').Plugin;
var User = require('./user').User; var User = require('./user').User;
var log = require("./logger")._system;
'use strict'; 'use strict';
module.exports = function() { module.exports = function() {
console.log("_____________________________________________________________________"); log.warn("_____________________________________________________________________");
console.log("IMPORTANT: Homebridge is in the middle of some big changes."); log.warn("IMPORTANT: Homebridge is in the middle of some big changes.");
console.log(" Read more about it here:"); log.warn(" Read more about it here:");
console.log(" https://github.com/nfarina/homebridge/wiki/Migration-Guide"); log.warn(" https://github.com/nfarina/homebridge/wiki/Migration-Guide");
console.log("_____________________________________________________________________"); log.warn("_____________________________________________________________________");
console.log(""); log.warn("");
program program
.version(version) .version(version)
+39 -15
View File
@@ -1,4 +1,5 @@
var chalk = require('chalk'); var chalk = require('chalk');
var util = require('util');
'use strict'; 'use strict';
@@ -22,43 +23,66 @@ var loggerCache = {};
* Logger class * Logger class
*/ */
function Logger(pluginName) { function Logger(prefix) {
this.pluginName = pluginName; this.prefix = prefix;
} }
Logger.prototype.debug = function(msg) { Logger.prototype.debug = function(msg) {
if (DEBUG_ENABLED) if (DEBUG_ENABLED)
this.log('debug', msg); this.log.apply(this, ['debug'].concat(Array.prototype.slice.call(arguments)));
} }
Logger.prototype.info = function(msg) { Logger.prototype.info = function(msg) {
this.log('info', msg); this.log.apply(this, ['info'].concat(Array.prototype.slice.call(arguments)));
} }
Logger.prototype.warn = function(msg) { Logger.prototype.warn = function(msg) {
this.log('warn', msg); this.log.apply(this, ['warn'].concat(Array.prototype.slice.call(arguments)));
} }
Logger.prototype.error = function(msg) { Logger.prototype.error = function(msg) {
this.log('error', msg); this.log.apply(this, ['error'].concat(Array.prototype.slice.call(arguments)));
} }
Logger.prototype.log = function(level, msg) { Logger.prototype.log = function(level, msg) {
if (level == 'debug') msg = util.format.apply(util, Array.prototype.slice.call(arguments, 1));
func = console.log;
if (level == 'debug') {
msg = chalk.gray(msg); msg = chalk.gray(msg);
else if (level == 'warn') }
else if (level == 'warn') {
msg = chalk.yellow(msg); msg = chalk.yellow(msg);
else if (level == 'error') func = console.error;
}
else if (level == 'error') {
msg = chalk.bold.red(msg); msg = chalk.bold.red(msg);
func = console.error;
}
// prepend plugin name if applicable // prepend prefix if applicable
if (this.pluginName) if (this.prefix)
msg = chalk.cyan("[" + this.pluginName + "]") + " " + msg; msg = chalk.cyan("[" + this.prefix + "]") + " " + msg;
console.log(msg); func(msg);
} }
Logger.forPlugin = function(pluginName) { Logger.withPrefix = function(prefix) {
return loggerCache[pluginName] || (loggerCache[pluginName] = new Logger(pluginName));
if (!loggerCache[prefix]) {
// create a class-like logger thing that acts as a function as well
// as an instance of Logger.
var logger = new Logger(prefix);
var log = logger.info.bind(logger);
log.debug = logger.debug;
log.info = logger.info;
log.warn = logger.warn;
log.error = logger.error;
log.log = logger.log;
log.prefix = logger.prefix;
loggerCache[prefix] = log;
}
return loggerCache[prefix];
} }
+8 -13
View File
@@ -18,12 +18,7 @@ module.exports = {
function Plugin(pluginPath) { function Plugin(pluginPath) {
this.pluginPath = pluginPath; // like "/usr/local/lib/node_modules/plugin-lockitron" this.pluginPath = pluginPath; // like "/usr/local/lib/node_modules/plugin-lockitron"
this.initializer; // exported function from the plugin that initializes it
// these are exports pulled from the loaded plugin module
this.accessory = null; // single exposed accessory
this.platform = null; // single exposed platform
this.accessories = []; // array of exposed accessories
this.platforms = []; // array of exposed platforms
} }
Plugin.prototype.name = function() { Plugin.prototype.name = function() {
@@ -58,12 +53,8 @@ Plugin.prototype.load = function(options) {
var mainPath = path.join(this.pluginPath, main); var mainPath = path.join(this.pluginPath, main);
// try to require() it // try to require() it and grab the exported initialization hook
var pluginModule = require(mainPath); this.initializer = require(mainPath);
// extract all exposed accessories and platforms
this.accessories = pluginModule.accessories || {};
this.platforms = pluginModule.platforms || {};
} }
Plugin.loadPackageJSON = function(pluginPath) { Plugin.loadPackageJSON = function(pluginPath) {
@@ -142,7 +133,11 @@ Plugin.installed = function() {
if (!fs.existsSync(requirePath)) if (!fs.existsSync(requirePath))
continue; continue;
var names = fs.readdirSync(requirePath); var names = fs.readdirSync(requirePath);
// does this path point inside a single plugin and not a directory containing plugins?
if (fs.existsSync(path.join(requirePath, "package.json")))
names = [""];
// read through each directory in this node_modules folder // read through each directory in this node_modules folder
for (var index2 in names) { for (var index2 in names) {
+33 -60
View File
@@ -7,6 +7,9 @@ var Service = require("hap-nodejs").Service;
var Characteristic = require("hap-nodejs").Characteristic; var Characteristic = require("hap-nodejs").Characteristic;
var Plugin = require('./plugin').Plugin; var Plugin = require('./plugin').Plugin;
var User = require('./user').User; var User = require('./user').User;
var API = require('./api').API;
var log = require("./logger")._system;
var Logger = require('./logger').Logger;
'use strict'; 'use strict';
@@ -15,9 +18,8 @@ module.exports = {
} }
function Server() { function Server() {
this._accessories = {}; // this._accessories[name] = accessory constructor this._api = new API(); // object we feed to Plugins
this._platforms = {}; // this._platforms[name] = platform constructor this._plugins = this._loadPlugins(); // plugins[name] = Plugin instance
this._plugins = this._loadPlugins(this._accessories, this._platforms); // plugins[name] = plugin
this._config = this._loadConfig(); this._config = this._loadConfig();
this._bridge = this._createBridge(); this._bridge = this._createBridge();
} }
@@ -49,6 +51,8 @@ Server.prototype._publish = function() {
pincode: bridgeConfig.pin || "031-45-154", pincode: bridgeConfig.pin || "031-45-154",
category: Accessory.Categories.OTHER category: Accessory.Categories.OTHER
}); });
log.info("Homebridge is running on port %s.", bridgeConfig.port || 51826);
} }
Server.prototype._loadPlugins = function(accessories, platforms) { Server.prototype._loadPlugins = function(accessories, platforms) {
@@ -63,44 +67,22 @@ Server.prototype._loadPlugins = function(accessories, platforms) {
plugin.load(); plugin.load();
} }
catch (err) { catch (err) {
console.error(err); log.error("====================")
log.error("ERROR LOADING PLUGIN " + plugin.name() + ":")
log.error(err);
log.error("====================")
plugin.loadError = err; plugin.loadError = err;
} }
// add it to our dict for easy lookup later // add it to our dict for easy lookup later
plugins[plugin.name()] = plugin; plugins[plugin.name()] = plugin;
console.log("Loaded plugin: " + plugin.name()); log.info("Loaded plugin: " + plugin.name());
if (plugin.accessories) { // call the plugin's initializer and pass it our API instance
var sep = "" plugin.initializer(this._api);
var line = "Accessories: [";
for (var name in plugin.accessories) {
if (accessories[name])
throw new Error("Plugin " + plugin.name() + " wants to publish an accessory '" + name + "' which has already been published by another plugin!");
accessories[name] = plugin.accessories[name]; // copy to global dict log.info("---");
line += sep + name; sep = ",";
}
line += "]";
if (sep) console.log(line);
}
if (plugin.platforms) {
var sep = ""
var line = "Platforms: [";
for (var name in plugin.platforms) {
if (plugin.platforms[name])
throw new Error("Plugin " + plugin.name() + " wants to publish a platform '" + name + "' which has already been published by another plugin!");
platforms[name] = plugin.platforms[name]; // copy to global dict
line += sep + name; sep = ",";
}
line += "]";
if (sep) console.log(line);
}
console.log("---");
}.bind(this)); }.bind(this));
@@ -114,7 +96,7 @@ Server.prototype._loadConfig = function() {
// Complain and exit if it doesn't exist yet // Complain and exit if it doesn't exist yet
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
console.log("Couldn't find a config.json file in the same directory as app.js. Look at config-sample.json for examples of how to format your config.js and add your home accessories."); log.error("Couldn't find a config.json file in the same directory as app.js. Look at config-sample.json for examples of how to format your config.js and add your home accessories.");
process.exit(1); process.exit(1);
} }
@@ -124,17 +106,17 @@ Server.prototype._loadConfig = function() {
config = JSON.parse(fs.readFileSync(configPath)); config = JSON.parse(fs.readFileSync(configPath));
} }
catch (err) { catch (err) {
console.log("There was a problem reading your config.json file."); log.error("There was a problem reading your config.json file.");
console.log("Please try pasting your config.json file here to validate it: http://jsonlint.com"); log.error("Please try pasting your config.json file here to validate it: http://jsonlint.com");
console.log(""); log.error("");
throw err; throw err;
} }
var accessoryCount = (config.accessories && config.accessories.length) || 0; var accessoryCount = (config.accessories && config.accessories.length) || 0;
var platformCount = (config.platforms && config.platforms.length) || 0; var platformCount = (config.platforms && config.platforms.length) || 0;
console.log("Loaded config.json with %s accessories and %s platforms.", accessoryCount, platformCount); log.info("Loaded config.json with %s accessories and %s platforms.", accessoryCount, platformCount);
console.log("---"); log.info("---");
return config; return config;
} }
@@ -150,7 +132,7 @@ Server.prototype._createBridge = function() {
Server.prototype._loadAccessories = function() { Server.prototype._loadAccessories = function() {
// Instantiate all accessories in the config // Instantiate all accessories in the config
console.log("Loading " + this._config.accessories.length + " accessories..."); log.info("Loading " + this._config.accessories.length + " accessories...");
for (var i=0; i<this._config.accessories.length; i++) { for (var i=0; i<this._config.accessories.length; i++) {
@@ -158,18 +140,18 @@ Server.prototype._loadAccessories = function() {
// Load up the class for this accessory // Load up the class for this accessory
var accessoryType = accessoryConfig["accessory"]; // like "Lockitron" var accessoryType = accessoryConfig["accessory"]; // like "Lockitron"
var accessoryConstructor = this._accessories[accessoryType]; // like "LockitronAccessory", a JavaScript constructor var accessoryConstructor = this._api.accessory(accessoryType); // like "LockitronAccessory", a JavaScript constructor
if (!accessoryConstructor) if (!accessoryConstructor)
throw new Error("Your config.json is requesting the accessory '" + accessoryType + "' which has not been published by any installed plugins."); throw new Error("Your config.json is requesting the accessory '" + accessoryType + "' which has not been published by any installed plugins.");
// Create a custom logging function that prepends the device display name for debugging // Create a custom logging function that prepends the device display name for debugging
var accessoryName = accessoryConfig["name"]; var accessoryName = accessoryConfig["name"];
var log = this._createLog(accessoryName); var accessoryLogger = Logger.withPrefix(accessoryName);
log("Initializing %s accessory...", accessoryType); accessoryLogger("Initializing %s accessory...", accessoryType);
var accessoryInstance = new accessoryConstructor(log, accessoryConfig); var accessoryInstance = new accessoryConstructor(accessoryLogger, accessoryConfig);
var accessory = this._createAccessory(accessoryInstance, accessoryName, accessoryType, accessoryConfig.uuid_base); //pass accessoryType for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation var accessory = this._createAccessory(accessoryInstance, accessoryName, accessoryType, accessoryConfig.uuid_base); //pass accessoryType for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
// add it to the bridge // add it to the bridge
@@ -179,7 +161,7 @@ Server.prototype._loadAccessories = function() {
Server.prototype._loadPlatforms = function() { Server.prototype._loadPlatforms = function() {
console.log("Loading " + this._config.platforms.length + " platforms..."); log.info("Loading " + this._config.platforms.length + " platforms...");
for (var i=0; i<this._config.platforms.length; i++) { for (var i=0; i<this._config.platforms.length; i++) {
@@ -188,18 +170,18 @@ Server.prototype._loadPlatforms = function() {
// Load up the class for this accessory // Load up the class for this accessory
var platformType = platformConfig["platform"]; // like "Wink" var platformType = platformConfig["platform"]; // like "Wink"
var platformName = platformConfig["name"]; var platformName = platformConfig["name"];
var platformConstructor = this._platforms[platformType]; // like "WinkPlatform", a JavaScript constructor var platformConstructor = this._api.platform(platformType); // like "WinkPlatform", a JavaScript constructor
if (!platformConstructor) if (!platformConstructor)
throw new Error("Your config.json is requesting the platform '" + platformType + "' which has not been published by any installed plugins."); throw new Error("Your config.json is requesting the platform '" + platformType + "' which has not been published by any installed plugins.");
// Create a custom logging function that prepends the platform name for debugging // Create a custom logging function that prepends the platform name for debugging
var log = this._createLog(platformName); var platformLogger = Logger.withPrefix(accessoryName);
log("Initializing %s platform...", platformType); platformLogger("Initializing %s platform...", platformType);
var platformInstance = new platformConstructor(log, platformConfig); var platformInstance = new platformConstructor(platformLogger, platformConfig);
this._loadPlatformAccessories(platformInstance, log, platformType); this._loadPlatformAccessories(platformInstance, platformLogger, platformType);
} }
} }
@@ -279,19 +261,10 @@ Server.prototype._createAccessory = function(accessoryInstance, displayName, acc
// Returns the setup code in a scannable format. // Returns the setup code in a scannable format.
Server.prototype._printPin = function(pin) { Server.prototype._printPin = function(pin) {
console.log("Scan this code with your HomeKit App on your iOS device:"); console.log("Scan this code with your HomeKit App on your iOS device to pair with Homebridge:");
console.log("\x1b[30;47m%s\x1b[0m", " "); console.log("\x1b[30;47m%s\x1b[0m", " ");
console.log("\x1b[30;47m%s\x1b[0m", " ┌────────────┐ "); console.log("\x1b[30;47m%s\x1b[0m", " ┌────────────┐ ");
console.log("\x1b[30;47m%s\x1b[0m", " │ " + pin + " │ "); console.log("\x1b[30;47m%s\x1b[0m", " │ " + pin + " │ ");
console.log("\x1b[30;47m%s\x1b[0m", " └────────────┘ "); console.log("\x1b[30;47m%s\x1b[0m", " └────────────┘ ");
console.log("\x1b[30;47m%s\x1b[0m", " "); console.log("\x1b[30;47m%s\x1b[0m", " ");
} }
// Returns a logging function that prepends messages with the given name in [brackets].
Server.prototype._createLog = function(name) {
return function(message) {
var rest = Array.prototype.slice.call(arguments, 1 ); // any arguments after message
var args = ["[%s] " + message, name].concat(rest);
console.log.apply(console, args);
}
}
+1
View File
@@ -29,6 +29,7 @@
}, },
"preferGlobal": true, "preferGlobal": true,
"dependencies": { "dependencies": {
"chalk": "^1.1.1",
"commander": "2.8.1", "commander": "2.8.1",
"hap-nodejs": "0.0.2", "hap-nodejs": "0.0.2",
"semver": "5.0.3" "semver": "5.0.3"