homebridge
Version:
HomeKit support for the impatient
477 lines • 24.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Server = exports.ServerStatus = void 0;
const fs_1 = __importDefault(require("fs"));
const chalk_1 = __importDefault(require("chalk"));
const qrcode_terminal_1 = __importDefault(require("qrcode-terminal"));
const api_1 = require("./api");
const bridgeService_1 = require("./bridgeService");
const childBridgeService_1 = require("./childBridgeService");
const externalPortService_1 = require("./externalPortService");
const ipcService_1 = require("./ipcService");
const logger_1 = require("./logger");
const pluginManager_1 = require("./pluginManager");
const user_1 = require("./user");
const mac = __importStar(require("./util/mac"));
const log = logger_1.Logger.internal;
var ServerStatus;
(function (ServerStatus) {
/**
* When the server is starting up
*/
ServerStatus["PENDING"] = "pending";
/**
* When the server is online and has published the main bridge
*/
ServerStatus["OK"] = "ok";
/**
* When the server is shutting down
*/
ServerStatus["DOWN"] = "down";
})(ServerStatus || (exports.ServerStatus = ServerStatus = {}));
class Server {
options;
api;
pluginManager;
bridgeService;
ipcService;
externalPortService;
config;
// used to keep track of child bridges
childBridges = new Map();
// current server status
serverStatus = "pending" /* ServerStatus.PENDING */;
constructor(options = {}) {
this.options = options;
this.config = Server.loadConfig();
// object we feed to Plugins and BridgeService
this.api = new api_1.HomebridgeAPI();
this.ipcService = new ipcService_1.IpcService();
this.externalPortService = new externalPortService_1.ExternalPortService(this.config.ports);
// set status to pending
this.setServerStatus("pending" /* ServerStatus.PENDING */);
// create new plugin manager
const pluginManagerOptions = {
activePlugins: this.config.plugins,
disabledPlugins: this.config.disabledPlugins,
customPluginPath: options.customPluginPath,
strictPluginResolution: options.strictPluginResolution,
};
this.pluginManager = new pluginManager_1.PluginManager(this.api, pluginManagerOptions);
// create new bridge service
const bridgeConfig = {
cachedAccessoriesDir: user_1.User.cachedAccessoryPath(),
cachedAccessoriesItemName: "cachedAccessories",
};
// shallow copy the homebridge options to the bridge options object
Object.assign(bridgeConfig, this.options);
this.bridgeService = new bridgeService_1.BridgeService(this.api, this.pluginManager, this.externalPortService, bridgeConfig, this.config.bridge, this.config);
// watch bridge events to check when server is online
this.bridgeService.bridge.on("advertised" /* AccessoryEventTypes.ADVERTISED */, () => {
this.setServerStatus("ok" /* ServerStatus.OK */);
});
// watch for the paired event to update the server status
this.bridgeService.bridge.on("paired" /* AccessoryEventTypes.PAIRED */, () => {
this.setServerStatus(this.serverStatus);
});
// watch for the unpaired event to update the server status
this.bridgeService.bridge.on("unpaired" /* AccessoryEventTypes.UNPAIRED */, () => {
this.setServerStatus(this.serverStatus);
});
}
/**
* Set the current server status and update parent via IPC
* @param status
*/
setServerStatus(status) {
this.serverStatus = status;
this.ipcService.sendMessage("serverStatusUpdate" /* IpcOutgoingEvent.SERVER_STATUS_UPDATE */, {
status: this.serverStatus,
paired: this.bridgeService?.bridge?._accessoryInfo?.paired() ?? null,
setupUri: this.bridgeService?.bridge?.setupURI() ?? null,
name: this.bridgeService?.bridge?.displayName || this.config.bridge.name,
username: this.config.bridge.username,
pin: this.config.bridge.pin,
});
}
async start() {
if (this.config.bridge.disableIpc !== true) {
this.initializeIpcEventHandlers();
}
const promises = [];
// load the cached accessories
await this.bridgeService.loadCachedPlatformAccessoriesFromDisk();
// initialize plugins
await this.pluginManager.initializeInstalledPlugins();
if (this.config.platforms.length > 0) {
promises.push(...this.loadPlatforms());
}
if (this.config.accessories.length > 0) {
this.loadAccessories();
}
// start child bridges
for (const childBridge of this.childBridges.values()) {
childBridge.start();
}
// restore cached accessories
this.bridgeService.restoreCachedPlatformAccessories();
this.api.signalFinished();
// wait for all platforms to publish their accessories before we publish the bridge
await Promise.all(promises).then(() => {
log.warn("\n\nNOTICE TO USERS AND PLUGIN DEVELOPERS"
+ "\n> Homebridge v2.0 is on the way and brings some breaking changes to existing plugins.\n"
+ "> Please visit the following link to learn more about the changes and how to prepare:\n"
+ "> https://github.com/homebridge/homebridge/wiki/Updating-To-Homebridge-v2.0\n"
+ "> Homebridge v2.0 will also drop support for Node.js v18, so now is a good time to update to v20 or v22.\n");
this.publishBridge();
});
}
teardown() {
this.bridgeService.teardown();
this.setServerStatus("down" /* ServerStatus.DOWN */);
}
publishBridge() {
this.bridgeService.publishBridge();
this.printSetupInfo(this.config.bridge.pin);
}
static loadConfig() {
// Look for the configuration file
const configPath = user_1.User.configPath();
const defaultBridge = {
name: "Homebridge",
username: "CC:22:3D:E3:CE:30",
pin: "031-45-154",
};
if (!fs_1.default.existsSync(configPath)) {
log.warn("config.json (%s) not found.", configPath);
return {
bridge: defaultBridge,
accessories: [],
platforms: [],
};
}
let config;
try {
config = JSON.parse(fs_1.default.readFileSync(configPath, { encoding: "utf8" }));
}
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: https://jsonlint.com");
log.error("");
throw err;
}
if (config.ports !== undefined) {
if (config.ports.start && config.ports.end) {
if (config.ports.start > config.ports.end) {
log.error("Invalid port pool configuration. End should be greater than or equal to start.");
config.ports = undefined;
}
}
else {
log.error("Invalid configuration for 'ports'. Missing 'start' and 'end' properties! Ignoring it!");
config.ports = undefined;
}
}
const bridge = config.bridge || defaultBridge;
bridge.name = bridge.name || defaultBridge.name;
bridge.username = bridge.username || defaultBridge.username;
bridge.pin = bridge.pin || defaultBridge.pin;
config.bridge = bridge;
const username = config.bridge.username;
if (!mac.validMacAddress(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.`);
}
config.accessories = config.accessories || [];
config.platforms = config.platforms || [];
if (!Array.isArray(config.accessories)) {
log.error("Value provided for accessories must be an array[]");
config.accessories = [];
}
if (!Array.isArray(config.platforms)) {
log.error("Value provided for platforms must be an array[]");
config.platforms = [];
}
log.info("Loaded config.json with %s accessories and %s platforms.", config.accessories.length, config.platforms.length);
if (config.bridge.advertiser) {
if (![
"bonjour-hap" /* MDNSAdvertiser.BONJOUR */,
"ciao" /* MDNSAdvertiser.CIAO */,
"avahi" /* MDNSAdvertiser.AVAHI */,
"resolved" /* MDNSAdvertiser.RESOLVED */,
].includes(config.bridge.advertiser)) {
config.bridge.advertiser = undefined;
log.error("Value provided in bridge.advertiser is not valid, reverting to platform default.");
}
}
else {
config.bridge.advertiser = undefined;
}
// Warn existing Homebridge 1.3.0 beta users they need to swap to bridge.advertiser
if (config.mdns && config.mdns.legacyAdvertiser === false && config.bridge.advertiser === "bonjour-hap" /* MDNSAdvertiser.BONJOUR */) {
log.error(`The "mdns"."legacyAdvertiser" = false option has been removed. Please use "bridge"."advertiser" = "${"ciao" /* MDNSAdvertiser.CIAO */}" to enable the Ciao mDNS advertiser. You should remove the "mdns"."legacyAdvertiser" section from your config.json.`);
}
return config;
}
loadAccessories() {
log.info("Loading " + this.config.accessories.length + " accessories...");
this.config.accessories.forEach((accessoryConfig, index) => {
if (!accessoryConfig.accessory) {
log.warn("Your config.json contains an illegal accessory configuration object at position %d. " +
"Missing property 'accessory'. Skipping entry...", index + 1); // we rather count from 1 for the normal people?
return;
}
const accessoryIdentifier = accessoryConfig.accessory;
const displayName = accessoryConfig.name;
if (!displayName) {
log.warn("Could not load accessory %s at position %d as it is missing the required 'name' property!", accessoryIdentifier, index + 1);
return;
}
let plugin;
let constructor;
try {
plugin = this.pluginManager.getPluginForAccessory(accessoryIdentifier);
}
catch (error) {
log.error(error.message);
return;
}
// check the plugin is not disabled
if (plugin.disabled) {
log.warn(`Ignoring config for the accessory "${accessoryIdentifier}" in your config.json as the plugin "${plugin.getPluginIdentifier()}" has been disabled.`);
return;
}
try {
constructor = plugin.getAccessoryConstructor(accessoryIdentifier);
}
catch (error) {
log.error(`Error loading the accessory "${accessoryIdentifier}" requested in your config.json at position ${index + 1} - this is likely an issue with the "${plugin.getPluginIdentifier()}" plugin.`);
log.error(error); // error message contains more information and full stack trace
return;
}
const logger = logger_1.Logger.withPrefix(displayName);
logger("Initializing %s accessory...", accessoryIdentifier);
if (accessoryConfig._bridge) {
// ensure the username is always uppercase
accessoryConfig._bridge.username = accessoryConfig._bridge.username.toUpperCase();
try {
this.validateChildBridgeConfig("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, accessoryConfig._bridge);
}
catch (error) {
log.error(error.message);
return;
}
let childBridge;
if (this.childBridges.has(accessoryConfig._bridge.username)) {
childBridge = this.childBridges.get(accessoryConfig._bridge.username);
logger(`Adding to existing child bridge ${accessoryConfig._bridge.username}`);
}
else {
logger(`Initializing child bridge ${accessoryConfig._bridge.username}`);
childBridge = new childBridgeService_1.ChildBridgeService("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, plugin, accessoryConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
this.childBridges.set(accessoryConfig._bridge.username, childBridge);
}
// add config to child bridge service
childBridge.addConfig(accessoryConfig);
return;
}
const accessoryInstance = new constructor(logger, accessoryConfig, this.api);
//pass accessoryIdentifier for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
const accessory = this.bridgeService.createHAPAccessory(plugin, accessoryInstance, displayName, accessoryIdentifier, accessoryConfig.uuid_base);
if (accessory) {
try {
this.bridgeService.bridge.addBridgedAccessory(accessory);
}
catch (e) {
logger.error(`Error loading the accessory "${accessoryIdentifier}" from "${plugin.getPluginIdentifier()}" requested in your config.json:`, e.message);
return;
}
}
else {
logger.info("Accessory %s returned empty set of services; not adding it to the bridge.", accessoryIdentifier);
}
});
}
loadPlatforms() {
log.info("Loading " + this.config.platforms.length + " platforms...");
const promises = [];
this.config.platforms.forEach((platformConfig, index) => {
if (!platformConfig.platform) {
log.warn("Your config.json contains an illegal platform configuration object at position %d. " +
"Missing property 'platform'. Skipping entry...", index + 1); // we rather count from 1 for the normal people?
return;
}
const platformIdentifier = platformConfig.platform;
const displayName = platformConfig.name || platformIdentifier;
let plugin;
let constructor;
// do not load homebridge-config-ui-x when running in service mode
if (platformIdentifier === "config" && process.env.UIX_SERVICE_MODE === "1") {
return;
}
try {
plugin = this.pluginManager.getPluginForPlatform(platformIdentifier);
}
catch (error) {
log.error(error.message);
return;
}
// check the plugin is not disabled
if (plugin.disabled) {
log.warn(`Ignoring config for the platform "${platformIdentifier}" in your config.json as the plugin "${plugin.getPluginIdentifier()}" has been disabled.`);
return;
}
try {
constructor = plugin.getPlatformConstructor(platformIdentifier);
}
catch (error) {
log.error(`Error loading the platform "${platformIdentifier}" requested in your config.json at position ${index + 1} - this is likely an issue with the "${plugin.getPluginIdentifier()}" plugin.`);
log.error(error); // error message contains more information and full stack trace
return;
}
const logger = logger_1.Logger.withPrefix(displayName);
logger("Initializing %s platform...", platformIdentifier);
if (platformConfig._bridge) {
// ensure the username is always uppercase
platformConfig._bridge.username = platformConfig._bridge.username.toUpperCase();
try {
this.validateChildBridgeConfig("platform" /* PluginType.PLATFORM */, platformIdentifier, platformConfig._bridge);
}
catch (error) {
log.error(error.message);
return;
}
logger(`Initializing child bridge ${platformConfig._bridge.username}`);
const childBridge = new childBridgeService_1.ChildBridgeService("platform" /* PluginType.PLATFORM */, platformIdentifier, plugin, platformConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
this.childBridges.set(platformConfig._bridge.username, childBridge);
// add config to child bridge service
childBridge.addConfig(platformConfig);
return;
}
const platform = new constructor(logger, platformConfig, this.api);
if (api_1.HomebridgeAPI.isDynamicPlatformPlugin(platform)) {
plugin.assignDynamicPlatform(platformIdentifier, platform);
}
else if (api_1.HomebridgeAPI.isStaticPlatformPlugin(platform)) { // Plugin 1.0, load accessories
promises.push(this.bridgeService.loadPlatformAccessories(plugin, platform, platformIdentifier, logger));
}
else {
// otherwise it's a IndependentPlatformPlugin which doesn't expose any methods at all.
// We just call the constructor and let it be enabled.
}
});
return promises;
}
/**
* Validate an external bridge config
*/
validateChildBridgeConfig(type, identifier, bridgeConfig) {
if (!mac.validMacAddress(bridgeConfig.username)) {
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` +
`not a valid username in _bridge.username: "${bridgeConfig.username}". Must be 6 pairs of colon-separated hexadecimal chars (A-F 0-9), like a MAC address.`);
}
if (this.childBridges.has(bridgeConfig.username)) {
const childBridge = this.childBridges.get(bridgeConfig.username);
if (type === "platform" /* PluginType.PLATFORM */) {
// only a single platform can exist on one child bridge
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` +
`Duplicate username found in _bridge.username: "${bridgeConfig.username}". Each platform child bridge must have it's own unique username.`);
}
else if (childBridge?.identifier !== identifier) {
// only accessories of the same type can be added to the same child bridge
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` +
`Duplicate username found in _bridge.username: "${bridgeConfig.username}". You can only group accessories of the same type in a child bridge.`);
}
}
if (bridgeConfig.username === this.config.bridge.username.toUpperCase()) {
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - ` +
`Username found in _bridge.username: "${bridgeConfig.username}" is the same as the main bridge. Each child bridge platform/accessory must have it's own unique username.`);
}
}
/**
* Takes care of the IPC Events sent to Homebridge
*/
initializeIpcEventHandlers() {
// start ipc service
this.ipcService.start();
// handle restart child bridge event
this.ipcService.on("restartChildBridge" /* IpcIncomingEvent.RESTART_CHILD_BRIDGE */, (username) => {
if (typeof username === "string") {
const childBridge = this.childBridges.get(username.toUpperCase());
childBridge?.restartChildBridge();
}
});
// handle stop child bridge event
this.ipcService.on("stopChildBridge" /* IpcIncomingEvent.STOP_CHILD_BRIDGE */, (username) => {
if (typeof username === "string") {
const childBridge = this.childBridges.get(username.toUpperCase());
childBridge?.stopChildBridge();
}
});
// handle start child bridge event
this.ipcService.on("startChildBridge" /* IpcIncomingEvent.START_CHILD_BRIDGE */, (username) => {
if (typeof username === "string") {
const childBridge = this.childBridges.get(username.toUpperCase());
childBridge?.startChildBridge();
}
});
this.ipcService.on("childBridgeMetadataRequest" /* IpcIncomingEvent.CHILD_BRIDGE_METADATA_REQUEST */, () => {
this.ipcService.sendMessage("childBridgeMetadataResponse" /* IpcOutgoingEvent.CHILD_BRIDGE_METADATA_RESPONSE */, Array.from(this.childBridges.values()).map(x => x.getMetadata()));
});
}
printSetupInfo(pin) {
console.log("Setup Payload:");
console.log(this.bridgeService.bridge.setupURI());
if (!this.options.hideQRCode) {
console.log("Scan this code with your HomeKit app on your iOS device to pair with Homebridge:");
qrcode_terminal_1.default.setErrorLevel("M"); // HAP specifies level M or higher for ECC
qrcode_terminal_1.default.generate(this.bridgeService.bridge.setupURI());
console.log("Or enter this code with your HomeKit app on your iOS device to pair with Homebridge:");
}
else {
console.log("Enter this code with your HomeKit app on your iOS device to pair with Homebridge:");
}
console.log(chalk_1.default.black.bgWhite(" "));
console.log(chalk_1.default.black.bgWhite(" ┌────────────┐ "));
console.log(chalk_1.default.black.bgWhite(" │ " + pin + " │ "));
console.log(chalk_1.default.black.bgWhite(" └────────────┘ "));
console.log(chalk_1.default.black.bgWhite(" "));
}
}
exports.Server = Server;
//# sourceMappingURL=server.js.map