UNPKG

homebridge-xbox-tv

Version:

Homebridge plugin to control Xbox game consoles.

161 lines (140 loc) 5.91 kB
import { join } from 'path'; import { mkdirSync, existsSync, writeFileSync } from 'fs'; import XboxDevice from './src/xboxdevice.js'; import ImpulseGenerator from './src/impulsegenerator.js'; import { PluginName, PlatformName } from './src/constants.js'; class XboxPlatform { constructor(log, config, api) { // only load if configured if (!config || !Array.isArray(config.devices)) { log.warn(`No configuration found for ${PluginName}`); return; } this.accessories = []; const prefDir = join(api.user.storagePath(), 'xboxTv'); try { mkdirSync(prefDir, { recursive: true }); } catch (error) { log.error(`Prepare directory error: ${error.message ?? error}`); return; } api.on('didFinishLaunching', () => { // Each device is set up independently — a failure in one does not // block the others. Promise.allSettled runs all in parallel. Promise.allSettled( config.devices.map(device => this.setupDevice(device, prefDir, log, api) ) ).then(results => { results.forEach((result, i) => { if (result.status === 'rejected') { log.error(`Device[${i}] setup error: ${result.reason?.message ?? result.reason}`); } }); }); }); } // ── Per-device setup ────────────────────────────────────────────────────── async setupDevice(device, prefDir, log, api) { const { name, host, xboxLiveId, displayType } = device; if (!name || !host || !xboxLiveId || !displayType) { log.warn(`Device: ${host || 'host missing'}, ${name || 'name missing'}, ${xboxLiveId || 'xbox live id missing'}${!displayType ? ', display type disabled' : ''} in config, will not be published in the Home app`); return; } const logLevel = { devInfo: device.log?.deviceInfo, success: device.log?.success, info: device.log?.info, warn: device.log?.warn, error: device.log?.error, debug: device.log?.debug, }; if (logLevel.debug) { log.info(`Device: ${host} ${name}, did finish launching.`); const safeConfig = { ...device, xboxLiveId: 'removed', webApi: { token: 'removed', clientSecret: 'removed', clientId: 'removed', }, mqtt: { auth: { ...device.mqtt?.auth, passwd: 'removed', }, }, }; log.info(`Device: ${host} ${name}, Config: ${JSON.stringify(safeConfig, null, 2)}.`); } // Resolve all file paths up front — before the impulse generator starts, // so a file-creation failure aborts early rather than inside the retry loop. const postFix = host.split('.').join(''); const authTokenFile = `${prefDir}/authToken_${postFix}`; const devInfoFile = `${prefDir}/devInfo_${postFix}`; const inputsFile = `${prefDir}/inputs_${postFix}`; const inputsNamesFile = `${prefDir}/inputsNames_${postFix}`; const inputsTargetVisibilityFile = `${prefDir}/inputsTargetVisibility_${postFix}`; try { const files = [ authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile, ]; files.forEach(file => { if (!existsSync(file)) { writeFileSync(file, ''); } }); } catch (error) { if (logLevel.error) log.error(`Device: ${host} ${name}, Prepare files error: ${error.message ?? error}`); return; } // The startup impulse generator retries the full connect cycle // every 120 s until it succeeds, then hands off to the xboxDevice // impulse generator and stops itself. const impulseGenerator = new ImpulseGenerator() .on('start', async () => { try { await this.startDevice({ device, name, host, authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile, logLevel, log, api, impulseGenerator, }); } catch (error) { if (logLevel.error) log.error(`Device: ${host} ${name}, Start impulse generator error: ${error.message ?? error}, trying again.`); } }) .on('state', (state) => { if (logLevel.debug) log.info(`Device: ${host} ${name}, Start impulse generator ${state ? 'started' : 'stopped'}.`); }); await impulseGenerator.state(true, [{ name: 'start', sampling: 120_000 }]); } // ── Connect and register a single Xbox device as a Homebridge accessory ─── async startDevice({ device, name, host, authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile, logLevel, log, api, impulseGenerator }) { const xboxDevice = new XboxDevice(api, device, authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile) .on('devInfo', (info) => logLevel.devInfo && log.info(info)) .on('success', (msg) => logLevel.success && log.success(`Device: ${host} ${name}, ${msg}`)) .on('info', (msg) => logLevel.info && log.info(`Device: ${host} ${name}, ${msg}`)) .on('debug', (msg) => logLevel.debug && log.info(`Device: ${host} ${name}, debug: ${msg}`)) .on('warn', (msg) => logLevel.warn && log.warn(`Device: ${host} ${name}, ${msg}`)) .on('error', (msg) => logLevel.error && log.error(`Device: ${host} ${name}, ${msg}`)); const accessory = await xboxDevice.start(); if (!accessory) return; api.publishExternalAccessories(PluginName, [accessory]); if (logLevel.success) log.success(`Device: ${host} ${name}, Published as external accessory.`); // Hand off to the xboxDevice impulse generator and stop the startup one. await xboxDevice.startStopImpulseGenerator(true, [{ name: 'connect', sampling: 6000 }]); await impulseGenerator.state(false); } // ── Homebridge accessory cache ──────────────────────────────────────────── configureAccessory(accessory) { this.accessories.push(accessory); } } export default (api) => { api.registerPlatform(PluginName, PlatformName, XboxPlatform); };