homebridge
Version:
HomeKit support for the impatient
455 lines • 26.8 kB
JavaScript
"use strict";
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.BridgeService = void 0;
const hap_nodejs_1 = require("hap-nodejs");
const logger_1 = require("./logger");
const platformAccessory_1 = require("./platformAccessory");
const pluginManager_1 = require("./pluginManager");
const storageService_1 = require("./storageService");
const mac = __importStar(require("./util/mac"));
const version_1 = __importDefault(require("./version"));
const log = logger_1.Logger.internal;
class BridgeService {
api;
pluginManager;
externalPortService;
bridgeOptions;
bridgeConfig;
config;
bridge;
storageService;
allowInsecureAccess;
cachedPlatformAccessories = [];
cachedAccessoriesFileLoaded = false;
publishedExternalAccessories = new Map();
constructor(api, pluginManager, externalPortService, bridgeOptions, bridgeConfig, config) {
this.api = api;
this.pluginManager = pluginManager;
this.externalPortService = externalPortService;
this.bridgeOptions = bridgeOptions;
this.bridgeConfig = bridgeConfig;
this.config = config;
this.storageService = new storageService_1.StorageService(this.bridgeOptions.cachedAccessoriesDir);
this.storageService.initSync();
// 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 characteristics (i.e. flipping your lights on and off).
this.allowInsecureAccess = this.bridgeOptions.insecureAccess || false;
this.api.on("registerPlatformAccessories" /* InternalAPIEvent.REGISTER_PLATFORM_ACCESSORIES */, this.handleRegisterPlatformAccessories.bind(this));
this.api.on("updatePlatformAccessories" /* InternalAPIEvent.UPDATE_PLATFORM_ACCESSORIES */, this.handleUpdatePlatformAccessories.bind(this));
this.api.on("unregisterPlatformAccessories" /* InternalAPIEvent.UNREGISTER_PLATFORM_ACCESSORIES */, this.handleUnregisterPlatformAccessories.bind(this));
this.api.on("publishExternalAccessories" /* InternalAPIEvent.PUBLISH_EXTERNAL_ACCESSORIES */, this.handlePublishExternalAccessories.bind(this));
this.bridge = new hap_nodejs_1.Bridge(bridgeConfig.name, hap_nodejs_1.uuid.generate("HomeBridge"));
this.bridge.on("characteristic-warning" /* AccessoryEventTypes.CHARACTERISTIC_WARNING */, () => {
// We register characteristic warning handlers on every bridged accessory (to have a reference to the plugin).
// For Bridges the warnings will propagate to the main Bridge accessory, thus we need to silence them here.
// Otherwise, those would be printed twice (by us and HAP-NodeJS as it detects no handlers on the bridge).
});
}
// characteristic warning event has additional parameter originatorChain: string[] which is currently unused
static printCharacteristicWriteWarning(plugin, accessory, opts, warning) {
const wikiInfo = "See https://homebridge.io/w/JtMGR for more info.";
switch (warning.type) {
case "slow-read" /* CharacteristicWarningType.SLOW_READ */:
case "slow-write" /* CharacteristicWarningType.SLOW_WRITE */:
if (!opts.ignoreSlow) {
log.info((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), "This plugin slows down Homebridge.", warning.message, wikiInfo);
}
break;
case "timeout-read" /* CharacteristicWarningType.TIMEOUT_READ */:
case "timeout-write" /* CharacteristicWarningType.TIMEOUT_WRITE */:
log.error((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), "This plugin slows down Homebridge.", warning.message, wikiInfo);
break;
case "warn-message" /* CharacteristicWarningType.WARN_MESSAGE */:
log.info((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), `This plugin generated a warning from the characteristic '${warning.characteristic.displayName}':`, warning.message + ".", wikiInfo);
break;
case "error-message" /* CharacteristicWarningType.ERROR_MESSAGE */:
log.error((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), `This plugin threw an error from the characteristic '${warning.characteristic.displayName}':`, warning.message + ".", wikiInfo);
break;
case "debug-message" /* CharacteristicWarningType.DEBUG_MESSAGE */:
log.debug((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), `Characteristic '${warning.characteristic.displayName}':`, warning.message + ".", wikiInfo);
break;
default: // generic message for yet unknown types
log.info((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), `This plugin generated a warning from the characteristic '${warning.characteristic.displayName}':`, warning.message + ".", wikiInfo);
break;
}
if (warning.stack) {
log.debug((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), warning.stack);
}
}
publishBridge() {
const bridgeConfig = this.bridgeConfig;
const info = this.bridge.getService(hap_nodejs_1.Service.AccessoryInformation);
info.setCharacteristic(hap_nodejs_1.Characteristic.Manufacturer, bridgeConfig.manufacturer || "homebridge.io");
info.setCharacteristic(hap_nodejs_1.Characteristic.Model, bridgeConfig.model || "homebridge");
info.setCharacteristic(hap_nodejs_1.Characteristic.SerialNumber, bridgeConfig.serialNumber || bridgeConfig.username);
info.setCharacteristic(hap_nodejs_1.Characteristic.FirmwareRevision, bridgeConfig.firmwareRevision || (0, version_1.default)());
this.bridge.on("listening" /* AccessoryEventTypes.LISTENING */, (port) => {
log.success("Homebridge v%s (HAP v%s) (%s) is running on port %s.", (0, version_1.default)(), (0, hap_nodejs_1.HAPLibraryVersion)(), bridgeConfig.name, port);
});
// noinspection JSDeprecatedSymbols
const publishInfo = {
username: bridgeConfig.username,
port: bridgeConfig.port,
pincode: bridgeConfig.pin,
category: 2 /* Categories.BRIDGE */,
bind: bridgeConfig.bind,
mdns: this.config.mdns, // this is deprecated now
addIdentifyingMaterial: true,
advertiser: bridgeConfig.advertiser,
};
if (bridgeConfig.setupID && bridgeConfig.setupID.length === 4) {
publishInfo.setupID = bridgeConfig.setupID;
}
log.debug("Publishing bridge accessory (name: %s, publishInfo: %o).", this.bridge.displayName, BridgeService.strippingPinCode(publishInfo));
this.bridge.publish(publishInfo, this.allowInsecureAccess);
}
/**
* Attempt to load the cached accessories from disk.
*/
async loadCachedPlatformAccessoriesFromDisk() {
let cachedAccessories = null;
try {
cachedAccessories = await this.storageService.getItem(this.bridgeOptions.cachedAccessoriesItemName);
}
catch (e) {
log.error("Failed to load cached accessories from disk:", e.message);
if (e instanceof SyntaxError) {
// syntax error probably means invalid json / corrupted file; try and restore from backup
cachedAccessories = await this.restoreCachedAccessoriesBackup();
}
else {
log.error("Not restoring cached accessories - some accessories may be reset.");
}
}
if (cachedAccessories) {
log.info(`Loaded ${cachedAccessories.length} cached accessories from ${this.bridgeOptions.cachedAccessoriesItemName}.`);
this.cachedPlatformAccessories = cachedAccessories.map(serialized => {
return platformAccessory_1.PlatformAccessory.deserialize(serialized);
});
if (cachedAccessories.length) {
// create a backup of the cache file
await this.createCachedAccessoriesBackup();
}
}
this.cachedAccessoriesFileLoaded = true;
}
/**
* Return the name of the backup cache file
*/
get backupCacheFileName() {
return `.${this.bridgeOptions.cachedAccessoriesItemName}.bak`;
}
/**
* Create a backup of the cached file
* This is used if we ever have trouble reading the main cache file
*/
async createCachedAccessoriesBackup() {
try {
await this.storageService.copyItem(this.bridgeOptions.cachedAccessoriesItemName, this.backupCacheFileName);
}
catch (e) {
log.warn(`Failed to create a backup of the ${this.bridgeOptions.cachedAccessoriesItemName} cached accessories file:`, e.message);
}
}
/**
* Restore a cached accessories backup
* This is used if the main cache file has a JSON syntax error / is corrupted
*/
async restoreCachedAccessoriesBackup() {
try {
const cachedAccessories = await this.storageService.getItem(this.backupCacheFileName);
if (cachedAccessories && cachedAccessories.length) {
log.warn(`Recovered ${cachedAccessories.length} accessories from ${this.bridgeOptions.cachedAccessoriesItemName} cache backup.`);
}
return cachedAccessories;
}
catch (e) {
return null;
}
}
restoreCachedPlatformAccessories() {
this.cachedPlatformAccessories = this.cachedPlatformAccessories.filter(accessory => {
let plugin = this.pluginManager.getPlugin(accessory._associatedPlugin);
if (!plugin) { // a little explainer here. This section is basically here to resolve plugin name changes of dynamic platform plugins
try {
// resolve platform accessories by searching for plugins which registered a dynamic platform for the given name
plugin = this.pluginManager.getPluginByActiveDynamicPlatform(accessory._associatedPlatform);
if (plugin) { // if it's undefined the no plugin was found
// could improve on this by calculating the Levenshtein distance to only allow platform ownership changes
// when something like a typo happened. Are there other reasons the name could change?
// And how would we define the threshold?
log.info("When searching for the associated plugin of the accessory '" + accessory.displayName + "' " +
"it seems like the plugin name changed from '" + accessory._associatedPlugin + "' to '" +
plugin.getPluginIdentifier() + "'. Plugin association is now being transformed!");
accessory._associatedPlugin = plugin.getPluginIdentifier(); // update the associated plugin to the new one
}
}
catch (error) { // error is thrown if multiple plugins where found for the given platform name
log.info("Could not find the associated plugin for the accessory '" + accessory.displayName + "'. " +
"Tried to find the plugin by the platform name but " + error.message);
}
}
const platformPlugins = plugin && plugin.getActiveDynamicPlatform(accessory._associatedPlatform);
if (plugin) {
accessory._associatedHAPAccessory.on("characteristic-warning" /* AccessoryEventTypes.CHARACTERISTIC_WARNING */, BridgeService.printCharacteristicWriteWarning.bind(this, plugin, accessory._associatedHAPAccessory, {}));
}
if (!platformPlugins) {
log.info(`Failed to find plugin to handle accessory ${accessory._associatedHAPAccessory.displayName}`);
if (!this.bridgeOptions.keepOrphanedCachedAccessories) {
log.info(`Removing orphaned accessory ${accessory._associatedHAPAccessory.displayName}`);
return false; // filter it from the list
}
}
else {
// We set a placeholder for FirmwareRevision before configureAccessory is called so the plugin has the opportunity to override it.
accessory.getService(hap_nodejs_1.Service.AccessoryInformation)?.setCharacteristic(hap_nodejs_1.Characteristic.FirmwareRevision, "0");
platformPlugins.configureAccessory(accessory);
}
try {
this.bridge.addBridgedAccessory(accessory._associatedHAPAccessory);
}
catch (e) {
log.warn(`${accessory._associatedPlugin ? (0, logger_1.getLogPrefix)(accessory._associatedPlugin) : ""} Could not restore cached accessory '${accessory._associatedHAPAccessory.displayName}':`, e?.message);
return false; // filter it from the list
}
return true; // keep it in the list
});
}
/**
* Save the cached accessories back to disk.
*/
saveCachedPlatformAccessoriesOnDisk() {
try {
// only save the cache file back to disk if we have already attempted to load it
// this should prevent the cache being deleted should homebridge be shutdown before it has finished launching
if (this.cachedAccessoriesFileLoaded) {
const serializedAccessories = this.cachedPlatformAccessories.map(accessory => platformAccessory_1.PlatformAccessory.serialize(accessory));
this.storageService.setItemSync(this.bridgeOptions.cachedAccessoriesItemName, serializedAccessories);
}
}
catch (e) {
log.error("Failed to save cached accessories to disk:", e.message);
log.error("Your accessories will not persist between restarts until this issue is resolved.");
}
}
handleRegisterPlatformAccessories(accessories) {
const hapAccessories = accessories.map(accessory => {
this.cachedPlatformAccessories.push(accessory);
const plugin = this.pluginManager.getPlugin(accessory._associatedPlugin);
if (plugin) {
const platforms = plugin.getActiveDynamicPlatform(accessory._associatedPlatform);
if (!platforms) {
log.warn("The plugin '%s' registered a new accessory for the platform '%s'. The platform couldn't be found though!", accessory._associatedPlugin, accessory._associatedPlatform);
}
accessory._associatedHAPAccessory.on("characteristic-warning" /* AccessoryEventTypes.CHARACTERISTIC_WARNING */, BridgeService.printCharacteristicWriteWarning.bind(this, plugin, accessory._associatedHAPAccessory, {}));
}
else {
log.warn("A platform configured a new accessory under the plugin name '%s'. However no loaded plugin could be found for the name!", accessory._associatedPlugin);
}
return accessory._associatedHAPAccessory;
});
this.bridge.addBridgedAccessories(hapAccessories);
this.saveCachedPlatformAccessoriesOnDisk();
}
handleUpdatePlatformAccessories(accessories) {
if (!Array.isArray(accessories)) {
// This could be quite destructive if a non-array is passed in, so we'll just ignore it.
return;
}
const nonUpdatedPlugins = this.cachedPlatformAccessories.filter(cachedPlatformAccessory => (accessories.find(accessory => accessory.UUID === cachedPlatformAccessory._associatedHAPAccessory.UUID) === undefined));
this.cachedPlatformAccessories = [...nonUpdatedPlugins, ...accessories];
// Update persisted accessories
this.saveCachedPlatformAccessoriesOnDisk();
}
handleUnregisterPlatformAccessories(accessories) {
const hapAccessories = accessories.map(accessory => {
const index = this.cachedPlatformAccessories.indexOf(accessory);
if (index >= 0) {
this.cachedPlatformAccessories.splice(index, 1);
}
return accessory._associatedHAPAccessory;
});
this.bridge.removeBridgedAccessories(hapAccessories);
this.saveCachedPlatformAccessoriesOnDisk();
}
async handlePublishExternalAccessories(accessories) {
const accessoryPin = this.bridgeConfig.pin;
for (const accessory of accessories) {
const hapAccessory = accessory._associatedHAPAccessory;
const advertiseAddress = mac.generate(hapAccessory.UUID);
// get external port allocation
const accessoryPort = await this.externalPortService.requestPort(advertiseAddress);
if (this.publishedExternalAccessories.has(advertiseAddress)) {
throw new Error(`Accessory ${hapAccessory.displayName} experienced an address collision.`);
}
else {
this.publishedExternalAccessories.set(advertiseAddress, accessory);
}
const plugin = this.pluginManager.getPlugin(accessory._associatedPlugin);
if (plugin) {
hapAccessory.on("characteristic-warning" /* AccessoryEventTypes.CHARACTERISTIC_WARNING */, BridgeService.printCharacteristicWriteWarning.bind(this, plugin, hapAccessory, { ignoreSlow: true }));
}
else if (pluginManager_1.PluginManager.isQualifiedPluginIdentifier(accessory._associatedPlugin)) {
// we did already complain in api.ts if it wasn't a qualified name
log.warn("A platform configured a external accessory under the plugin name '%s'. However no loaded plugin could be found for the name!", accessory._associatedPlugin);
}
hapAccessory.on("listening" /* AccessoryEventTypes.LISTENING */, (port) => {
log.success("%s is running on port %s.", hapAccessory.displayName, port);
log.info("Please add [%s] manually in Home app. Setup Code: %s", hapAccessory.displayName, accessoryPin);
});
// noinspection JSDeprecatedSymbols
const publishInfo = {
username: advertiseAddress,
pincode: accessoryPin,
category: accessory.category,
port: accessoryPort,
bind: this.bridgeConfig.bind,
mdns: this.config.mdns, // this is deprecated and not used anymore
addIdentifyingMaterial: true,
advertiser: this.bridgeConfig.advertiser,
};
log.debug("Publishing external accessory (name: %s, publishInfo: %o).", hapAccessory.displayName, BridgeService.strippingPinCode(publishInfo));
hapAccessory.publish(publishInfo, this.allowInsecureAccess);
}
}
createHAPAccessory(plugin, accessoryInstance, displayName, accessoryType, uuidBase) {
const services = (accessoryInstance.getServices() || [])
.filter(service => !!service); // filter out undefined values; a common mistake
const controllers = (accessoryInstance.getControllers && accessoryInstance.getControllers() || [])
.filter(controller => !!controller);
if (services.length === 0 && controllers.length === 0) { // check that we only add valid accessory with at least one service
return undefined;
}
if (!(services[0] instanceof hap_nodejs_1.Service)) {
// The returned "services" for this accessory is assumed to be the old style: a big array
// of JSON-style objects that will need to be parsed by HAP-NodeJS's AccessoryLoader.
return hap_nodejs_1.AccessoryLoader.parseAccessoryJSON({
displayName: displayName,
services: services,
});
}
else {
// The returned "services" for this accessory are simply an array of new-API-style
// Service instances which we can add to a created HAP-NodeJS Accessory directly.
const accessoryUUID = hap_nodejs_1.uuid.generate(accessoryType + ":" + (uuidBase || displayName));
const accessory = new hap_nodejs_1.Accessory(displayName, accessoryUUID);
// listen for the identify event if the accessory instance has defined an identify() method
if (accessoryInstance.identify) {
accessory.on("identify" /* AccessoryEventTypes.IDENTIFY */, (paired, callback) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
accessoryInstance.identify(() => { }); // empty callback for backwards compatibility
callback();
});
}
const informationService = accessory.getService(hap_nodejs_1.Service.AccessoryInformation);
services.forEach(service => {
// if you returned an AccessoryInformation service, merge its values with ours
if (service instanceof hap_nodejs_1.Service.AccessoryInformation) {
service.setCharacteristic(hap_nodejs_1.Characteristic.Name, displayName); // ensure display name is set
// ensure the plugin has not hooked already some listeners (some weird ones do).
// Otherwise, they would override our identify listener registered by the HAP-NodeJS accessory
service.getCharacteristic(hap_nodejs_1.Characteristic.Identify).removeAllListeners("set" /* CharacteristicEventTypes.SET */);
// pull out any values and listeners (get and set) you may have defined
informationService.replaceCharacteristicsFromService(service);
}
else {
accessory.addService(service);
}
});
accessory.on("characteristic-warning" /* AccessoryEventTypes.CHARACTERISTIC_WARNING */, BridgeService.printCharacteristicWriteWarning.bind(this, plugin, accessory, {}));
controllers.forEach(controller => {
accessory.configureController(controller);
});
return accessory;
}
}
async loadPlatformAccessories(plugin, platformInstance, platformType, logger) {
// Plugin 1.0, load accessories
return new Promise(resolve => {
// warn the user if the static platform is blocking the startup of Homebridge for to long
const loadDelayWarningInterval = setInterval(() => {
log.warn((0, logger_1.getLogPrefix)(plugin.getPluginIdentifier()), "This plugin is taking long time to load and preventing Homebridge from starting. See https://homebridge.io/w/JtMGR for more info.");
}, 20000);
platformInstance.accessories((0, hap_nodejs_1.once)((accessories) => {
// clear the load delay warning interval
clearInterval(loadDelayWarningInterval);
// loop through accessories adding them to the list and registering them
accessories.forEach((accessoryInstance, index) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const accessoryName = accessoryInstance.name; // assume this property was set
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const uuidBase = accessoryInstance.uuid_base; // optional base uuid
log.info("Initializing platform accessory '%s'...", accessoryName);
const accessory = this.createHAPAccessory(plugin, accessoryInstance, accessoryName, platformType, uuidBase);
if (accessory) {
this.bridge.addBridgedAccessory(accessory);
}
else {
logger("Platform %s returned an accessory at index %d with an empty set of services. Won't adding it to the bridge!", platformType, index);
}
});
resolve();
}));
});
}
teardown() {
this.bridge.unpublish();
for (const accessory of this.publishedExternalAccessories.values()) {
accessory._associatedHAPAccessory.unpublish();
}
this.saveCachedPlatformAccessoriesOnDisk();
this.api.signalShutdown();
}
static strippingPinCode(publishInfo) {
const info = {
...publishInfo,
};
info.pincode = "***-**-***";
return info;
}
}
exports.BridgeService = BridgeService;
//# sourceMappingURL=bridgeService.js.map