UNPKG

homebridge-philips-hue-sync-box

Version:
337 lines (307 loc) 11.4 kB
import { API, DynamicPlatformPlugin, Logging, PlatformAccessory, PlatformConfig, } from 'homebridge'; import { HueSyncBoxPlatformConfig } from './config'; import { SyncBoxClient } from './lib/client.js'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; import { State } from './state.js'; import { SyncBoxDevice } from './accessories/base.js'; import { ENTERTAINMENT_TV_ACCESSORY, INTENSITY_TV_ACCESSORY, LIGHTBULB, LIGHTBULB_ACCESSORY, MODE_TV_ACCESSORY, SWITCH, SWITCH_ACCESSORY, TV_ACCESSORY, } from './lib/constants.js'; import { SwitchDevice } from './accessories/switch.js'; import { LightbulbDevice } from './accessories/lightbulb.js'; import { TvDevice } from './accessories/tv.js'; import { ModeTvDevice } from './accessories/modeTv.js'; import { IntensityTvDevice } from './accessories/intensityTv.js'; import { EntertainmentTvDevice } from './accessories/entertainmentTv.js'; import { ApiServer } from './api-server.js'; export class HueSyncBoxPlatform implements DynamicPlatformPlugin { public readonly config: HueSyncBoxPlatformConfig; public readonly log: Logging | Console; public readonly client: SyncBoxClient; public readonly api: API; private readonly devices: Array<SyncBoxDevice> = []; private readonly accessories: Map<string, PlatformAccessory> = new Map(); private readonly existingAccessories: Map<string, PlatformAccessory> = new Map(); private readonly externalAccessories: Map<string, PlatformAccessory> = new Map(); private mainAccessory?: PlatformAccessory; private readonly TV_ACCESSORY_TYPES_TO_CATEGORY: Record<string, number>; constructor( public readonly logger: Logging, public readonly platformConfig: PlatformConfig, public readonly apiInput: API ) { if (!apiInput) { throw new Error('API is not defined'); } this.config = platformConfig as HueSyncBoxPlatformConfig; this.handleConfigDefaults(); this.api = apiInput; this.log = logger ?? console; this.log.info('Initializing platform:', this.config.name); this.TV_ACCESSORY_TYPES_TO_CATEGORY = { settopbox: this.api.hap.Categories.TV_SET_TOP_BOX, tvstick: this.api.hap.Categories.TV_STREAMING_STICK, audioreceiver: this.api.hap.Categories.AUDIO_RECEIVER, tv: this.api.hap.Categories.TELEVISION, }; if (!this.config.syncBoxIpAddress || !this.config.syncBoxApiAccessToken) { this.log.error('Missing required configuration parameters'); throw new Error('Missing required configuration parameters'); } this.client = new SyncBoxClient(this.log, this.config); this.log.info('Finished initializing platform:', this.config.name); this.api.on('didFinishLaunching', async () => { this.log.debug('Executed didFinishLaunching callback'); await this.discoverDevices(); }); if (this.config.apiServerEnabled) { const apiServer = new ApiServer(this); apiServer.start(); } } handleConfigDefaults() { this.config.updateIntervalInSeconds = this.config.updateIntervalInSeconds ?? 5; this.config.apiServerEnabled = this.config.apiServerEnabled ?? false; this.config.apiServerPort = this.config.apiServerPort ?? 40220; this.config.defaultOnMode = this.config.defaultOnMode ?? 'video'; this.config.defaultOffMode = this.config.defaultOffMode ?? 'passthrough'; this.config.baseAccessory = this.config.baseAccessory ?? 'lightbulb'; this.config.tvAccessory = this.config.tvAccessory ?? false; this.config.tvAccessoryType = this.config.tvAccessoryType ?? 'tv'; this.config.tvAccessoryLightbulb = this.config.tvAccessoryLightbulb ?? false; this.config.modeTvAccessory = this.config.modeTvAccessory ?? false; this.config.modeTvAccessoryType = this.config.modeTvAccessoryType ?? 'tv'; this.config.modeTvAccessoryLightbulb = this.config.modeTvAccessoryLightbulb ?? false; this.config.intensityTvAccessory = this.config.intensityTvAccessory ?? false; this.config.intensityTvAccessoryType = this.config.intensityTvAccessoryType ?? 'tv'; this.config.intensityTvAccessoryLightbulb = this.config.intensityTvAccessoryLightbulb ?? false; this.config.entertainmentTvAccessory = this.config.entertainmentTvAccessory ?? false; this.config.entertainmentTvAccessoryType = this.config.entertainmentTvAccessoryType ?? 'tv'; this.config.entertainmentTvAccessoryLightbulb = this.config.entertainmentTvAccessoryLightbulb ?? false; } configureAccessory(accessory: PlatformAccessory) { this.log.info('Loading accessory from cache:', accessory.context.kind); this.existingAccessories.set(accessory.UUID, accessory); } async discoverDevices() { const state = await this.client.getState(); if (!state) { throw new Error( 'Could not get state from sync box on initialization. This error is not recoverable, ' + 'ensure the sync box is online and the API token is correct and restart the plugin.' ); } this.log.debug('Discovered state:', state); const accessories = this.discoverAccessories(state); const uuids = accessories.map(accessory => { const uuid = accessory.UUID; // see if an accessory with the same uuid has already been registered and restored from const existingAccessory = this.existingAccessories.get(uuid); const isMainAccessory = accessory.UUID === this.mainAccessory?.UUID; if (existingAccessory) { this.log.debug( 'Restoring existing accessory from cache: ', existingAccessory.context.kind ); const device = this.createDevice(existingAccessory, state); this.accessories.set(accessory.UUID, existingAccessory); this.devices.push(device); if (isMainAccessory) { this.api.updatePlatformAccessories([existingAccessory]); } } else { this.log.info('Registering new accessory:', accessory.context.kind); const device = this.createDevice(accessory, state); this.devices.push(device); this.accessories.set(accessory.UUID, accessory); if (isMainAccessory) { this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ accessory, ]); } } return uuid; }); this.existingAccessories.forEach(existingAccessory => { if (!uuids.includes(existingAccessory.UUID)) { this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ existingAccessory, ]); this.log.info( 'Removing existing accessory from cache:', existingAccessory.context.kind ); } }); this.log.debug( `Publishing external ${Array.from(this.externalAccessories.values()).length} accessories` ); this.api.publishExternalAccessories( PLUGIN_NAME, Array.from(this.externalAccessories.values()) ); this.log.debug(`Discovered ${this.devices.length} devices`); await this.update(state); setInterval(async () => { const state = await this.client.getState(); await this.update(state); }, this.config.updateIntervalInSeconds * 1000); } async update(state: State | null) { this.log.debug('Updating state called'); if (!state) { this.log.warn('Could not get state from sync box. Skipping update.'); return; } for (const device of this.devices) { this.log.debug('Updating device:', device.accessory.displayName); device.update(state); } } private discoverAccessories(state: State): PlatformAccessory[] { this.log.debug( 'Loaded existing accessories:', this.existingAccessories.size ); this.log.debug('Discovering accessories'); const accessories: PlatformAccessory[] = []; if ( this.config.baseAccessory === LIGHTBULB || this.config.baseAccessory === SWITCH ) { this.mainAccessory = this.createPlatformAccessory( state, this.config.baseAccessory === LIGHTBULB ? LIGHTBULB_ACCESSORY : SWITCH_ACCESSORY ); accessories.push(this.mainAccessory); } if (this.config.tvAccessory) { const tvAccessory = this.createTvAccessory( state, TV_ACCESSORY, this.config.tvAccessoryType ); accessories.push(tvAccessory); } if (this.config.modeTvAccessory) { const accessory = this.createTvAccessory( state, MODE_TV_ACCESSORY, this.config.modeTvAccessoryType ); accessories.push(accessory); } if (this.config.intensityTvAccessory) { const accessory = this.createTvAccessory( state, INTENSITY_TV_ACCESSORY, this.config.intensityTvAccessoryType ); accessories.push(accessory); } if (this.config.entertainmentTvAccessory) { const accessory = this.createTvAccessory( state, ENTERTAINMENT_TV_ACCESSORY, this.config.entertainmentTvAccessoryType ); accessories.push(accessory); } this.log.debug(`Discovered ${accessories.length} accessories`); return accessories; } private createPlatformAccessory(state: State, kind: string) { this.log.debug('Creating new accessory with kind ' + kind + '.'); const uuidSeed = this.config.uuidSeed ?? ''; const accessory = new this.api.platformAccessory( state.device.name, this.api.hap.uuid.generate(kind + uuidSeed) ); if (kind === LIGHTBULB_ACCESSORY) { this.mainAccessory = accessory; accessory.category = this.api.hap.Categories.LIGHTBULB; } else if (kind === SWITCH_ACCESSORY) { this.mainAccessory = accessory; accessory.category = this.api.hap.Categories.SWITCH; } accessory.context.kind = kind; return accessory; } private createTvAccessory( state: State, accessoryName: string, accessoryType: string ) { const accessory = this.createPlatformAccessory(state, accessoryName); accessory.category = this.TV_ACCESSORY_TYPES_TO_CATEGORY[accessoryType.toLowerCase()] ?? this.api.hap.Categories.TELEVISION; this.externalAccessories.set(accessory.UUID, accessory); this.log.debug( 'Created TV named ' + accessoryName + ' with type ' + accessoryType + ' and category ' + accessory.category ); return accessory; } private createDevice( accessory: PlatformAccessory, state: State ): SyncBoxDevice { const type = accessory.context.kind; switch (type) { case SWITCH_ACCESSORY: return new SwitchDevice(this, accessory, state); case LIGHTBULB_ACCESSORY: return new LightbulbDevice(this, accessory, state); case TV_ACCESSORY: return new TvDevice(this, accessory, state, this.mainAccessory); case MODE_TV_ACCESSORY: return new ModeTvDevice(this, accessory, state, this.mainAccessory); case INTENSITY_TV_ACCESSORY: return new IntensityTvDevice( this, accessory, state, this.mainAccessory ); case ENTERTAINMENT_TV_ACCESSORY: return new EntertainmentTvDevice( this, accessory, state, this.mainAccessory ); default: throw new Error('Unknown accessory type: ' + type); } } }