UNPKG

homebridge-soundtouch-plugins

Version:
447 lines (419 loc) 14 kB
import { PlatformAccessory, Service } from 'homebridge' import { ContentItem, KeyValue, SourceStatus } from '@bossoq/soundtouch-api' import { deviceIsOn } from './sound-touch-device' import { SoundTouchVolume } from './sound-touch-volume' import { SoundTouchLightbulbVolume } from './sound-touch-lightbulb-volume' import { SoundTouchSpeakerVolume } from './sound-touch-speaker-volume' import { callbackify, HomebridgeAccessoryWrapper } from './utils' import { DeviceOnOffListener, HomebridgeContextProps, SoundTouchDevice, VolumeMode, } from './types' 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.speaker) { SoundTouchLightbulbVolume.clearServices(this) return new SoundTouchSpeakerVolume(this.device, this) } if (this.device.volumeSettings.mode === VolumeMode.lightbulb) { SoundTouchSpeakerVolume.clearServices(this) return new SoundTouchLightbulbVolume(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 ? 'On' : '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 = 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() } } const _sourceToServiceType = (source: string, account?: string): string => { return `${source.toLowerCase()}${account || ''}Service` } const _presetIndexToServiceType = (index: number): string => { return `preset${index}Service` }