import * as mac from "./util/mac" ; import getVersion from "./version" ; import { PluginManager } from "./pluginManager" ; import { StorageService } from "./storageService" ; import { Logger , Logging , getLogPrefix } from "./logger" ; import { Plugin } from "./plugin" ; import { PlatformAccessory , SerializedPlatformAccessory , } from "./platformAccessory" ; import { Accessory , AccessoryEventTypes , AccessoryLoader , Bridge , Categories , Characteristic , CharacteristicEventTypes , CharacteristicWarning , CharacteristicWarningType , InterfaceName , IPAddress , MacAddress , once , PublishInfo , Service , uuid , VoidCallback , MDNSAdvertiser , HAPLibraryVersion , } from "hap-nodejs" ; import { AccessoryIdentifier , AccessoryName , AccessoryPlugin , HomebridgeAPI , InternalAPIEvent , PlatformIdentifier , PlatformName , PluginIdentifier , StaticPlatformPlugin , } from "./api" ; import { ExternalPortService , ExternalPortsConfiguration , } from "./externalPortService" ; import { HomebridgeOptions } from "./server" ; const log = Logger . internal ; export interface BridgeConfiguration { name : string ; username : MacAddress ; pin : string ; advertiser : MDNSAdvertiser ; port ? : number ; bind ? : ( InterfaceName | IPAddress ) | ( InterfaceName | IPAddress ) [ ] ; setupID ? : string [ 4 ] ; manufacturer ? : string ; model ? : string ; disableIpc ? : boolean ; } export interface AccessoryConfig extends Record < string , any > { accessory : AccessoryName | AccessoryIdentifier ; name : string ; uuid_base ? : string ; _bridge ? : BridgeConfiguration ; } export interface PlatformConfig extends Record < string , any > { platform : PlatformName | PlatformIdentifier ; name ? : string ; _bridge ? : BridgeConfiguration ; } export interface HomebridgeConfig { bridge : BridgeConfiguration ; deprecated mdns ? : any ; accessories : AccessoryConfig [ ] ; platforms : PlatformConfig [ ] ; plugins ? : PluginIdentifier [ ] ; /**
     * Array of disabled plugins.
     * Unlike the plugins[] config which prevents plugins from being initialised at all, disabled plugins still have their alias loaded so
     * we can match config blocks of disabled plugins and show an appropriate message in the logs.
     */ disabledPlugins ? : PluginIdentifier [ ] ; ports ? : ExternalPortsConfiguration ; } export interface BridgeOptions extends HomebridgeOptions { cachedAccessoriesDir : string ; cachedAccessoriesItemName : string ; } export interface CharacteristicWarningOpts { ignoreSlow ? : boolean ; } export class BridgeService { public bridge : Bridge ; private storageService : StorageService ; private readonly allowInsecureAccess : boolean ; private cachedPlatformAccessories : PlatformAccessory [ ] = [ ] ; private cachedAccessoriesFileLoaded = false ; private readonly publishedExternalAccessories : Map < MacAddress , PlatformAccessory > = new Map ( ) ; constructor ( private api , private pluginManager , private externalPortService , private bridgeOptions , private bridgeConfig , private config ) { this . storageService = new StorageService ( this . bridgeOptions . cachedAccessoriesDir ) ; this . storageService . initSync ( ) ; this . allowInsecureAccess = this . bridgeOptions . insecureAccess || false ; this . api . on ( InternalAPIEvent . REGISTER_PLATFORM_ACCESSORIES , this . handleRegisterPlatformAccessories . bind ( this ) ) ; this . api . on ( InternalAPIEvent . UPDATE_PLATFORM_ACCESSORIES , this . handleUpdatePlatformAccessories . bind ( this ) ) ; this . api . on ( InternalAPIEvent . UNREGISTER_PLATFORM_ACCESSORIES , this . handleUnregisterPlatformAccessories . bind ( this ) ) ; this . api . on ( InternalAPIEvent . PUBLISH_EXTERNAL_ACCESSORIES , this . handlePublishExternalAccessories . bind ( this ) ) ; this . bridge = new Bridge ( bridgeConfig . name , uuid . generate ( "HomeBridge" ) ) ; this . bridge . on ( AccessoryEventTypes . CHARACTERISTIC_WARNING , ( ) => { } ) ; } public static printCharacteristicWriteWarning ( plugin , accessory , opts , warning ) { const wikiInfo = "See https://git.io/JtMGR for more info." ; switch ( warning . type ) { case CharacteristicWarningType . SLOW_READ : case CharacteristicWarningType . SLOW_WRITE : if ( ! opts . ignoreSlow ) { log . info ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , "This plugin slows down Homebridge." , warning . message , wikiInfo ) ; } break ; case CharacteristicWarningType . TIMEOUT_READ : case CharacteristicWarningType . TIMEOUT_WRITE : log . error ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , "This plugin slows down Homebridge." , warning . message , wikiInfo ) ; break ; case CharacteristicWarningType . WARN_MESSAGE : log . info ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , `This plugin generated a warning from the characteristic '${ warning . characteristic . displayName }':` , warning . message + "." , wikiInfo ) ; break ; case CharacteristicWarningType . ERROR_MESSAGE : log . error ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , `This plugin threw an error from the characteristic '${ warning . characteristic . displayName }':` , warning . message + "." , wikiInfo ) ; break ; case CharacteristicWarningType . DEBUG_MESSAGE : log . debug ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , `Characteristic '${ warning . characteristic . displayName }':` , warning . message + "." , wikiInfo ) ; break ; default : log . info ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , `This plugin generated a warning from the characteristic '${ warning . characteristic . displayName }':` , warning . message + "." , wikiInfo ) ; break ; } if ( warning . stack ) { log . debug ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , warning . stack ) ; } } public publishBridge ( ) { const bridgeConfig = this . bridgeConfig ; const info = this . bridge . getService ( Service . AccessoryInformation ) ! ; info . setCharacteristic ( Characteristic . Manufacturer , bridgeConfig . manufacturer || "homebridge.io" ) ; info . setCharacteristic ( Characteristic . Model , bridgeConfig . model || "homebridge" ) ; info . setCharacteristic ( Characteristic . SerialNumber , bridgeConfig . username ) ; info . setCharacteristic ( Characteristic . FirmwareRevision , getVersion ( ) ) ; this . bridge . on ( AccessoryEventTypes . LISTENING , ( port ) => { log . info ( "Homebridge v%s (HAP v%s) (%s) is running on port %s." , getVersion ( ) , HAPLibraryVersion ( ) , bridgeConfig . name , port ) ; } ) ; const publishInfo = { username : bridgeConfig . username , port : bridgeConfig . port , pincode : bridgeConfig . pin , category : Categories . BRIDGE , bind : bridgeConfig . bind , mdns : this . config . mdns , 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.
     */ public async loadCachedPlatformAccessoriesFromDisk ( ) { let cachedAccessories = null ; try { cachedAccessories = await this . storageService . getItem < SerializedPlatformAccessory [ ] > ( this . bridgeOptions . cachedAccessoriesItemName ) ; } catch ( e ) { log . error ( "Failed to load cached accessories from disk:" , e . message ) ; if ( e instanceof SyntaxError ) { 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 . deserialize ( serialized ) ; } ) ; if ( cachedAccessories . length ) { await this . createCachedAccessoriesBackup ( ) ; } } this . cachedAccessoriesFileLoaded = true ; } /**
     * Return the name of the backup cache file
     */ private 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
     */ private 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
     */ private async restoreCachedAccessoriesBackup ( ) { try { const cachedAccessories = await this . storageService . getItem < SerializedPlatformAccessory [ ] > ( this . backupCacheFileName ) ; if ( cachedAccessories && cachedAccessories . length ) { log . warn ( `Recovered ${ cachedAccessories . length } accessories from ${ this . bridgeOptions . cachedAccessoriesItemName } cache backup.` ) ; } return cachedAccessories ; } catch ( e ) { return null ; } } public restoreCachedPlatformAccessories ( ) { this . cachedPlatformAccessories = this . cachedPlatformAccessories . filter ( accessory => { let plugin = this . pluginManager . getPlugin ( accessory . _associatedPlugin ! ) ; if ( ! plugin ) { try { plugin = this . pluginManager . getPluginByActiveDynamicPlatform ( accessory . _associatedPlatform ! ) ; if ( plugin ) { 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 ( ) ; } } catch ( error ) { 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 ( 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 ; } } else { accessory . getService ( Service . AccessoryInformation ) ! . setCharacteristic ( Characteristic . FirmwareRevision , plugin ! . version ) ; platformPlugins . configureAccessory ( accessory ) ; } try { this . bridge . addBridgedAccessory ( accessory . _associatedHAPAccessory ) ; } catch ( e ) { log . warn ( `${ accessory . _associatedPlugin ? getLogPrefix ( accessory . _associatedPlugin ) : "" } Could not restore cached accessory '${ accessory . _associatedHAPAccessory . displayName }':` , e ?. message ) ; return false ; } return true ; } ) ; } /**
     * Save the cached accessories back to disk.
     */ public saveCachedPlatformAccessoriesOnDisk ( ) { try { if ( this . cachedAccessoriesFileLoaded ) { const serializedAccessories = this . cachedPlatformAccessories . map ( accessory => 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 informationService = accessory . getService ( Service . AccessoryInformation ) ! ; if ( informationService . getCharacteristic ( Characteristic . FirmwareRevision ) . value === "0.0.0" ) { informationService . setCharacteristic ( Characteristic . FirmwareRevision , plugin . version ) ; } 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 ( 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 ) { 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 ) ; 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 ) { const informationService = hapAccessory . getService ( Service . AccessoryInformation ) ! ; if ( informationService . getCharacteristic ( Characteristic . FirmwareRevision ) . value === "0.0.0" ) { informationService . setCharacteristic ( Characteristic . FirmwareRevision , plugin . version ) ; } hapAccessory . on ( AccessoryEventTypes . CHARACTERISTIC_WARNING , BridgeService . printCharacteristicWriteWarning . bind ( this , plugin , hapAccessory , { ignoreSlow : true } ) ) ; } else if ( PluginManager . isQualifiedPluginIdentifier ( accessory . _associatedPlugin ! ) ) { 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 ( AccessoryEventTypes . LISTENING , ( port ) => { log . info ( "%s is running on port %s." , hapAccessory . displayName , port ) ; log . info ( "Please add [%s] manually in Home app. Setup Code: %s" , hapAccessory . displayName , accessoryPin ) ; } ) ; const publishInfo = { username : advertiseAddress , pincode : accessoryPin , category : accessory . category , port : accessoryPort , bind : this . bridgeConfig . bind , mdns : this . config . mdns , 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 ) ; } } public createHAPAccessory ( plugin , accessoryInstance , displayName , accessoryType , uuidBase ? ) { const services = ( accessoryInstance . getServices ( ) || [ ] ) . filter ( service => ! ! service ) ; const controllers = ( accessoryInstance . getControllers && accessoryInstance . getControllers ( ) || [ ] ) . filter ( controller => ! ! controller ) ; if ( services . length === 0 && controllers . length === 0 ) { return undefined ; } if ( ! ( services [ 0 ] instanceof Service ) ) { return AccessoryLoader . parseAccessoryJSON ( { displayName : displayName , services : services , } ) ; } else { const accessoryUUID = uuid . generate ( accessoryType + ":" + ( uuidBase || displayName ) ) ; const accessory = new Accessory ( displayName , accessoryUUID ) ; if ( accessoryInstance . identify ) { accessory . on ( AccessoryEventTypes . IDENTIFY , ( paired , callback ) => { accessoryInstance . identify ! ( ( ) => { } ) ; callback ( ) ; } ) ; } const informationService = accessory . getService ( Service . AccessoryInformation ) ! ; services . forEach ( service => { if ( service instanceof Service . AccessoryInformation ) { service . setCharacteristic ( Characteristic . Name , displayName ) ; service . getCharacteristic ( Characteristic . Identify ) . removeAllListeners ( CharacteristicEventTypes . SET ) ; informationService . replaceCharacteristicsFromService ( service ) ; } else { accessory . addService ( service ) ; } } ) ; if ( informationService . getCharacteristic ( Characteristic . FirmwareRevision ) . value === "0.0.0" ) { informationService . setCharacteristic ( Characteristic . FirmwareRevision , plugin . version ) ; } accessory . on ( AccessoryEventTypes . CHARACTERISTIC_WARNING , BridgeService . printCharacteristicWriteWarning . bind ( this , plugin , accessory , { } ) ) ; controllers . forEach ( controller => { accessory . configureController ( controller ) ; } ) ; return accessory ; } } public async loadPlatformAccessories ( plugin , platformInstance , platformType , logger ) { return new Promise ( resolve => { const loadDelayWarningInterval = setInterval ( ( ) => { log . warn ( getLogPrefix ( plugin . getPluginIdentifier ( ) ) , "This plugin is taking long time to load and preventing Homebridge from starting. See https://git.io/JtMGR for more info." ) ; } , 20000 ) ; platformInstance . accessories ( once ( ( accessories ) => { clearInterval ( loadDelayWarningInterval ) ; accessories . forEach ( ( accessoryInstance , index ) => { const accessoryName = accessoryInstance . name ; const uuidBase = accessoryInstance . uuid_base ; 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 ( ) ; } private static strippingPinCode ( publishInfo ) { const info = { ... publishInfo , } ; info . pincode = "***-**-***" ; return info ; } }