homebridge-soundtouch-plugins
Version:
Homebridge SoundTouch plugins
447 lines (419 loc) • 14 kB
text/typescript
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`
}