UNPKG

homebridge-soundtouch-platform

Version:
360 lines (331 loc) 15.3 kB
import {ContentItem, KeyValue, SourceStatus} from 'soundtouch-api'; import {deviceIsOn, DeviceOnOffListener, SoundTouchDevice} from './sound-touch-device'; import {SoundTouchVolume} from './sound-touch-volume'; import {SoundTouchSpeakerVolume} from './sound-touch-speaker-volume'; import {VolumeMode} from './accessory-config'; import {SoundTouchLightbulbVolume} from './sound-touch-lightbulb-volume'; import {callbackify, HomebridgeContextProps, HomebridgeAccessoryWrapper} from 'homebridge-base-platform'; import {PlatformAccessory, Service} from "homebridge"; interface SoundTouchSelectedSource { readonly sourceItem?: ContentItem; readonly presetIndex?: number; } export class SoundTouchAccessoryWrapper extends HomebridgeAccessoryWrapper<SoundTouchDevice> implements DeviceOnOffListener { private readonly volume?: SoundTouchVolume; private readonly onService: Service; private readonly presetServices: Service[]; private readonly sourceServices: Service[]; private readonly informationService: Service; private static readonly presetValues: KeyValue[] = [ KeyValue.preset1, KeyValue.preset2, KeyValue.preset3, KeyValue.preset4, KeyValue.preset5, KeyValue.preset6 ]; constructor(context: HomebridgeContextProps, accessory: PlatformAccessory, device: SoundTouchDevice) { super(context, accessory, device); this.informationService = this.initInformationService(); this.onService = this.initOnService(); this.volume = this.initVolumeService(); this.presetServices = this.initPresetServices(); this.sourceServices = this.initSourceServices(); if(this.device.verbose === true) { this.log(`[${this.getDisplayName()}] Device ready`); } if(this.device.pollingInterval !== undefined) { this._refreshDeviceServices().then(() => this.log('Infinite loop')); } } public getDisplayName(): string { return this.device.name; } private initVolumeService(): SoundTouchVolume | undefined { if(this.device.volumeSettings.mode !== VolumeMode.none) { if(this.device.volumeSettings.mode === VolumeMode.lightbulb) { SoundTouchSpeakerVolume.clearServices(this); return new SoundTouchLightbulbVolume(this.device, this); } if(this.device.volumeSettings.mode === VolumeMode.speaker) { SoundTouchLightbulbVolume.clearServices(this); return new SoundTouchSpeakerVolume(this.device, this); } } SoundTouchLightbulbVolume.clearServices(this); SoundTouchSpeakerVolume.clearServices(this); return undefined; } private initOnService(): Service { const onService = this.getService(this.Service.Switch, this.getDisplayName(), 'onService'); onService .getCharacteristic(this.Characteristic.On) .on('get', callbackify(async () => { const isOn = await deviceIsOn(this.device); if(this.device.verbose) { this.log(`[${this.getDisplayName()}] ${isOn ? 'Is on' : 'Is off'}`); } return isOn; })) .on('set', callbackify(this.setOn.bind(this))); return onService; } private initPresetServices(): Service[] { const presetServices = []; for(let i = 1; i <= 6; i++) { const presetType = _presetIndexToServiceType(i); const preset = this.device.presets.find((p) => p.index === i); if(preset === undefined) { this.removeService(this.Service.Switch, presetType); continue; } const service = this.getService(this.Service.Switch, preset.name, presetType); const characteristic = service.getCharacteristic(this.Characteristic.On); characteristic.on('get', callbackify(() => this.isSelectedPreset(preset.index))); characteristic.on('set', callbackify((on: boolean) => this.setPreset(on, preset.index))); presetServices.push(service); } return presetServices; } private initSourceServices(): Service[] { const sourceServices = []; for(let src of this.device.sources) { const sourceType = _sourceToServiceType(src.source, src.account); if(src.enabled === false) { this.removeService(this.Service.Switch, sourceType); continue; } const service = this.getService(this.Service.Switch, src.name, sourceType); const characteristic = service.getCharacteristic(this.Characteristic.On); characteristic.on('get', callbackify(() => this.isSelectedSource(src.source, src.account))); characteristic.on('set', callbackify((on: boolean) => this.setSource(on, src.source, src.account))); sourceServices.push(service); } return sourceServices; } private initInformationService(): Service { const informationService = this.accessory.getService(this.Service.AccessoryInformation); informationService .setCharacteristic(this.Characteristic.Name, this.getDisplayName()) .setCharacteristic(this.Characteristic.Manufacturer, 'Bose') .setCharacteristic(this.Characteristic.Model, this.device.model) .setCharacteristic(this.Characteristic.SerialNumber, this.device.id); if(this.device.version) { informationService.setCharacteristic(this.Characteristic.FirmwareRevision, this.device.version); } return informationService; }; public async setOn(on: boolean, updateOn?: boolean): Promise<boolean> { const nowPlaying = await this.device.api.getNowPlaying(); let success = false; if(nowPlaying.source === SourceStatus.standBy) { if(on) { success = await this.device.api.pressKey(KeyValue.power); if(!success) { return false; } success = await this.deviceDidTurnOn(updateOn, true); if(!success) { return false; } await new Promise((resolve => setTimeout(resolve, 500))); const selectedSource = await this.getSelectedSource(); this.switchSelectedSource(on, selectedSource); } } else if(!on) { const selectedSource = await this.getSelectedSource(); success = await this.device.api.pressKey(KeyValue.power); if(!success) { return false; } success = await this.deviceDidTurnOff(updateOn, true); this.switchSelectedSource(on, selectedSource); } return success; } private async getSelectedSource(): Promise<SoundTouchSelectedSource | undefined> { const nowPlaying = await this.device.api.getNowPlaying(); if(nowPlaying.source === SourceStatus.standBy) { return undefined; } const presets = await this.device.api.getPresets(); for(const preset of presets) { if (JSON.stringify(preset.contentItem) === JSON.stringify(nowPlaying.contentItem)) { return { presetIndex: preset.id } } } return { sourceItem: { source: nowPlaying.source, sourceAccount: nowPlaying.sourceAccount } }; } private async isSelectedPreset(index: number): Promise<boolean> { const selectedPreset = await this.getSelectedSource(); if(selectedPreset !== undefined) { if(selectedPreset.presetIndex === index) { if(this.device.verbose) { this.log(`[${this.getDisplayName()}] Current preset n°${index}`); } return true; } } return false; } private async setPreset(on: boolean, index: number): Promise<boolean> { let success = false; if(on) { const isOn = await deviceIsOn(this.device); if (!isOn) { success = await this.setOn(on, true); if (!success) { return false; } } const selectedSource: SoundTouchSelectedSource = await this.getSelectedSource(); success = await this.device.api.pressKey(SoundTouchAccessoryWrapper.presetValues[index - 1]); if (!success) { return false; } if (success) { if(this.device.verbose) { this.log(`[${this.getDisplayName()}] Set preset n°${index}`); } this.switchSelectedSource(false, selectedSource); } } else { success = await this.setOn(on, true); } return success; } public async deviceDidTurnOn(updateOn?: boolean, updateVolume?: boolean): Promise<boolean> { let success = true; if(this.device.verbose) { this.log(`[${this.getDisplayName()}] Turn on`); } if(updateOn === true) { this.onService.getCharacteristic(this.Characteristic.On).updateValue(true); if(this.volume !== undefined) { this.volume.getMuteCharacteristic().updateValue(true); } } if(this.volume !== undefined && updateVolume === true && this.device.volumeSettings.onValue >= 0) { success = await this.volume.setVolume(this.device.volumeSettings.onValue, true); } return success; } public async deviceDidTurnOff(updateOn?: boolean, updateVolume?: boolean): Promise<boolean> { if(this.device.verbose) { this.log(`[${this.getDisplayName()}] Turn off`); } if(updateOn === true) { this.onService.getCharacteristic(this.Characteristic.On).updateValue(false); } if(updateVolume === true) { this.volume.getMuteCharacteristic().updateValue(false); } return true; } private switchSelectedSource(on: boolean, selectedSource?: SoundTouchSelectedSource): void { if(selectedSource) { if(selectedSource.presetIndex !== undefined) { this.switchPresetService(on, selectedSource.presetIndex); } else if(selectedSource.sourceItem !== undefined) { this.switchSourceService(on, selectedSource.sourceItem.source, selectedSource.sourceItem.sourceAccount); } } } private switchService(on: boolean, type: string, services: Service[]) { for(const service of services) { if(service.subtype === type) { service.getCharacteristic(this.Characteristic.On).updateValue(on); return; } } } private switchPresetService(on: boolean, index: number): void { this.switchService(on, _presetIndexToServiceType(index), this.presetServices); } private switchSourceService(on: boolean, source: string, account?: string): void { this.switchService(on, _sourceToServiceType(source, account), this.sourceServices); } private async isSelectedSource(source: string, account: string): Promise<boolean> { const selectedSource = await this.getSelectedSource(); if(selectedSource !== undefined) { const sourceItem = selectedSource.sourceItem; if(sourceItem !== undefined && sourceItem.source === source && sourceItem.sourceAccount === account) { if(this.device.verbose) { this.log(`[${this.getDisplayName()}] Current source: '${source}', account: '${account}'`); } return true; } } return false; } private async setSource(on: boolean, source: string, account: string): Promise<boolean> { let success = false; if(on) { const isOn = await deviceIsOn(this.device); if(!isOn) { success = await this.setOn(on, true); if(!success) { return false; } } const selectedSource = await this.getSelectedSource(); success = await this.device.api.selectSource({ source: source, sourceAccount: account }); if(success) { if(this.device.verbose) { this.log(`[${this.getDisplayName()}] Select source: '${source}', account: '${account}'`); } this.switchSelectedSource(false, selectedSource); } } else { success = await this.setOn(on,true); } return success; } private async _refreshDeviceServices(): Promise<void> { await new Promise(((resolve) => setTimeout(resolve, this.device.pollingInterval))); const selectedSource = await this.getSelectedSource(); const onCharacteristic = this.onService.getCharacteristic(this.Characteristic.On); if(selectedSource !== undefined) { onCharacteristic.updateValue(true); const presetType = selectedSource.presetIndex !== undefined ? _presetIndexToServiceType(selectedSource.presetIndex) : undefined; this.presetServices.forEach((service) => { const presetOnCharacteristic = service.getCharacteristic(this.Characteristic.On); presetOnCharacteristic.updateValue(service.subtype === presetType); }); const sourceType = selectedSource.sourceItem !== undefined ? _sourceToServiceType(selectedSource.sourceItem.source, selectedSource.sourceItem.sourceAccount) : undefined; this.sourceServices.forEach((service) => { const sourceOnCharacteristic = service.getCharacteristic(this.Characteristic.On); sourceOnCharacteristic.updateValue(service.subtype === sourceType); }); const deviceVolume = await this.device.api.getVolume(); if(this.volume) { this.volume.getVolumeCharacteristic().updateValue(deviceVolume.actual); this.volume.getMuteCharacteristic().updateValue(!deviceVolume.isMuted); } } else { onCharacteristic.setValue(false); this.presetServices.forEach((service) => service.getCharacteristic(this.Characteristic.On).updateValue(false)); this.sourceServices.forEach((service) => service.getCharacteristic(this.Characteristic.On).updateValue(false)); if(this.volume) { this.volume.getMuteCharacteristic().updateValue(false); } } return this._refreshDeviceServices(); } } function _sourceToServiceType(source: string, account?: string): string { return `${source.toLowerCase()}${account || ''}Service`; } function _presetIndexToServiceType(index: number): string { return `preset${index}Service`; }