homebridge-xbox-tv
Version:
Homebridge plugin to control Xbox game consoles.
985 lines (893 loc) • 59.7 kB
JavaScript
import { promises as fsPromises } from 'fs';
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 { DefaultInputs, LocalApi } 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.name = device.name;
this.host = device.host;
this.xboxLiveId = device.xboxLiveId;
this.webApiControl = device.webApiControl || false;
this.webApiPowerOnOff = this.webApiControl ? device.webApiPowerOnOff : false;
this.getInputsFromDevice = this.webApiControl ? device.getInputsFromDevice : false;
this.filterGames = device.filterGames || false;
this.filterApps = device.filterApps || false;
this.filterSystemApps = device.filterSystemApps || false;
this.filterDlc = device.filterDlc || false;
this.inputsDisplayOrder = device.inputsDisplayOrder || 0;
this.inputs = device.inputs || [];
this.buttons = device.buttons || [];
this.sensorPower = device.sensorPower || false;
this.sensorInput = device.sensorInput || false;
this.sensorScreenSaver = device.sensorScreenSaver || false;
this.sensorInputs = device.sensorInputs || [];
this.webApiClientId = device.webApiClientId;
this.webApiClientSecret = device.webApiClientSecret;
this.infoButtonCommand = device.infoButtonCommand || 'nexus';
this.volumeControl = device.volumeControl || false;
this.volumeControlNamePrefix = device.volumeControlNamePrefix || false;
this.volumeControlName = device.volumeControlName || 'Volume';
this.enableDebugMode = device.enableDebugMode || false;
this.disableLogInfo = device.disableLogInfo || 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;
//add configured inputs to the default inputs and chack duplicated inputs
const tempInputs = [...DefaultInputs, ...this.inputs];
const inputsArr = [];
for (const input of tempInputs) {
const inputName = input.name ?? false;
const inputReference = input.reference ?? false;
const duplicatedInput = inputsArr.some(input => input.reference === inputReference) ?? false;
const push = inputName && inputReference && !duplicatedInput ? inputsArr.push(input) : false;
}
this.inputs = inputsArr;
//sensors
this.sensorsInputsConfigured = [];
for (const sensor of this.sensorInputs) {
const sensorInputName = sensor.name ?? false;
const sensorInputReference = sensor.reference ?? false;
const sensorInputDisplayType = sensor.displayType ?? 0;
if (sensorInputName && sensorInputReference && sensorInputDisplayType > 0) {
sensor.serviceType = ['', Service.MotionSensor, Service.OccupancySensor, Service.ContactSensor][sensorInputDisplayType];
sensor.characteristicType = ['', Characteristic.MotionDetected, Characteristic.OccupancyDetected, Characteristic.ContactSensorState][sensorInputDisplayType];
sensor.state = false;
this.sensorsInputsConfigured.push(sensor);
} else {
const log = sensorInputDisplayType === 0 ? false : this.emit('info', `Sensor Name: ${sensorInputName ? sensorInputName : 'Missing'}, Reference: ${sensorInputReference ? sensorInputReference : 'Missing'}`);
};
}
this.sensorsInputsConfiguredCount = this.sensorsInputsConfigured.length || 0;
//buttons
this.buttonsConfigured = [];
for (const button of this.buttons) {
const buttonName = button.name ?? false;
const buttonMode = button.mode ?? -1;
const buttonReferenceCommand = [button.mediaCommand, button.gamePadCommand, button.tvRemoteCommand, button.consoleControlCommand, button.gameAppControlCommand][buttonMode] ?? false;
const buttonDisplayType = button.displayType ?? 0;
if (buttonName && buttonMode >= 0 && buttonReferenceCommand && buttonDisplayType > 0) {
button.serviceType = ['', Service.Outlet, Service.Switch][buttonDisplayType];
button.state = false;
this.buttonsConfigured.push(button);
} else {
const log = buttonDisplayType === 0 ? false : this.emit('info', `Button Name: ${buttonName ? buttonName : 'Missing'}, ${buttonMode ? 'Command:' : 'Reference:'} ${buttonReferenceCommand ? buttonReferenceCommand : 'Missing'}, Mode: ${buttonMode ? buttonMode : 'Missing'}`);
};
}
this.buttonsConfiguredCount = this.buttonsConfigured.length || 0;
//variable
this.allServices = [];
this.sensorsInputsServices = [];
this.buttonsServices = [];
this.inputsConfigured = [];
this.inputIdentifier = 1;
this.startPrepareAccessory = true;
this.power = false;
this.volume = 0;
this.mute = true;
this.mediaState = 0;
this.reference = '';
this.sensorScreenSaverState = false;
this.sensorInputState = false;
this.consoleAuthorized = false;
}
async saveData(path, data) {
try {
data = JSON.stringify(data, null, 2);
await fsPromises.writeFile(path, data);
const debug = this.enableDebugMode ? this.emit('debug', `Saved data: ${data}`) : false;
return true;
} catch (error) {
throw new Error(`Save data error: ${error}`);
};
}
async readData(path) {
try {
const data = await fsPromises.readFile(path);
const debug = !this.enableDebugMode ? false : this.emit('debug', `Read data: ${data}`);
return data;
} catch (error) {
throw new Error(`Read data error: ${error}`);
};
}
async sanitizeString(str) {
// Replace dots, colons, and semicolons inside words with a space
str = str.replace(/(\w)[.:;]+(\w)/g, '$1 $2');
// Remove remaining dots, colons, semicolons, plus, and minus anywhere in the string
str = str.replace(/[.:;+\-]/g, '');
// Replace all other invalid characters (anything not A-Z, a-z, 0-9, space, or apostrophe) with a space
str = str.replace(/[^A-Za-z0-9 ']/g, ' ');
// Trim leading and trailing spaces
str = str.trim();
return str;
}
async setOverExternalIntegration(integration, key, value) {
try {
let set = false
switch (key) {
case 'Power':
switch (this.consoleAuthorized && this.webApiPowerOnOff) {
case true:
switch (value) {
case true: //off
set = await this.xboxWebApi.send('Power', 'WakeUp');
break;
case false: //on
set = await this.xboxWebApi.send('Power', 'TurnOff');
break;
}
break;
case false:
switch (value) {
case true: //off
set = await this.xboxLocalApi.powerOff();
break;
case false: //on
set = await this.xboxLocalApi.powerOn();
break;
}
}
break;
case 'App':
const payload = [{ 'oneStoreProductId': value }];
set = await this.xboxWebApi.send('Shell', 'ActivateApplicationWithOneStoreProductId', payload);
break;
case 'Volume':
switch (value) {
case 'up': //Up
set = await this.xboxWebApi.send('Volume', 'Up');
break;
case 'down': //Down
set = await this.xboxWebApi.send('Volume', 'Down');
break;
}
break;
case 'Mute':
switch (value) {
case true: //Mute
set = await this.xboxWebApi.send('Audio', 'Mute');
break;
case false: //Unmute;
set = await this.xboxWebApi.send('Audio', 'Unmute');
break;
}
break;
case 'RcControl':
set = await this.xboxWebApi.send('Shell', 'InjectKey', [{ 'keyType': value }]);
break;
default:
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() {
try {
//RESTFul server
const restFulEnabled = this.restFul.enable || false;
if (restFulEnabled) {
this.restFul1 = new RestFul({
port: this.restFul.port || 3000,
debug: this.restFul.debug || false
});
this.restFul1.on('connected', (message) => {
this.emit('success', message);
this.restFulConnected = true;
})
.on('set', async (key, value) => {
try {
await this.setOverExternalIntegration('RESTFul', key, value);
} catch (error) {
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);
});
}
//mqtt client
const mqttEnabled = this.mqtt.enable || false;
if (mqttEnabled) {
this.mqtt1 = new Mqtt({
host: this.mqtt.host,
port: this.mqtt.port || 1883,
clientId: this.mqtt.clientId || `lgwebos_${Math.random().toString(16).slice(3)}`,
prefix: `${this.mqtt.prefix}/${this.name}`,
user: this.mqtt.user,
passwd: this.mqtt.passwd,
debug: this.mqtt.debug || false
});
this.mqtt1.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) {
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);
});
};
return true;
} catch (error) {
this.emit('warn', `External integration start error: ${error}`);
};
}
async displayOrder() {
try {
switch (this.inputsDisplayOrder) {
case 0:
this.inputsConfigured.sort((a, b) => a.identifier - b.identifier);
break;
case 1:
this.inputsConfigured.sort((a, b) => a.name.localeCompare(b.name));
break;
case 2:
this.inputsConfigured.sort((a, b) => b.name.localeCompare(a.name));
break;
case 3:
this.inputsConfigured.sort((a, b) => a.reference.localeCompare(b.reference));
break;
case 4:
this.inputsConfigured.sort((a, b) => b.reference.localeCompare(a.reference));
break;
}
const debug = !this.enableDebugMode ? false : this.emit('debug', `Inputs display order: ${JSON.stringify(this.inputsConfigured, null, 2)}`);
const displayOrder = this.inputsConfigured.map(input => input.identifier);
this.televisionService.setCharacteristic(Characteristic.DisplayOrder, Encode(1, displayOrder).toString('base64'));
return true;
} catch (error) {
throw new Error(`Display order error: ${error}`);
};
}
async prepareDataForAccessory() {
try {
//read dev info from file
const savedInfo = await this.readData(this.devInfoFile);
this.savedInfo = savedInfo.toString().trim() !== '' ? JSON.parse(savedInfo) : {};
const debug = !this.enableDebugMode ? false : this.emit('debug', `Read saved Info: ${JSON.stringify(this.savedInfo, null, 2)}`);
//read inputs file
const savedInputs = await this.readData(this.inputsFile);
this.savedInputs = savedInputs.toString().trim() !== '' ? JSON.parse(savedInputs) : this.inputs;
const debug2 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs: ${JSON.stringify(this.savedInputs, null, 2)}`);
//read inputs names from file
const savedInputsNames = await this.readData(this.inputsNamesFile);
this.savedInputsNames = savedInputsNames.toString().trim() !== '' ? JSON.parse(savedInputsNames) : {};
const debug3 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs Names: ${JSON.stringify(this.savedInputsNames, null, 2)}`);
//read inputs visibility from file
const savedInputsTargetVisibility = await this.readData(this.inputsTargetVisibilityFile);
this.savedInputsTargetVisibility = savedInputsTargetVisibility.toString().trim() !== '' ? JSON.parse(savedInputsTargetVisibility) : {};
const debug4 = !this.enableDebugMode ? false : 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 startImpulseGenerator() {
try {
//start web api impulse generator
const startImpulseGenerator = this.consoleAuthorized ? await this.xboxWebApi.impulseGenerator.start([{ name: 'checkAuthorization', sampling: 900000 }]) : false;
//start local api impulse generator
await this.xboxLocalApi.impulseGenerator.start([{ name: 'heartBeat', sampling: 10000 }]);
return true;
} catch (error) {
throw new Error(`Impulse generator start error: ${error}`);
};
}
//Prepare accessory
async prepareAccessory() {
try {
//Accessory
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare accessory`);
const accessoryName = this.name;
const accessoryUUID = AccessoryUUID.generate(this.xboxLiveId);
const accessoryCategory = Categories.TV_SET_TOP_BOX;
const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory);
//Prepare information service
this.informationService = accessory.getService(Service.AccessoryInformation)
.setCharacteristic(Characteristic.Manufacturer, this.savedInfo.manufacturer ?? 'Microsoft')
.setCharacteristic(Characteristic.Model, this.savedInfo.modelName ?? 'Xbox')
.setCharacteristic(Characteristic.SerialNumber, this.savedInfo.serialNumber ?? this.xboxLiveId)
.setCharacteristic(Characteristic.FirmwareRevision, this.savedInfo.firmwareRevision ?? 'Firmware Revision')
.setCharacteristic(Characteristic.ConfiguredName, accessoryName);
this.allServices.push(this.informationService);
//Prepare television service
const debug1 = !this.enableDebugMode ? false : 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 (this.power == state) {
return;
}
try {
switch (this.consoleAuthorized && this.webApiPowerOnOff) {
case true:
switch (this.power) {
case true: //off
await this.xboxWebApi.send('Power', 'TurnOff');
break;
case false: //on
await this.xboxWebApi.send('Power', 'WakeUp');
break;
}
break;
case false:
switch (this.power) {
case true: //off
await this.xboxLocalApi.powerOff();
break;
case false: //on
await this.xboxLocalApi.powerOn();
break;
}
}
const logInfo = this.disableLogInfo ? false : this.emit('info', `set Power: ${state ? 'ON' : 'OFF'}`);
await new Promise(resolve => setTimeout(resolve, 3000));
} catch (error) {
this.emit('warn', `set Power, error: ${error}`);
};
});
this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier)
.onGet(async () => {
const inputIdentifier = this.inputIdentifier;
return inputIdentifier;
})
.onSet(async (activeIdentifier) => {
try {
const input = this.inputsConfigured.find(input => input.identifier === activeIdentifier);
const inputOneStoreProductId = input.oneStoreProductId;
const inputReference = input.reference;
const inputName = input.name;
let channelName;
let command;
let payload;
switch (this.power) {
case false:
await new Promise(resolve => setTimeout(resolve, 4000));
const tryAgain = this.power ? this.televisionService.setCharacteristic(Characteristic.ActiveIdentifier, activeIdentifier) : false;
break;
case true:
switch (inputOneStoreProductId) {
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': inputOneStoreProductId }];
break;
}
await this.xboxWebApi.send(channelName, command, payload);
const logInfo = this.disableLogInfo ? false : this.emit('info', `set Input: ${inputName}, Reference: ${inputReference}, Product Id: ${inputOneStoreProductId}`);
break;
}
} catch (error) {
this.emit('warn', `set Input error: ${JSON.stringify(error, null, 2)}`);
};
});
this.televisionService.getCharacteristic(Characteristic.RemoteKey)
.onSet(async (remoteKey) => {
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 }]);
const logInfo = this.disableLogInfo ? false : this.emit('info', `Remote Key: ${command}`);
} catch (error) {
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 = [2, 0, 1, 3, 4][this.mediaState];
return value;
});
this.televisionService.getCharacteristic(Characteristic.TargetMediaState)
.onGet(async () => {
//0 - PLAY, 1 - PAUSE, 2 - STOP
const value = [2, 0, 1, 3, 4][this.mediaState];
return value;
})
.onSet(async (value) => {
try {
const newMediaState = value;
const setMediaState = this.power ? false : false;
const logInfo = this.disableLogInfo ? false : this.emit('info', `set Target Media: ${['PLAY', 'PAUSE', 'STOP', 'LOADING', 'INTERRUPTED'][value]}`);
} catch (error) {
this.emit('warn', `set Target Media error: ${error}`);
};
});
this.televisionService.getCharacteristic(Characteristic.PowerModeSelection)
.onSet(async (powerModeSelection) => {
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;
};
const logInfo = this.disableLogInfo ? false : this.emit('info', `set Power Mode Selection: ${powerModeSelection === 0 ? 'SHOW' : 'HIDE'}`);
} catch (error) {
this.emit('warn', `set Power Mode Selection error: ${error}`);
};
});
this.allServices.push(this.televisionService);
//Prepare speaker service
const debug2 = !this.enableDebugMode ? false : this.emit('debug', `Prepare speaker service`);
this.speakerService = accessory.addService(Service.TelevisionSpeaker, `${accessoryName} Speaker`, 'Speaker');
this.speakerService.getCharacteristic(Characteristic.Active)
.onGet(async () => {
const state = this.power;
return state;
})
.onSet(async (state) => {
});
this.speakerService.getCharacteristic(Characteristic.VolumeControlType)
.onGet(async () => {
const state = 3; //none, relative, relative with current, absolute
return state;
});
this.speakerService.getCharacteristic(Characteristic.VolumeSelector)
.onSet(async (volumeSelector) => {
try {
switch (volumeSelector) {
case 0: //Up
await this.xboxWebApi.send('Volume', 'Up');
break;
case 1: //Down
await this.xboxWebApi.send('Volume', 'Down');
break;
}
const logInfo = this.disableLogInfo ? false : this.emit('info', `set Volume Selector: ${volumeSelector ? 'Down' : 'UP'}`);
} catch (error) {
this.emit('warn', `set Volume Selector error: ${error}`);
};
})
this.speakerService.getCharacteristic(Characteristic.Volume)
.onGet(async () => {
const volume = this.volume;
return volume;
})
.onSet(async (volume) => {
const logInfo = this.disableLogInfo ? false : this.emit('info', `set Volume: ${volume}`);
});
this.speakerService.getCharacteristic(Characteristic.Mute)
.onGet(async () => {
const state = this.mute;
return state;
})
.onSet(async (state) => {
try {
switch (state) {
case 0: //Mute
await this.xboxWebApi.send('Audio', 'Mute');
break;
case 1: //Unmute
await this.xboxWebApi.send('Audio', 'Unmute');
break;
}
const logInfo = this.disableLogInfo ? false : this.emit('info', `set Mute: ${state ? 'ON' : 'OFF'}`);
} catch (error) {
this.emit('warn', `set Mute error: ${error}`);
};
});
this.allServices.push(this.speakerService);
//prepare inputs service
const debug3 = !this.enableDebugMode ? false : this.emit('debug', `Prepare inputs service`);
//filter unnecessary inputs
const filteredInputsArr = [];
for (const input of this.savedInputs) {
const contentType = input.contentType;
const filterGames = this.filterGames ? (contentType === 'Game') : false;
const filterApps = this.filterApps ? (contentType === 'App') : false;
const filterSystemApps = this.filterSystemApps ? (contentType === 'systemApp') : false;
const filterDlc = this.filterDlc ? (contentType === 'Dlc') : false;
const push = this.getInputsFromDevice ? ((!filterGames && !filterApps && !filterSystemApps && !filterDlc) ? filteredInputsArr.push(input) : false) : filteredInputsArr.push(input);
}
//check possible inputs count (max 85)
const inputs = filteredInputsArr;
const inputsCount = inputs.length;
const possibleInputsCount = 85 - this.allServices.length;
const maxInputsCount = inputsCount >= possibleInputsCount ? possibleInputsCount : inputsCount;
for (let i = 0; i < maxInputsCount; i++) {
//input
const input = inputs[i];
//get identifier
const inputIdentifier = i + 1;
//get input reference
const inputReference = input.reference || input.titleId;
input.reference = inputReference;
//get input name
const name = input.name ?? `Input ${inputIdentifier}`;
//saved string
const savedInputsNames = this.savedInputsNames[inputReference] ?? false;
input.name = savedInputsNames ? savedInputsNames.substring(0, 64) : name.substring(0, 64);
//get input type
const inputSourceType = 0;
//get input configured
const isConfigured = 1;
//get visibility
input.visibility = this.savedInputsTargetVisibility[inputReference] ?? 0;
//add identifier to the input
input.identifier = inputIdentifier;
//input service
const sanitizedName = await this.sanitizeString(input.name);
const inputService = accessory.addService(Service.InputSource, sanitizedName, `Input ${inputIdentifier}`);
inputService
.setCharacteristic(Characteristic.Identifier, inputIdentifier)
.setCharacteristic(Characteristic.Name, sanitizedName)
.setCharacteristic(Characteristic.InputSourceType, inputSourceType)
.setCharacteristic(Characteristic.IsConfigured, isConfigured)
.setCharacteristic(Characteristic.CurrentVisibilityState, input.visibility)
inputService.getCharacteristic(Characteristic.ConfiguredName)
.onGet(async () => {
return sanitizedName;
})
.onSet(async (value) => {
try {
input.name = value;
this.savedInputsNames[inputReference] = value;
await this.saveData(this.inputsNamesFile, this.savedInputsNames);
const debug = !this.enableDebugMode ? false : this.emit('debug', `Saved Input Name: ${value}, Reference: ${inputReference}`);
//sort inputs
const index = this.inputsConfigured.findIndex(input => input.reference === inputReference);
this.inputsConfigured[index].name = value;
await this.displayOrder();
} catch (error) {
this.emit('warn', `save Input Name error: ${error}`);
}
});
inputService.getCharacteristic(Characteristic.TargetVisibilityState)
.onGet(async () => {
return input.visibility;
})
.onSet(async (state) => {
try {
input.visibility = state,
this.savedInputsTargetVisibility[inputReference] = state;
await this.saveData(this.inputsTargetVisibilityFile, this.savedInputsTargetVisibility);
const debug = !this.enableDebugMode ? false : this.emit('debug', `Saved Input: ${input.name} Target Visibility: ${state ? 'HIDEN' : 'SHOWN'}`);
} catch (error) {
this.emit('warn', `save Target Visibility error: ${error}`);
}
});
this.inputsConfigured.push(input);
this.televisionService.addLinkedService(inputService);
this.allServices.push(inputService);
}
//Prepare volume service
if (this.volumeControl) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare volume service`);
const volumeServiceName = this.volumeControlNamePrefix ? `${accessoryName} ${this.volumeControlName}` : this.volumeControlName;
if (this.volumeControl === 1) {
this.volumeService = accessory.addService(Service.Lightbulb, `${volumeServiceName}`, volumeServiceName);
this.volumeService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.volumeService.setCharacteristic(Characteristic.ConfiguredName, `${volumeServiceName}`);
this.volumeService.getCharacteristic(Characteristic.Brightness)
.onGet(async () => {
const volume = this.volume;
return volume;
})
.onSet(async (volume) => {
this.speakerService.setCharacteristic(Characteristic.Volume, volume);
});
this.volumeService.getCharacteristic(Characteristic.On)
.onGet(async () => {
const state = !this.mute;
return state;
})
.onSet(async (state) => {
this.speakerService.setCharacteristic(Characteristic.Mute, !state);
});
this.allServices.push(this.volumeService);
}
if (this.volumeControl === 2) {
this.volumeServiceFan = accessory.addService(Service.Fan, `${volumeServiceName}`, volumeServiceName);
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 (volume) => {
this.speakerService.setCharacteristic(Characteristic.Volume, volume);
});
this.volumeServiceFan.getCharacteristic(Characteristic.On)
.onGet(async () => {
const state = !this.mute;
return state;
})
.onSet(async (state) => {
this.speakerService.setCharacteristic(Characteristic.Mute, !state);
});
this.allServices.push(this.volumeServiceFan);
}
}
//prepare sensor service
if (this.sensorPower) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare power sensor service`);
this.sensorPowerService = accessory.addService(Service.ContactSensor, `${accessoryName} Power Sensor`, `Power Sensor`);
this.sensorPowerService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.sensorPowerService.setCharacteristic(Characteristic.ConfiguredName, `${accessoryName} Power Sensor`);
this.sensorPowerService.getCharacteristic(Characteristic.ContactSensorState)
.onGet(async () => {
const state = this.power;
return state;
});
this.allServices.push(this.sensorPowerService);
};
if (this.sensorInput) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare input sensor service`);
this.sensorInputService = accessory.addService(Service.ContactSensor, `${accessoryName} Input Sensor`, `Input Sensor`);
this.sensorInputService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.sensorInputService.setCharacteristic(Characteristic.ConfiguredName, `${accessoryName} Input Sensor`);
this.sensorInputService.getCharacteristic(Characteristic.ContactSensorState)
.onGet(async () => {
const state = this.power ? this.sensorInputState : false;
return state;
});
this.allServices.push(this.sensorInputService);
};
if (this.sensorScreenSaver) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare screen saver sensor service`);
this.sensorScreenSaverService = accessory.addService(Service.ContactSensor, `${accessoryName} Screen Saver Sensor`, `Screen Saver Sensor`);
this.sensorScreenSaverService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.sensorScreenSaverService.setCharacteristic(Characteristic.ConfiguredName, `${accessoryName} Screen Saver Sensor`);
this.sensorScreenSaverService.getCharacteristic(Characteristic.ContactSensorState)
.onGet(async () => {
const state = this.power ? this.sensorScreenSaverState : false;
return state;
});
this.allServices.push(this.sensorScreenSaverService);
};
//prepare sonsor service
const possibleSensorInputsCount = 99 - this.allServices.length;
const maxSensorInputsCount = this.sensorsInputsConfiguredCount >= possibleSensorInputsCount ? possibleSensorInputsCount : this.sensorsInputsConfiguredCount;
if (maxSensorInputsCount > 0) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare inputs sensors services`);
for (let i = 0; i < maxSensorInputsCount; i++) {
//get sensor
const sensorInput = this.sensorsInputsConfigured[i];
//get sensor name
const sensorInputName = sensorInput.name;
//get sensor name prefix
const namePrefix = sensorInput.namePrefix || false;
//get service type
const serviceType = sensorInput.serviceType;
//get service type
const characteristicType = sensorInput.characteristicType;
const serviceName = namePrefix ? `${accessoryName} ${sensorInputName}` : sensorInputName;
const sensorInputService = new serviceType(serviceName, `Sensor ${i}`);
sensorInputService.addOptionalCharacteristic(Characteristic.ConfiguredName);
sensorInputService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
sensorInputService.getCharacteristic(characteristicType)
.onGet(async () => {
const state = sensorInput.state;
return state;
});
this.sensorsInputsServices.push(sensorInputService);
this.allServices.push(sensorInputService);
accessory.addService(sensorInputService);
}
}
//Prepare buttons services
const possibleButtonsCount = 99 - this.allServices.length;
const maxButtonsCount = this.buttonsConfiguredCount >= possibleButtonsCount ? possibleButtonsCount : this.buttonsConfiguredCount;
if (maxButtonsCount > 0) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare buttons services`);
for (let i = 0; i < maxButtonsCount; i++) {
//get button
const button = this.buttonsConfigured[i];
//get button name
const buttonName = button.name;
//get button command
const buttonMode = button.mode;
//get button command
const buttonCommand = [button.mediaCommand, button.gamePadCommand, button.tvRemoteCommand, button.consoleControlCommand, button.gameAppControlCommand][buttonMode];
//get button name prefix
const namePrefix = button.namePrefix ?? false;
//get service type
const serviceType = button.serviceType;
const serviceName = namePrefix ? `${accessoryName} ${buttonName}` : buttonName;
const buttonService = new serviceType(serviceName, `Button ${i}`);
buttonService.addOptionalCharacteristic(Characteristic.ConfiguredName);
buttonService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
buttonService.getCharacteristic(Characteristic.On)
.onGet(async () => {
const state = button.state;
return state;
})
.onSet(async (state) => {
if (!this.power) {
this.emit('warn', `console is off`);
return;
}
try {
switch (buttonMode) {
case 0: case 1: case 2:
const send = state ? await this.xboxWebApi.send('Shell', 'InjectKey', [{ 'keyType': buttonCommand }]) : false;
break;
case 3:
switch (buttonCommand) {
case 'reboot':
const send = state ? await this.xboxWebApi.send('Power', 'Reboot') : false;
break;
case 'recordGameDvr':
const send1 = state ? await this.xboxLocalApi.recordGameDvr() : false;
break;
}
break;
case 4:
switch (buttonCommand) {
case 'Dashboard': case 'Settings': case 'SettingsTv': case 'Accessory': case 'Screensaver': case 'NetworkTroubleshooter': case 'MicrosoftStore':
const send3 = state ? await this.xboxWebApi.send('Shell', 'GoHome') : false;
break;
case 'Television':
const send4 = state ? await this.xboxWebApi.send('TV', 'ShowGuide') : false;
break;
case 'XboxGuide':
const send5 = state ? await this.xboxWebApi.send('Shell', 'ShowGuideTab', [{ 'tabName': 'Guide' }]) : false;
break;
default:
const send6 = state ? await this.xboxWebApi.send('Shell', 'ActivateApplicationWithOneStoreProductId', [{ 'oneStoreProductId': buttonCommand }]) : false;
break;
}
break;
}
} catch (error) {
this.emit('warn', `set Button error: ${error}`);
};
});
this.buttonsServices.push(buttonService);
this.allServices.push(buttonService);
accessory.addService(buttonService);
}
}
//sort inputs list
await this.displayOrder();
return accessory;
} catch (error) {
throw new Error(error)
};
}
//start
async start() {
try {
//web api client
try {
this.xboxWebApi = new XboxWebApi({
xboxLiveId: this.xboxLiveId,