UNPKG

homebridge-philips-hue-sync-box

Version:
295 lines 14.4 kB
import { SyncBoxClient } from './lib/client.js'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; import { ENTERTAINMENT_TV_ACCESSORY, INTENSITY_TV_ACCESSORY, LIGHTBULB, LIGHTBULB_ACCESSORY, MODE_TV_ACCESSORY, POWER_SWITCH_ACCESSORY, SWITCH, SWITCH_ACCESSORY, TV_ACCESSORY, DEFAULT_ON_MODE, DEFAULT_OFF_MODE, DEFAULT_UPDATE_INTERVAL_SECONDS, DEFAULT_API_SERVER_PORT, DEFAULT_BASE_ACCESSORY, DEFAULT_TV_ACCESSORY_TYPE, TV_TYPE_SET_TOP_BOX, TV_TYPE_STREAMING_STICK, TV_TYPE_AUDIO_RECEIVER, TV_TYPE_TV, } from './lib/constants.js'; import { SwitchDevice } from './accessories/switch.js'; import { LightbulbDevice } from './accessories/lightbulb.js'; import { PowerSwitchDevice } from './accessories/powerSwitch.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 { logger; platformConfig; apiInput; config; log; client; api; devices = []; accessories = new Map(); existingAccessories = new Map(); externalAccessories = new Map(); mainAccessory; TV_ACCESSORY_TYPES_TO_CATEGORY; constructor(logger, platformConfig, apiInput) { this.logger = logger; this.platformConfig = platformConfig; this.apiInput = apiInput; if (!apiInput) { throw new Error('API is not defined'); } this.config = platformConfig; this.handleConfigDefaults(); this.api = apiInput; this.log = logger ?? console; this.log.info('Initializing platform:', this.config.name); this.TV_ACCESSORY_TYPES_TO_CATEGORY = { [TV_TYPE_SET_TOP_BOX]: 35 /* this.api.hap.Categories.TV_SET_TOP_BOX */, [TV_TYPE_STREAMING_STICK]: 36 /* this.api.hap.Categories.TV_STREAMING_STICK */, [TV_TYPE_AUDIO_RECEIVER]: 34 /* this.api.hap.Categories.AUDIO_RECEIVER */, [TV_TYPE_TV]: 31 /* 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'); // A rejection here would otherwise escape this async event listener as // an unhandled rejection, which Homebridge treats as fatal to the // entire process rather than just this plugin - the exact failure mode // this class of fix is meant to close off. try { await this.discoverDevices(); } catch (e) { this.log.error('Failed to discover devices on startup.', e); } }); if (this.config.apiServerEnabled) { const apiServer = new ApiServer(this); apiServer.start(); } } handleConfigDefaults() { this.config.updateIntervalInSeconds = this.config.updateIntervalInSeconds ?? DEFAULT_UPDATE_INTERVAL_SECONDS; this.config.apiServerEnabled = this.config.apiServerEnabled ?? false; this.config.apiServerPort = this.config.apiServerPort ?? DEFAULT_API_SERVER_PORT; this.config.defaultOnMode = this.config.defaultOnMode ?? DEFAULT_ON_MODE; this.config.defaultOffMode = this.config.defaultOffMode ?? DEFAULT_OFF_MODE; this.config.baseAccessory = this.config.baseAccessory ?? DEFAULT_BASE_ACCESSORY; this.config.powerSwitchAccessory = this.config.powerSwitchAccessory ?? false; this.config.tvAccessory = this.config.tvAccessory ?? false; this.config.tvAccessoryType = this.config.tvAccessoryType ?? DEFAULT_TV_ACCESSORY_TYPE; this.config.tvAccessoryLightbulb = this.config.tvAccessoryLightbulb ?? false; this.config.modeTvAccessory = this.config.modeTvAccessory ?? false; this.config.modeTvAccessoryType = this.config.modeTvAccessoryType ?? DEFAULT_TV_ACCESSORY_TYPE; this.config.modeTvAccessoryLightbulb = this.config.modeTvAccessoryLightbulb ?? false; this.config.intensityTvAccessory = this.config.intensityTvAccessory ?? false; this.config.intensityTvAccessoryType = this.config.intensityTvAccessoryType ?? DEFAULT_TV_ACCESSORY_TYPE; this.config.intensityTvAccessoryLightbulb = this.config.intensityTvAccessoryLightbulb ?? false; this.config.entertainmentTvAccessory = this.config.entertainmentTvAccessory ?? false; this.config.entertainmentTvAccessoryType = this.config.entertainmentTvAccessoryType ?? DEFAULT_TV_ACCESSORY_TYPE; this.config.entertainmentTvAccessoryLightbulb = this.config.entertainmentTvAccessoryLightbulb ?? false; } // Omits WiFi SSID and LAN IP addresses from debug output, since debug logs are // routinely pasted into public support requests/issues. redactStateForLogging(state) { return { ...state, device: { ...state.device, ipAddress: '[REDACTED]', wifi: { ...state.device.wifi, ssid: '[REDACTED]' }, }, hue: { ...state.hue, bridgeIpAddress: '[REDACTED]', }, }; } configureAccessory(accessory) { 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:', this.redactStateForLogging(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; const isPowerSwitch = accessory.context.kind === POWER_SWITCH_ACCESSORY; 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 || isPowerSwitch) { 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 || isPowerSwitch) { 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); this.schedulePolling(); } schedulePolling() { let polling = false; setInterval(async () => { // setInterval doesn't wait for a previous tick's promise to settle. // If one getState() call runs long (retries, a slow response), firing // another on top of it just queues up behind the same client-side // lock instead of skipping the tick, and the queue can grow without // bound on a short poll interval. Skip overlapping ticks instead. if (polling) { this.log.debug('Skipping poll tick, previous update still in progress'); return; } polling = true; try { const state = await this.client.getState(); await this.update(state); } finally { polling = false; } }, this.config.updateIntervalInSeconds * 1000); } async update(state) { 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); } } discoverAccessories(state) { this.log.debug('Loaded existing accessories:', this.existingAccessories.size); this.log.debug('Discovering accessories'); const accessories = []; 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.powerSwitchAccessory) { const powerSwitchAccessory = this.createPlatformAccessory(state, POWER_SWITCH_ACCESSORY); accessories.push(powerSwitchAccessory); } 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; } createPlatformAccessory(state, kind) { this.log.debug('Creating new accessory with kind ' + kind + '.'); const uuidSeed = this.config.uuidSeed ?? ''; const accessory = new this.api.platformAccessory(kind === POWER_SWITCH_ACCESSORY ? state.device.name + ' Power' : state.device.name, this.api.hap.uuid.generate(kind + uuidSeed)); if (kind === LIGHTBULB_ACCESSORY) { this.mainAccessory = accessory; accessory.category = 5 /* this.api.hap.Categories.LIGHTBULB */; } else if (kind === SWITCH_ACCESSORY) { this.mainAccessory = accessory; accessory.category = 8 /* this.api.hap.Categories.SWITCH */; } else if (kind === POWER_SWITCH_ACCESSORY) { accessory.category = 8 /* this.api.hap.Categories.SWITCH */; } accessory.context.kind = kind; return accessory; } createTvAccessory(state, accessoryName, accessoryType) { const accessory = this.createPlatformAccessory(state, accessoryName); accessory.category = this.TV_ACCESSORY_TYPES_TO_CATEGORY[accessoryType.toLowerCase()] ?? 31 /* 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; } createDevice(accessory, state) { 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 POWER_SWITCH_ACCESSORY: return new PowerSwitchDevice(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); } } } //# sourceMappingURL=platform.js.map