var path = require('path'); var fs = require('fs'); var uuid = require("hap-nodejs").uuid; var accessoryStorage = require('node-persist').create(); var Bridge = require("hap-nodejs").Bridge; var Accessory = require("hap-nodejs").Accessory; var Service = require("hap-nodejs").Service; var Characteristic = require("hap-nodejs").Characteristic; var AccessoryLoader = require("hap-nodejs").AccessoryLoader; var once = require("hap-nodejs/lib/util/once").once; var Plugin = require('./plugin').Plugin; var User = require('./user').User; var API = require('./api').API; var PlatformAccessory = require("./platformAccessory").PlatformAccessory; var BridgeSetupManager = require("./bridgeSetupManager").BridgeSetupManager; var log = require("./logger")._system; var Logger = require('./logger').Logger; var mac = require("./util/mac"); 'use strict'; module.exports = { Server: Server } function Server(insecureAccess) { // Setup Accessory Cache Storage accessoryStorage.initSync({ dir: User.cachedAccessoryPath() }); this._api = new API(); // object we feed to Plugins this._api.on('registerPlatformAccessories', function(accessories) { this._handleRegisterPlatformAccessories(accessories); }.bind(this)); this._api.on('updatePlatformAccessories', function(accessories) { this._handleUpdatePlatformAccessories(accessories); }.bind(this)); this._api.on('unregisterPlatformAccessories', function(accessories) { this._handleUnregisterPlatformAccessories(accessories); }.bind(this)); this._api.on('publishCameraAccessories', function(accessories) { this._handlePublishCameraAccessories(accessories); }.bind(this)); this._plugins = this._loadPlugins(); // plugins[name] = Plugin instance this._config = this._loadConfig(); this._cachedPlatformAccessories = this._loadCachedPlatformAccessories(); this._bridge = this._createBridge(); this._activeDynamicPlugins = {}; this._configurablePlatformPlugins = {}; this._publishedCameras = {}; this._setupManager = new BridgeSetupManager(); this._setupManager.on('newConfig', this._handleNewConfig.bind(this)); this._setupManager.on('requestCurrentConfig', function(callback) { callback(this._config); }.bind(this)); // Server is "secure by default", meaning it creates a top-level Bridge accessory that // will not allow unauthenticated requests. This matches the behavior of actual HomeKit // accessories. However you can set this to true to allow all requests without authentication, // which can be useful for easy hacking. Note that this will expose all functions of your // bridged accessories, like changing charactersitics (i.e. flipping your lights on and off). this._allowInsecureAccess = insecureAccess || false; } Server.prototype.run = function() { // keep track of async calls we're waiting for callbacks on before we can start up this._asyncCalls = 0; this._asyncWait = true; if (this._config.platforms) this._loadPlatforms(); if (this._config.accessories) this._loadAccessories(); this._loadDynamicPlatforms(); this._configCachedPlatformAccessories(); this._setupManager.configurablePlatformPlugins = this._configurablePlatformPlugins; this._bridge.addService(this._setupManager.service); this._asyncWait = false; // publish now unless we're waiting on anyone if (this._asyncCalls == 0) this._publish(); this._api.emit('didFinishLaunching'); } Server.prototype._publish = function() { // pull out our custom Bridge settings from config.json, if any var bridgeConfig = this._config.bridge || {}; var info = this._bridge.getService(Service.AccessoryInformation); if (bridgeConfig.manufacturer) info.setCharacteristic(Characteristic.Manufacturer, bridgeConfig.manufacturer); if (bridgeConfig.model) info.setCharacteristic(Characteristic.Model, bridgeConfig.model); if (bridgeConfig.serialNumber) info.setCharacteristic(Characteristic.SerialNumber, bridgeConfig.serialNumber); this._printPin(bridgeConfig.pin); this._bridge.on('listening', function(port) { log.info("Homebridge is running on port %s.", port); }); this._bridge.publish({ username: bridgeConfig.username || "CC:22:3D:E3:CE:30", pincode: bridgeConfig.pin || "031-45-154", category: Accessory.Categories.BRIDGE }, this._allowInsecureAccess); } Server.prototype._loadPlugins = function(accessories, platforms) { var plugins = {}; var foundOnePlugin = false; // load and validate plugins - check for valid package.json, etc. Plugin.installed().forEach(function(plugin) { // attempt to load it try { plugin.load(); } catch (err) { log.error("====================") log.error("ERROR LOADING PLUGIN " + plugin.name() + ":") log.error(err.stack); log.error("====================") plugin.loadError = err; } if (!plugin.loadError) { // add it to our dict for easy lookup later plugins[plugin.name()] = plugin; log.info("Loaded plugin: " + plugin.name()); // call the plugin's initializer and pass it our API instance plugin.initializer(this._api); log.info("---"); foundOnePlugin = true; } }.bind(this)); // Complain if you don't have any plugins. if (!foundOnePlugin) { log.warn("No plugins found. See the README for information on installing plugins.") } return plugins; } Server.prototype._loadConfig = function() { // Look for the configuration file var configPath = User.configPath(); // Complain and exit if it doesn't exist yet if (!fs.existsSync(configPath)) { log.warn("config.json (%s) not found.", configPath); var config = {}; config.bridge = { "name": "Homebridge", "username": "CC:22:3D:E3:CE:30", "pin": "031-45-154" }; return config; // log.error("Couldn't find a config.json file at '"+configPath+"'. Look at config-sample.json for examples of how to format your config.js and add your home accessories."); // process.exit(1); } // Load up the configuration file var config; try { config = JSON.parse(fs.readFileSync(configPath)); } catch (err) { log.error("There was a problem reading your config.json file."); log.error("Please try pasting your config.json file here to validate it: http://jsonlint.com"); log.error(""); throw err; } var accessoryCount = (config.accessories && config.accessories.length) || 0; var username = config.bridge.username; var validMac = /^([0-9A-F]{2}:){5}([0-9A-F]{2})$/; if (!validMac.test(username)){ throw new Error('Not a valid username: ' + username + '. Must be 6 pairs of colon-' + 'separated hexadecimal chars (A-F 0-9), like a MAC address.'); } var accessoryCount = (config.accessories && config.accessories.length) || 0; var platformCount = (config.platforms && config.platforms.length) || 0; log.info("Loaded config.json with %s accessories and %s platforms.", accessoryCount, platformCount); log.info("---"); return config; } Server.prototype._loadCachedPlatformAccessories = function() { var cachedAccessories = accessoryStorage.getItem("cachedAccessories"); var platformAccessories = []; if (cachedAccessories) { for (var index in cachedAccessories) { var serializedAccessory = cachedAccessories[index]; var platformAccessory = new PlatformAccessory(serializedAccessory.displayName, serializedAccessory.UUID, serializedAccessory.category); platformAccessory._configFromData(serializedAccessory); platformAccessories.push(platformAccessory); } } return platformAccessories; } Server.prototype._createBridge = function() { // pull out our custom Bridge settings from config.json, if any var bridgeConfig = this._config.bridge || {}; // Create our Bridge which will host all loaded Accessories return new Bridge(bridgeConfig.name || 'Homebridge', uuid.generate("HomeBridge")); } Server.prototype._loadAccessories = function() { // Instantiate all accessories in the config log.info("Loading " + this._config.accessories.length + " accessories..."); for (var i=0; i