From 5611e38bebd6f6efb8973f05d12053e19dfa1d7f Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Wed, 15 Jul 2015 22:36:45 -0700 Subject: [PATCH] Provider config and cli-based setup --- bin/homebridge | 2 +- .../homebridge-lockitron/index.js | 21 ++++ .../homebridge-lockitron/package.json | 7 +- lib/cli.js | 118 ++++++++++++++++++ lib/config.js | 40 ++++++ lib/homebridge.js | 6 +- lib/homebridge/cli.js | 70 ----------- lib/homebridge/user.js | 56 --------- lib/{homebridge => }/provider.js | 8 +- lib/{homebridge => }/server.js | 0 lib/user.js | 31 +++++ lib/util.js | 9 ++ package.json | 2 + 13 files changed, 233 insertions(+), 137 deletions(-) create mode 100644 lib/cli.js create mode 100644 lib/config.js delete mode 100644 lib/homebridge/cli.js delete mode 100644 lib/homebridge/user.js rename lib/{homebridge => }/provider.js (93%) rename lib/{homebridge => }/server.js (100%) create mode 100644 lib/user.js create mode 100644 lib/util.js diff --git a/bin/homebridge b/bin/homebridge index 0158b35..4ac4444 100755 --- a/bin/homebridge +++ b/bin/homebridge @@ -20,4 +20,4 @@ var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib'); global.homebridge = require(lib + '/homebridge'); // Run the HomeBridge CLI -homebridge.cli(); +require(lib + '/cli')(); diff --git a/example-providers/homebridge-lockitron/index.js b/example-providers/homebridge-lockitron/index.js index e9a5dfd..158d685 100644 --- a/example-providers/homebridge-lockitron/index.js +++ b/example-providers/homebridge-lockitron/index.js @@ -1,3 +1,24 @@ +import request from 'request'; // Demonstrate that we were loaded console.log("Lockitron provider loaded!"); + +module.exports = { + + config: { + accessToken: { + type: 'string', + description: "You can find your personal Access Token at: https://api.lockitron.com", + required: true + } + }, + + validateConfig: function(callback) { + + // validate the accessToken + let accessToken = homebridge.config.get('homebridge-lockitron.accessToken'); + + // prove that we got a value + console.log(`Access Token: ${accessToken}`); + } +} diff --git a/example-providers/homebridge-lockitron/package.json b/example-providers/homebridge-lockitron/package.json index 2e16cf3..2e549f7 100644 --- a/example-providers/homebridge-lockitron/package.json +++ b/example-providers/homebridge-lockitron/package.json @@ -8,5 +8,8 @@ }, "keywords": [ "homebridge-provider" - ] -} \ No newline at end of file + ], + "dependencies": { + "request": "^2.58.0" + } +} diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..496ac2d --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,118 @@ +import program from 'commander'; +import log from 'npmlog'; +import prompt from 'prompt'; +import { HOMEBRIDGE_VERSION } from './homebridge'; +import { User } from './user'; +import { Server } from './server'; +import { Provider } from './provider'; +import { camelCaseToRegularForm } from './util'; + +export default function() { + + // Global options (none currently) and version printout + program + .version(HOMEBRIDGE_VERSION); + + // Run the HomeBridge server + program + .command('server') + .description('Run the HomeBridge server.') + .action(runServer); + + program + .command('providers') + .description('List installed providers.') + .action(listInstalledProviders); + + program + .command('setup [provider]') + .description('Sets up a new HomeBridge provider or re-configures an existing one.') + .action(setupProvider); + + // Parse options and execute HomeBridge + program.parse(process.argv); + + // Display help by default if no commands or options given + if (!process.argv.slice(2).length) { + program.help(); + } +} + +function runServer(options) { + + // get all installed providers + let providers:Array = Provider.installed(); + + // load and validate providers - check for valid package.json, etc. + try { + this.providerModules = providers.map((provider) => provider.load()); + } + catch (err) { + console.log(err.message); + process.exit(1); + } +} + +function listInstalledProviders(options) { + Provider.installed().forEach((provider) => console.log(provider.name)); +} + +function setupProvider(providerName, options) { + + // if you didn't specify a provider, print help + if (!providerName) { + log.error("You must specify the name of the provider to setup. Type 'homebridge providers' to list the providers currently installed."); + program.help(); + } + + try { + let provider = new Provider(providerName); + let providerModule:object = provider.load({skipConfigCheck: true}); + + if (providerModule.config) { + + prompt.message = ""; + prompt.delimiter = ""; + prompt.start(); + prompt.get(buildPromptSchema(providerName, providerModule.config), (err, result) => { + + // apply configuration values entered by the user + for (let key:string in result) { + let value:object = result[key]; + + User.config.set(`${providerName}.${key}`, value); + } + + providerModule.validateConfig(); + }); + } + else { + providerModule.validateConfig(); + } + } + catch (err) { + log.error(`Setup failed: ${err.message}`); + } +} + +// builds a "schema" obejct for the prompt lib based on the provider's config spec +function buildPromptSchema(providerName: string, providerConfig: object): object { + let properties = {}; + + for (let key:string in providerConfig) { + let spec:object = providerConfig[key]; + + // do we have a value for this config key currently? + let currentValue = User.config.get(`${providerName}.${key}`); + + // copy over config spec with some modifications + properties[key] = { + description: `\n${spec.description}\n${camelCaseToRegularForm(key).white}:`, + type: spec.type, + required: spec.required, + default: currentValue + } + } + + return { properties }; +} diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..4258c69 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,40 @@ +import fs from 'fs'; + +export class Config { + + constructor(path:string, data:object = {}) { + this.path = path; + this.data = data; + } + + get(key:string) { + this.validateKey(key); + let [providerName, keyName] = key.split("."); + return this.data[providerName] && this.data[providerName][keyName]; + } + + set(key:string, value:object) { + this.validateKey(key); + let [providerName, keyName] = key.split("."); + this.data[providerName] = this.data[providerName] || {}; + this.data[providerName][keyName] = value; + this.save(); + } + + validateKey(key:string) { + if (key.split(".").length != 2) + throw new Error(`The config key '${key}' is invalid. Configuration keys must be in the form [my-provider].[myKey]`); + } + + static load(configPath: string): Config { + // load up the previous config if found + if (fs.existsSync(configPath)) + return new Config(configPath, JSON.parse(fs.readFileSync(configPath))); + else + return new Config(configPath); // empty initial config + } + + save() { + fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2)); + } +} \ No newline at end of file diff --git a/lib/homebridge.js b/lib/homebridge.js index 16e537b..13d0818 100644 --- a/lib/homebridge.js +++ b/lib/homebridge.js @@ -1,5 +1,5 @@ import fs from 'fs'; -import cli from './homebridge/cli'; +import { User } from './user'; // // Main HomeBridge Module with global exports. @@ -8,7 +8,5 @@ import cli from './homebridge/cli'; // HomeBridge version export const HOMEBRIDGE_VERSION = JSON.parse(fs.readFileSync('package.json')).version; -// HomeBridge CLI -export { cli } - // HomeBridge API +export let config = User.config; \ No newline at end of file diff --git a/lib/homebridge/cli.js b/lib/homebridge/cli.js deleted file mode 100644 index 2c50dc1..0000000 --- a/lib/homebridge/cli.js +++ /dev/null @@ -1,70 +0,0 @@ -import program from 'commander'; -import { HOMEBRIDGE_VERSION } from '../homebridge'; -import { Server } from './server'; -import { Provider } from './provider'; - -export default function() { - - // Global options (none currently) and version printout - program - .version(HOMEBRIDGE_VERSION); - - // Run the HomeBridge server - program - .command('server') - .description('Run the HomeBridge server.') - .action(runServer); - - program - .command('providers') - .description('List installed providers.') - .action(listInstalledProviders); - - program - .command('setup [provider]') - .description('Sets up a new HomeBridge provider or re-configures an existing one.') - .action(setupProvider); - - // Parse options and execute HomeBridge - program.parse(process.argv); - - // Display help by default if no commands or options given - if (!process.argv.slice(2).length) { - program.help(); - } -} - -function runServer(options) { - - // get all installed providers - let providers:Array = Provider.installed(); - - // load and validate providers - check for valid package.json, etc. - try { - this.providerModules = providers.map((provider) => provider.load()); - } - catch (err) { - console.log(err.message); - process.exit(1); - } -} - -function listInstalledProviders(options) { - Provider.installed().forEach((provider) => console.log(provider.name)); -} - -function setupProvider(providerName, options) { - - // if you didn't specify a provider, print help - if (!providerName) { - console.log("You must specify the name of the provider to setup. Type 'homebridge providers' to list the providers currently installed."); - program.help(); - } - - try { - let provider = new Provider(providerName); - } - catch (err) { - - } -} \ No newline at end of file diff --git a/lib/homebridge/user.js b/lib/homebridge/user.js deleted file mode 100644 index 919df07..0000000 --- a/lib/homebridge/user.js +++ /dev/null @@ -1,56 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -// -// Manages user settings and storage locations. -// - -// global cached config -let config:Config; - -export class User { - - static get config():Config { - return config || (config = new Config()); - } - - static get storagePath():string { - let home = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; - return path.join(home, ".homebridge"); - } - - static get configPath():string { - return path.join(User.storagePath, "config.json"); - } - - static get providersPath():string { - return path.join(User.storagePath, "providers"); - } -} - -export class Config { - - constructor(data:object = {}) { - this.data = data; - } - - get(key:string) { - return this.data[key]; - } - - set(key:string, value:object) { - this.data[key] = value; - } - - static load():Config { - // load up the previous config if found - if (fs.existsSync(User.configPath)) - return new Config(JSON.parse(fs.readFileSync(User.configPath))); - else - return new Config(); // empty initial config - } - - save() { - fs.writeFileSync(User.configPath, JSON.stringify(this.data)); - } -} \ No newline at end of file diff --git a/lib/homebridge/provider.js b/lib/provider.js similarity index 93% rename from lib/homebridge/provider.js rename to lib/provider.js index 144859c..964b333 100644 --- a/lib/homebridge/provider.js +++ b/lib/provider.js @@ -2,7 +2,7 @@ import path from 'path'; import fs from 'fs'; import semver from 'semver'; import { User } from './user'; -import { HOMEBRIDGE_VERSION } from '../homebridge'; +import { HOMEBRIDGE_VERSION } from './homebridge'; // This class represents a HomeBridge Provider that may or may not be installed. export class Provider { @@ -15,11 +15,11 @@ export class Provider { return path.join(User.providersPath, this.name); } - load():object { + load(options:object = {}):object { // does this provider exist at all? if (!fs.existsSync(this.path)) { - throw new Error(`Provider ${this.name} was not found. Make sure a directory matching the name '${this.name}' exists in your ~/.homebridge/providers folder.`) + throw new Error(`Provider ${this.name} was not found. Make sure the directory '~/.homebridge/providers/${this.name}' exists.`) } // check for a package.json @@ -62,7 +62,7 @@ export class Provider { let providerConfig = loadedProvider.config; // verify that all required values are present - if (providerConfig) { + if (providerConfig && !options.skipConfigCheck) { for (let key:string in providerConfig) { let configParams:object = providerConfig[key]; diff --git a/lib/homebridge/server.js b/lib/server.js similarity index 100% rename from lib/homebridge/server.js rename to lib/server.js diff --git a/lib/user.js b/lib/user.js new file mode 100644 index 0000000..82101bb --- /dev/null +++ b/lib/user.js @@ -0,0 +1,31 @@ +import path from 'path'; +import fs from 'fs'; +import { Config } from './config'; + +// +// Manages user settings and storage locations. +// + +// global cached config +let config:Config; + +export class User { + + static get config():Config { + return config || (config = Config.load(User.configPath)); + } + + static get storagePath():string { + let home = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; + return path.join(home, ".homebridge"); + } + + static get configPath():string { + return path.join(User.storagePath, "config.json"); + } + + static get providersPath():string { + return path.join(User.storagePath, "providers"); + } +} + diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..ae4e64c --- /dev/null +++ b/lib/util.js @@ -0,0 +1,9 @@ + +// Converts "accessToken" to "Access Token" +export function camelCaseToRegularForm(camelCase: string): string { + return camelCase + // insert a space before all caps + .replace(/([A-Z])/g, ' $1') + // uppercase the first character + .replace(/^./, function(str){ return str.toUpperCase(); }) +} \ No newline at end of file diff --git a/package.json b/package.json index cd141ae..187bcd5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "babel": "^5.6.14", "commander": "^2.8.1", "hap-nodejs": "git+https://github.com/khaost/HAP-NodeJS#2a1bc8d99a2009317ab5da93faebea34c89f197c", + "npmlog": "^1.2.1", + "prompt": "^0.2.14", "semver": "^4.3.6" } }