UNPKG

homebridge-xbox-tv

Version:

Homebridge plugin to control Xbox game consoles.

943 lines (869 loc) 68.5 kB
import EventEmitter from 'events'; import RestFul from './restful.js'; import Mqtt from './mqtt.js'; import XboxWebApi from './webApi/xboxwebapi.js'; import XboxLocalApi from './localApi/xboxlocalapi.js'; import Functions from './functions.js'; import { DefaultInputs, WebApi } from './constants.js'; let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID; class XboxDevice extends EventEmitter { constructor(api, device, authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile) { super(); Accessory = api.platformAccessory; Characteristic = api.hap.Characteristic; Service = api.hap.Service; Categories = api.hap.Categories; Encode = api.hap.encode; AccessoryUUID = api.hap.uuid; //device configuration this.device = device; this.name = device.name; this.liveId = device.xboxLiveId; this.displayType = device.displayType; this.webApiControl = device.webApi?.enable || false; this.getInputsFromDevice = device.webApi?.enable ? device.inputs?.getFromDevice : false; this.filterGames = device.inputs?.filterGames || false; this.filterApps = device.inputs?.filterApps || false; this.filterSystemApps = device.inputs?.filterSystemApps || false; this.filterDlc = device.inputs?.filterDlc || false; this.inputsDisplayOrder = device.inputs?.displayOrder || 0; this.inputs = (device.inputs?.data || []).filter(input => input.name && input.reference); this.buttons = (device.buttons ?? []).filter(button => (button.displayType ?? 0) > 0); this.sensors = Array.isArray(device.sensors) ? (device.sensors ?? []).filter(sensor => (sensor.displayType ?? 0) > 0 && (sensor.mode ?? -1) >= 0) : []; this.volumeControl = device.volume?.displayType || 0; this.volumeControlName = device.volume?.name || 'Volume'; this.volumeControlNamePrefix = device.volume?.namePrefix || false; this.infoButtonCommand = device.infoButtonCommand || 'nexus'; this.logInfo = device.log?.info || false; this.logWarn = device.log?.warn || false; this.logDebug = device.log?.debug || false; this.authTokenFile = authTokenFile; this.devInfoFile = devInfoFile; this.inputsFile = inputsFile; this.inputsNamesFile = inputsNamesFile; this.inputsTargetVisibilityFile = inputsTargetVisibilityFile; //external integrations this.restFul = device.restFul ?? {}; this.restFulConnected = false; this.mqtt = device.mqtt ?? {}; this.mqttConnected = false; this.functions = new Functions(); //sensors for (const sensor of this.sensors) { sensor.serviceType = ['', Service.MotionSensor, Service.OccupancySensor, Service.ContactSensor][sensor.displayType]; sensor.characteristicType = ['', Characteristic.MotionDetected, Characteristic.OccupancyDetected, Characteristic.ContactSensorState][sensor.displayType]; sensor.state = false; } //buttons for (const button of this.buttons) { button.reference = [button.mediaCommand, button.gamePadCommand, button.tvRemoteCommand, button.consoleControlCommand, button.gameAppControlCommand][button.mode]; button.serviceType = ['', Service.Outlet, Service.Switch][button.displayType]; button.state = false; } //variable this.modelName = 'Xbox'; this.inputIdentifier = 1; this.power = false; this.volume = 0; this.mute = false; this.playState = false; this.mediaState = 2; this.reference = ''; this.screenSaver = false; this.consoleAuthorized = false; } async setOverExternalIntegration(integration, key, value) { if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set over external integration not possible, web api not enabled`); return; } try { let set = false; switch (key) { case 'Power': switch (value) { case true: //on set = await this.xboxWebApi.send('Power', 'WakeUp'); break; case false: //off set = await this.xboxWebApi.send('Power', 'TurnOff'); break; } break; case 'App': const payload = [{ 'oneStoreProductId': value }]; set = await this.xboxWebApi.send('Shell', 'ActivateApplicationWithOneStoreProductId', payload); break; case 'Volume': switch (value) { case 'up': set = await this.xboxWebApi.send('Volume', 'Up'); break; case 'down': set = await this.xboxWebApi.send('Volume', 'Down'); break; } break; case 'Mute': switch (value) { case true: set = await this.xboxWebApi.send('Audio', 'Mute'); break; case false: set = await this.xboxWebApi.send('Audio', 'Unmute'); break; } break; case 'RcControl': set = await this.xboxWebApi.send('Shell', 'InjectKey', [{ 'keyType': value }]); break; default: if (this.logWarn) this.emit('warn', `${integration}, received key: ${key}, value: ${value}`); break; }; return set; } catch (error) { throw new Error(`${integration} set key: ${key}, value: ${value}, error: ${error}`); } } async externalIntegrations() { //RESTFul server const restFulEnabled = this.restFul.enable || false; if (restFulEnabled) { try { this.restFul1 = new RestFul({ port: this.restFul.port || 3000, logWarn: this.logWarn, logDebug: this.logDebug }) .on('connected', (message) => { this.emit('success', message); this.restFulConnected = true; }) .on('set', async (key, value) => { try { await this.setOverExternalIntegration('RESTFul', key, value); } catch (error) { if (this.logWarn) this.emit('warn', `RESTFul set error: ${error}`); } }) .on('debug', (debug) => this.emit('debug', debug)) .on('warn', (warn) => this.emit('warn', warn)) .on('error', (error) => this.emit('error', error)); } catch (error) { if (this.logWarn) this.emit('warn', `RESTFul integration start error: ${error}`); } } //mqtt client const mqttEnabled = this.mqtt.enable || false; if (mqttEnabled) { try { this.mqtt1 = new Mqtt({ host: this.mqtt.host, port: this.mqtt.port || 1883, clientId: this.mqtt.clientId ? `microsoft_${this.mqtt.clientId}_${Math.random().toString(16).slice(3)}` : `microsoft_${Math.random().toString(16).slice(3)}`, prefix: this.mqtt.prefix ? `microsoft/${this.mqtt.prefix}/${this.name}` : `microsoft/${this.name}`, user: this.mqtt.auth?.user, passwd: this.mqtt.auth?.passwd, logWarn: this.logWarn, logDebug: this.logDebug }) .on('connected', (message) => { this.emit('success', message); this.mqttConnected = true; }) .on('subscribed', (message) => { this.emit('success', message); }) .on('set', async (key, value) => { try { await this.setOverExternalIntegration('MQTT', key, value); } catch (error) { if (this.logWarn) this.emit('warn', `MQTT set error: ${error}`); } }) .on('debug', (debug) => this.emit('debug', debug)) .on('warn', (warn) => this.emit('warn', warn)) .on('error', (error) => this.emit('error', error)); } catch (error) { if (this.logWarn) this.emit('warn', `MQTT integration start error: ${error}`); } }; return true; } async prepareDataForAccessory() { try { //read dev info from file this.savedInfo = await this.functions.readData(this.devInfoFile, true) ?? {}; if (this.logDebug) this.emit('debug', `Read saved Info: ${JSON.stringify(this.savedInfo, null, 2)}`); //read inputs file this.savedInputs = await this.functions.readData(this.inputsFile, true) ?? []; if (this.logDebug) this.emit('debug', `Read saved Inputs: ${JSON.stringify(this.savedInputs, null, 2)}`); //read inputs names from file this.savedInputsNames = await this.functions.readData(this.inputsNamesFile, true) ?? {}; if (this.logDebug) this.emit('debug', `Read saved Inputs Names: ${JSON.stringify(this.savedInputsNames, null, 2)}`); //read inputs visibility from file this.savedInputsTargetVisibility = await this.functions.readData(this.inputsTargetVisibilityFile, true) ?? {}; if (this.logDebug) this.emit('debug', `Read saved Inputs Target Visibility: ${JSON.stringify(this.savedInputsTargetVisibility, null, 2)}`); return true; } catch (error) { throw new Error(`Prepare data for accessory error: ${error}`); } } async startStopImpulseGenerator(state, timers = []) { try { //start web api impulse generator if (this.webApiControl) await this.xboxWebApi.impulseGenerator.state(true, [{ name: 'checkAuthorization', sampling: 900000 }]); //start impulse generator await this.xboxLocalApi.impulseGenerator.state(state, timers); return true; } catch (error) { throw new Error(`Impulse generator start error: ${error}`); } } async displayOrder() { try { const sortStrategies = { 1: (a, b) => a.name.localeCompare(b.name), 2: (a, b) => b.name.localeCompare(a.name), 3: (a, b) => a.reference.localeCompare(b.reference), 4: (a, b) => b.reference.localeCompare(a.reference), }; const sortFn = sortStrategies[this.inputsDisplayOrder]; // Sort only if a valid function exists if (sortFn) { this.inputsServices.sort(sortFn); } // Debug if (this.logDebug) { const orderDump = this.inputsServices.map(svc => ({ name: svc.name, reference: svc.reference, identifier: svc.identifier, })); this.emit('debug', `Inputs display order:\n${JSON.stringify(orderDump, null, 2)}`); } // Always update DisplayOrder characteristic, even for "none" const displayOrder = this.inputsServices.map(svc => svc.identifier); const encodedOrder = Encode(1, displayOrder).toString('base64'); this.televisionService.updateCharacteristic(Characteristic.DisplayOrder, encodedOrder); return; } catch (error) { throw new Error(`Display order error: ${error}`); } } async addRemoveOrUpdateInput(inputs, remove = false) { try { if (!this.inputsServices) return; let updated = false; for (const input of inputs) { if (this.inputsServices.length >= 85 && !remove) continue; // Filter const contentType = input.contentType; const filterGames = this.filterGames && contentType === 'Game'; const filterApps = this.filterApps && contentType === 'App'; const filterSystemApps = this.filterSystemApps && contentType === 'systemApp'; const filterDlc = this.filterDlc && contentType === 'Dlc'; if (filterGames || filterApps || filterSystemApps || filterDlc) continue; const inputReference = input.reference; const savedName = this.savedInputsNames[inputReference] ?? input.name; const sanitizedName = await this.functions.sanitizeString(savedName); const inputMode = input.mode ?? 0; const inputTitleId = input.titleId; const inputOneStoreProductId = input.oneStoreProductId; const inputVisibility = this.savedInputsTargetVisibility[inputReference] ?? 0; if (remove) { const svc = this.inputsServices.find(s => s.reference === inputReference); if (svc) { if (this.logDebug) this.emit('debug', `Removing input: ${input.name}, reference: ${inputReference}`); this.accessory.removeService(svc); this.inputsServices = this.inputsServices.filter(s => s.reference !== inputReference); updated = true; } continue; } let inputService = this.inputsServices.find(s => s.reference === inputReference); if (inputService) { const nameChanged = inputService.name !== sanitizedName; if (nameChanged) { inputService.name = sanitizedName; inputService .updateCharacteristic(Characteristic.Name, sanitizedName) .updateCharacteristic(Characteristic.ConfiguredName, sanitizedName); if (this.logDebug) this.emit('debug', `Updated Input: ${input.name}, reference: ${inputReference}`); updated = true; } } else { const identifier = this.inputsServices.length + 1; inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`); inputService.identifier = identifier; inputService.reference = inputReference; inputService.name = sanitizedName; inputService.mode = inputMode; inputService.titleId = inputTitleId; inputService.oneStoreProductId = inputOneStoreProductId; inputService.visibility = inputVisibility; inputService .setCharacteristic(Characteristic.Identifier, identifier) .setCharacteristic(Characteristic.Name, sanitizedName) .setCharacteristic(Characteristic.ConfiguredName, sanitizedName) .setCharacteristic(Characteristic.IsConfigured, 1) .setCharacteristic(Characteristic.InputSourceType, inputMode) .setCharacteristic(Characteristic.CurrentVisibilityState, inputVisibility) .setCharacteristic(Characteristic.TargetVisibilityState, inputVisibility); // ConfiguredName persistence inputService.getCharacteristic(Characteristic.ConfiguredName) .onSet(async (value) => { try { value = await this.functions.sanitizeString(value); inputService.name = value; this.savedInputsNames[inputReference] = value; await this.functions.saveData(this.inputsNamesFile, this.savedInputsNames); if (this.logDebug) this.emit('debug', `Saved Input: ${input.name}, reference: ${inputReference}`); await this.displayOrder(); } catch (error) { if (this.logWarn) this.emit('warn', `Save Input Name error: ${error}`); } }); // TargetVisibility persistence inputService.getCharacteristic(Characteristic.TargetVisibilityState) .onSet(async (state) => { try { inputService.visibility = state; this.savedInputsTargetVisibility[inputReference] = state; await this.functions.saveData(this.inputsTargetVisibilityFile, this.savedInputsTargetVisibility); if (this.logDebug) this.emit('debug', `Saved Input: ${input.name}, reference: ${inputReference}, target visibility: ${state ? 'HIDDEN' : 'SHOWN'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Save Target Visibility error: ${error}`); } }); this.inputsServices.push(inputService); this.televisionService.addLinkedService(inputService); if (this.logDebug) this.emit('debug', `Added Input: ${input.name}, reference: ${inputReference}`); updated = true; } } // Only one time run if (updated) await this.displayOrder(); return true; } catch (error) { throw new Error(`Add/Remove/Update input error: ${error}`); } } async setInput(input) { try { const { oneStoreProductId, name, reference } = input; let channelName; let command; let payload; switch (oneStoreProductId) { case 'Dashboard': case 'Settings': case 'SettingsTv': case 'Accessory': case 'Screensaver': case 'NetworkTroubleshooter': case 'MicrosoftStore': channelName = 'Shell'; command = 'GoHome'; break; case 'Television': channelName = 'TV'; command = 'ShowGuide'; break; case 'XboxGuide': channelName = 'Shell'; command = 'ShowGuideTab'; payload = [{ 'tabName': 'Guide' }]; break; default: channelName = 'Shell'; command = 'ActivateApplicationWithOneStoreProductId'; payload = [{ 'oneStoreProductId': oneStoreProductId }]; break; } await this.xboxWebApi.send(channelName, command, payload); if (this.logInfo) this.emit('info', `Set game/app: ${name}, reference: ${reference}, product id: ${oneStoreProductId}`); return; } catch (error) { if (this.logWarn) this.emit('warn', `Set game/app error: ${error}`); } } //Prepare accessory async prepareAccessory() { try { //Accessory if (this.logDebug) this.emit('debug', `Prepare accessory`); const accessoryName = this.name; const accessoryUUID = AccessoryUUID.generate(this.liveId); const accessoryCategory = [Categories.OTHER, Categories.TELEVISION, Categories.TV_SET_TOP_BOX, Categories.TV_STREAMING_STICK, Categories.AUDIO_RECEIVER][this.displayType]; const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory); this.accessory = accessory; //Prepare information service this.informationService = accessory.getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.Manufacturer, this.savedInfo.manufacturer) .setCharacteristic(Characteristic.Model, this.savedInfo.modelName) .setCharacteristic(Characteristic.SerialNumber, this.savedInfo.serialNumber ?? this.liveId) .setCharacteristic(Characteristic.FirmwareRevision, this.savedInfo.firmwareRevision) .setCharacteristic(Characteristic.ConfiguredName, accessoryName); //Prepare television service if (this.logDebug) this.emit('debug', `Prepare television service`); this.televisionService = accessory.addService(Service.Television, `${accessoryName} Television`, 'Television'); this.televisionService.setCharacteristic(Characteristic.ConfiguredName, accessoryName); this.televisionService.setCharacteristic(Characteristic.SleepDiscoveryMode, 1); this.televisionService.getCharacteristic(Characteristic.Active) .onGet(async () => { const state = this.power; return state; }) .onSet(async (state) => { if (!!state === this.power) return; if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set power not possible, web api not enabled`); return; } try { await this.xboxWebApi.send('Power', state ? 'WakeUp' : 'TurnOff'); if (this.logInfo) this.emit('info', `Set Power: ${state ? 'ON' : 'OFF'}`); await new Promise(resolve => setTimeout(resolve, 2000)); } catch (error) { if (this.logWarn) this.emit('warn', `Set Power, error: ${error}`); } }); this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier) .onGet(async () => { const inputIdentifier = this.inputIdentifier; return inputIdentifier; }) .onSet(async (activeIdentifier) => { if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set game/app not possible, web api not enabled`); return; } try { const input = this.inputsServices.find(i => i.identifier === activeIdentifier); if (!input) { if (this.logWarn) this.emit('warn', `Game/App with identifier ${activeIdentifier} not found`); return; } if (!this.power) { if (this.logDebug) this.emit('debug', `Device is off, deferring game/app switch to '${activeIdentifier}'`); (async () => { for (let attempt = 0; attempt < 20; attempt++) { await new Promise(resolve => setTimeout(resolve, 1500)); if (this.power) { // if input didn't switch → retry if (this.inputIdentifier !== activeIdentifier) { if (this.logDebug) this.emit('debug', `Retrying game/app switch (${attempt + 1}/20)`); await this.setInput(input); } else { // success this.televisionService.updateCharacteristic(Characteristic.ActiveIdentifier, activeIdentifier); if (this.logInfo) this.emit('info', `Game/App set successfully: ${input.name}`); return; } } } if (this.logWarn) this.emit('warn', `Failed to set game/app after retries: ${input.name}`); })().catch(err => { if (this.logWarn) this.emit('warn', `Set game/app retry error: ${err}`); }); return; } // device is on await this.setInput(input); if (this.logInfo) this.emit('info', `Set game/app name: ${input.name}, Reference: ${input.reference}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set game/app error: ${JSON.stringify(error, null, 2)}`); } }); this.televisionService.getCharacteristic(Characteristic.RemoteKey) .onSet(async (remoteKey) => { if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set remote key not possible, web api not enabled`); return; } try { let channelName; let command; switch (remoteKey) { case 0: //REWIND channelName = 'Shell'; command = 'rewind'; break; case 1: //FAST_FORWARD channelName = 'Shell'; command = 'fastForward'; break; case 2: //NEXT_TRACK channelName = 'Shell'; command = 'nextTrack'; break; case 3: //PREVIOUS_TRACK channelName = 'Shell'; command = 'previousTrack'; break; case 4: //ARROW_UP channelName = 'Shell'; command = 'up'; break; case 5: //ARROW_DOWN channelName = 'Shell'; command = 'down'; break; case 6: //ARROW_LEFT channelName = 'Shell'; command = 'left'; break; case 7: //ARROW_RIGHT channelName = 'Shell'; command = 'right'; break; case 8: //SELECT channelName = 'Shell'; command = 'a'; break; case 9: //BACK channelName = 'Shell'; command = 'b'; break; case 10: //EXIT channelName = 'Shell'; command = 'nexus'; break; case 11: //PLAY_PAUSE channelName = 'Shell'; command = 'playPause'; break; case 15: //INFORMATION channelName = 'Shell'; command = this.infoButtonCommand; break; } await this.xboxWebApi.send(channelName, 'InjectKey', [{ 'keyType': command }]); if (this.logInfo) this.emit('info', `Remote Key: ${command}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Remote Key error: ${JSON.stringify(error, null, 2)}`); } }); this.televisionService.getCharacteristic(Characteristic.CurrentMediaState) .onGet(async () => { //apple: 0 - PLAY, 1 - PAUSE, 2 - STOP, 3 - LOADING, 4 - INTERRUPTED //xbox: 0 - STOP, 1 - PLAY, 2 - PAUSE const value = this.mediaState; return value; }); this.televisionService.getCharacteristic(Characteristic.TargetMediaState) .onGet(async () => { //0 - PLAY, 1 - PAUSE, 2 - STOP const value = this.mediaState; return value; }) .onSet(async (value) => { // BŁ2 FIX: removed dead variables (newMediaState, setMediaState which was always false) try { if (this.logInfo) this.emit('info', `Set Target Media: ${['PLAY', 'PAUSE', 'STOP', 'LOADING', 'INTERRUPTED'][value]}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Target Media error: ${error}`); } }); this.televisionService.getCharacteristic(Characteristic.PowerModeSelection) .onSet(async (powerModeSelection) => { if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set power mode selection not possible, web api not enabled`); return; } try { switch (powerModeSelection) { case 0: //SHOW await this.xboxWebApi.send('Shell', 'InjectKey', [{ 'keyType': 'nexus' }]); break; case 1: //HIDE await this.xboxWebApi.send('Shell', 'InjectKey', [{ 'keyType': 'b' }]); break; }; if (this.logInfo) this.emit('info', `Set Power Mode Selection: ${powerModeSelection === 0 ? 'SHOW' : 'HIDE'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Power Mode Selection error: ${error}`); } }); //prepare inputs service if (this.logDebug) this.emit('debug', `Prepare inputs service`); this.inputsServices = []; await this.addRemoveOrUpdateInput(this.savedInputs, false); //Prepare volume service if (this.volumeControl > 0) { const volumeServiceName = this.volumeControlNamePrefix ? `${accessoryName} ${this.volumeControlName}` : this.volumeControlName; switch (this.volumeControl) { case 1: //lightbulb if (this.logDebug) this.emit('debug', `Prepare volume service lightbulb`); this.volumeServiceLightbulb = accessory.addService(Service.Lightbulb, volumeServiceName, 'Lightbulb Speaker'); this.volumeServiceLightbulb.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceLightbulb.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceLightbulb.getCharacteristic(Characteristic.Brightness) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { if (this.logInfo) this.emit('info', `Set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume error: ${error}`); } }); this.volumeServiceLightbulb.getCharacteristic(Characteristic.On) .onGet(async () => { const state = this.power ? !this.mute : false; return state; }) .onSet(async (state) => { try { if (this.logInfo) this.emit('info', `Set Mute: ${!state ? 'ON' : 'OFF'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Mute error: ${error}`); } }); break; case 2: //fan if (this.logDebug) this.emit('debug', `Prepare volume service fan`); this.volumeServiceFan = accessory.addService(Service.Fan, volumeServiceName, 'Fan Speaker'); this.volumeServiceFan.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceFan.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceFan.getCharacteristic(Characteristic.RotationSpeed) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { if (this.logInfo) this.emit('info', `Set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume error: ${error}`); } }); this.volumeServiceFan.getCharacteristic(Characteristic.On) .onGet(async () => { const state = this.power ? !this.mute : false; return state; }) .onSet(async (state) => { try { if (this.logInfo) this.emit('info', `Set Mute: ${!state ? 'ON' : 'OFF'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Mute error: ${error}`); } }); break; case 3: // tv speaker if (this.logDebug) this.emit('debug', `Prepare television speaker service`); this.volumeServiceTvSpeaker = accessory.addService(Service.TelevisionSpeaker, volumeServiceName, 'TV Speaker'); this.volumeServiceTvSpeaker.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Active) .onGet(async () => { const state = this.power; return state; }) .onSet(async (state) => { }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeControlType) .onGet(async () => { const state = 3; return state; }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeSelector) .onSet(async (volumeSelector) => { if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set volume selector not possible, web api not enabled`); return; } try { switch (volumeSelector) { case 0: //Up await this.xboxWebApi.send('Volume', 'Up'); break; case 1: //Down await this.xboxWebApi.send('Volume', 'Down'); break; } if (this.logInfo) this.emit('info', `Set Volume Selector: ${volumeSelector ? 'Down' : 'UP'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume Selector error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Volume) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { if (this.logInfo) this.emit('info', `Set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Mute) .onGet(async () => { const state = this.mute; return state; }) .onSet(async (state) => { try { if (this.logInfo) this.emit('info', `Set Mute: ${state ? 'ON' : 'OFF'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Mute error: ${error}`); } }); break; case 4: // tv speaker + lightbulb if (this.logDebug) this.emit('debug', `Prepare television speaker service`); this.volumeServiceTvSpeaker = accessory.addService(Service.TelevisionSpeaker, volumeServiceName, 'TV Speaker'); this.volumeServiceTvSpeaker.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Active) .onGet(async () => { const state = this.power; return state; }) .onSet(async (state) => { }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeControlType) .onGet(async () => { const state = 3; return state; }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeSelector) .onSet(async (volumeSelector) => { if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set volume selector not possible, web api not enabled`); return; } try { switch (volumeSelector) { case 0: //Up await this.xboxWebApi.send('Volume', 'Up'); break; case 1: //Down await this.xboxWebApi.send('Volume', 'Down'); break; } if (this.logInfo) this.emit('info', `Set Volume Selector: ${volumeSelector ? 'Down' : 'UP'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume Selector error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Volume) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { if (this.logInfo) this.emit('info', `Set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Mute) .onGet(async () => { const state = this.mute; return state; }) .onSet(async (state) => { try { if (this.logInfo) this.emit('info', `Set Mute: ${state ? 'ON' : 'OFF'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Mute error: ${error}`); } }); // lightbulb if (this.logDebug) this.emit('debug', `Prepare volume service lightbulb`); this.volumeServiceLightbulb = accessory.addService(Service.Lightbulb, volumeServiceName, 'Lightbulb Speaker'); this.volumeServiceLightbulb.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceLightbulb.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceLightbulb.getCharacteristic(Characteristic.Brightness) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.Volume, value); }); this.volumeServiceLightbulb.getCharacteristic(Characteristic.On) .onGet(async () => { const state = this.power ? !this.mute : false; return state; }) .onSet(async (state) => { this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.Mute, !state); }); break; case 5: // tv speaker + fan if (this.logDebug) this.emit('debug', `Prepare television speaker service`); this.volumeServiceTvSpeaker = accessory.addService(Service.TelevisionSpeaker, volumeServiceName, 'TV Speaker'); this.volumeServiceTvSpeaker.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Active) .onGet(async () => { const state = this.power; return state; }) .onSet(async (state) => { }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeControlType) .onGet(async () => { const state = 3; return state; }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeSelector) .onSet(async (volumeSelector) => { if (!this.consoleAuthorized && this.logWarn) { this.emit('warn', `Set volume selector not possible, web api not enabled`); return; } try { switch (volumeSelector) { case 0: //Up await this.xboxWebApi.send('Volume', 'Up'); break; case 1: //Down await this.xboxWebApi.send('Volume', 'Down'); break; } if (this.logInfo) this.emit('info', `Set Volume Selector: ${volumeSelector ? 'Down' : 'UP'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume Selector error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Volume) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { if (this.logInfo) this.emit('info', `Set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Volume error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Mute) .onGet(async () => { const state = this.mute; return state; }) .onSet(async (state) => { try { if (this.logInfo) this.emit('info', `Set Mute: ${!state ? 'ON' : 'OFF'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Set Mute error: ${error}`); } }); // fan if (this.logDebug) this.emit('debug', `Prepare volume service fan`); this.volumeServiceFan = accessory.addService(Service.Fan, volumeServiceName, 'Fan Speaker'); this.volumeServiceFan.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceFan.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceFan.getCharacteristic(Characteristic.RotationSpeed) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.Volume, value); }); this.volumeServiceFan.getCharacteristic(Characteristic.On) .onGet(async () => {