UNPKG

homebridge-xbox-tv

Version:

Homebridge plugin to control Xbox game consoles.

316 lines (271 loc) • 11.9 kB
<div class="container mt-3"> <div class="text-center"> <img src="homebridge-xbox-tv.png" alt="Image" height="120" /> </div> <div id="authorizationManager" class="card card-body mt-2"> <form id="configForm"> <div class="text-center"> <label id="deviceName" class="fw-bold" style="font-size: 23px;">Xbox</label><br> <label id="info" class="d-block" style="font-size: 17px;"></label> <label id="info1" class="d-block" style="font-size: 15px;"></label> </div> <div class="mb-2"> <label for="deviceHost" class="form-label">Host</label> <input id="deviceHost" type="text" class="form-control" required> </div> <div class="mb-2 position-relative"> <label for="deviceLiveId" class="form-label">Xbox Live ID</label> <div class="input-group"> <input id="deviceLiveId" type="password" class="form-control" autocomplete="new-password" required> <button type="button" id="toggleLiveId" class="btn btn-outline-secondary"> <i class="fas fa-eye"></i> </button> </div> </div> <div class="mb-2 position-relative"> <label for="deviceToken" class="form-label">Web API Token</label> <div class="input-group"> <input id="deviceToken" type="password" class="form-control" autocomplete="new-password" required> <button type="button" id="toggleToken" class="btn btn-outline-secondary"> <i class="fas fa-eye"></i> </button> </div> </div> <div class="form-check mb-2"> <input id="deviceWebApiControl" type="checkbox" class="form-check-input"> <label for="deviceWebApiControl" class="form-check-label">Web API Control</label> </div> <div class="text-center"> <button id="startAuthorizationButton" type="button" class="btn btn-primary">Start Authorization</button> <button id="clearTokenButton" type="button" class="btn btn-secondary">Clear Web API Token</button> <button id="configButton" type="button" class="btn btn-secondary"><i class="fas fa-gear"></i></button> </div> </form> <div id="consoleButton" class="d-flex flex-wrap justify-content-center gap-1 mt-3"></div> </div> </div> <script> (async () => { const pluginConfig = await homebridge.getPluginConfig(); if (!pluginConfig.length) { pluginConfig.push({}); await homebridge.updatePluginConfig(pluginConfig); homebridge.showSchemaForm(); return; } const devices = pluginConfig[0].devices || []; const devicesCount = devices.length; // Helper to get DOM elements const $ = id => document.getElementById(id); // Helper to set button classes const setButtonClass = (activeIndex) => { for (let j = 0; j < devicesCount; j++) { $(`button${j}`).className = j === activeIndex ? "btn btn-primary" : "btn btn-secondary"; } }; // Helper to update the device form const updateDeviceForm = (device) => { $('deviceName').innerHTML = device.name || ''; $('deviceHost').value = device.host || ''; $('deviceLiveId').value = device.xboxLiveId || ''; $('deviceToken').value = device.webApi.token || ''; $('deviceWebApiControl').checked = device.webApi.enable || false; const tokenLength = device.webApi.token?.length || 0; const btn = $('startAuthorizationButton'); if (btn.dataset.phase !== 'activate') { btn.innerText = tokenLength <= 10 ? "Start Authorization" : "Check State"; btn.dataset.phase = tokenLength <= 10 ? 'start' : 'check'; } $('deviceWebApiControl').disabled = tokenLength <= 10; if (tokenLength <= 10) { $('deviceWebApiControl').checked = false; device.webApi.enable = false; } }; // Create buttons for each device const container = document.getElementById("consoleButton"); container.style.display = 'flex'; container.style.flexWrap = 'wrap'; container.style.justifyContent = 'center'; container.style.gap = '0.25rem'; devices.forEach((device, i) => { this.device = device; const button = document.createElement("button"); button.type = "button"; button.id = `button${i}`; button.className = "btn btn-primary"; button.style.textTransform = 'none'; button.innerText = device.name || `Device ${i + 1}`; container.appendChild(button); button.addEventListener("click", async () => { setButtonClass(i); updateDeviceForm(devices[i]); this.device = device; }); // Auto-select the first device if (i === devicesCount - 1) { $("button0").click(); } }); // Show the authorization form $("authorizationManager").style.display = "block"; // Config button toggle let configButtonState = false; $("configButton").addEventListener("click", () => { configButtonState = !configButtonState; homebridge[configButtonState ? 'showSchemaForm' : 'hideSchemaForm'](); configButton.className = configButtonState ? 'btn btn-primary' : 'btn btn-secondary'; }); // Token toggle $("toggleLiveId").addEventListener("click", () => { const liveIdInput = $("deviceLiveId"); const icon = document.querySelector('#toggleLiveId i'); if (liveIdInput.type === 'password') { liveIdInput.type = 'text'; icon.classList.replace('fa-eye', 'fa-eye-slash'); } else { liveIdInput.type = 'password'; icon.classList.replace('fa-eye-slash', 'fa-eye'); } }); // Token toggle $("toggleToken").addEventListener("click", () => { const tokenInput = $("deviceToken"); const icon = document.querySelector('#toggleToken i'); if (tokenInput.type === 'password') { tokenInput.type = 'text'; icon.classList.replace('fa-eye', 'fa-eye-slash'); } else { tokenInput.type = 'password'; icon.classList.replace('fa-eye-slash', 'fa-eye'); } }); // Update config on form input $("configForm").addEventListener("input", async () => { const currentDevice = this.device; currentDevice.host = $('deviceHost').value; currentDevice.xboxLiveId = $('deviceLiveId').value; currentDevice.webApi.token = $('deviceToken').value; currentDevice.webApi.enable = $('deviceWebApiControl').checked; const tokenLength = currentDevice.webApi.token?.length || 0; const authBtn = $('startAuthorizationButton'); if (authBtn.dataset.phase !== 'activate') { authBtn.innerText = tokenLength <= 10 ? "Start Authorization" : "Check State"; authBtn.dataset.phase = tokenLength <= 10 ? 'start' : 'check'; } if (tokenLength <= 10) { authBtn.removeAttribute('disabled'); } await homebridge.updatePluginConfig(pluginConfig); await homebridge.savePluginConfig(pluginConfig); }); function updateInfo(id, text, color) { const el = document.getElementById(id); if (el) { el.innerText = text; el.style.color = color; } } // Trigger the operation via homebridge.request() (Socket.IO — unreliable for response), // then poll _result.json via HTTP until the server writes the result. // HTTP fetch bypasses the unreliable Socket.IO server→client path entirely. async function requestViaFile(path, body, ms = 15000) { const ts = Date.now(); homebridge.request(path, body).catch(() => {}); // fire-and-forget const deadline = ts + ms; while (Date.now() < deadline) { await new Promise(r => setTimeout(r, 300)); try { const res = await fetch(`_result.json?t=${Date.now()}`); if (!res.ok) continue; const data = await res.json(); if ((data.ts ?? 0) >= ts) { if (data.ok) return data.data ?? true; throw new Error(data.error || 'Unknown error'); } } catch (e) { if (e.message && e.message !== 'Failed to fetch') throw e; } } throw new Error('Request timed out'); } // Clear token button logic $("clearTokenButton").addEventListener("click", async () => { $("clearTokenButton").disabled = true; updateInfo('info', 'Clearing token...', 'yellow'); updateInfo('info1', '', ''); homebridge.showSpinner(); try { const host = this.device.host; await requestViaFile('/clearToken', { host }, 10000); Object.assign(this.device, { webApi: { token: '', enable: false } }); updateDeviceForm(this.device); updateInfo('info', "Web API token cleared. Now you can start a new authorization process.", "green"); $("startAuthorizationButton").removeAttribute("disabled"); homebridge.hideSpinner(); await homebridge.updatePluginConfig(pluginConfig); await homebridge.savePluginConfig(pluginConfig); } catch (error) { updateInfo('info', "Clear Web API token error.", "red"); updateInfo('info1', String(error), "red"); } finally { homebridge.hideSpinner(); $("clearTokenButton").disabled = false; } }); // Start authorization logic $("startAuthorizationButton").addEventListener("click", async () => { const phase = $("startAuthorizationButton").dataset.phase || 'start'; $("startAuthorizationButton").disabled = true; const infoMsg = phase === 'activate' ? "Activating console..." : phase === 'check' ? "Checking authorization state..." : "Starting authorization..."; updateInfo('info', infoMsg, "yellow"); updateInfo('info1', '', ''); homebridge.showSpinner(); try { const { host, webApi } = this.device; const { token, clientId, clientSecret } = webApi; const response = await requestViaFile('/startAuthorization', { host, token, clientId, clientSecret }, 60000); const { info, status } = response; switch (status) { case 0: // Authorized updateInfo('info', info, "green"); $("startAuthorizationButton").innerText = "Check State"; $("startAuthorizationButton").dataset.phase = 'check'; $("deviceWebApiControl").disabled = false; break; case 1: // Needs user interaction $("startAuthorizationButton").innerText = "Activate Console"; $("startAuthorizationButton").dataset.phase = 'activate'; $("deviceWebApiControl").checked = false; $("deviceWebApiControl").disabled = true; webApi.enable = false; open(info); updateInfo('info', "Sign in to Xbox Live. After redirected to localhost:8888, copy everything after ?code= and paste it into the Web API Token field, then press Activate Console.", "yellow"); updateInfo('info1', '', ''); break; case 2: // Successfully authorized updateInfo('info', info, "green"); $("startAuthorizationButton").innerText = "Check State"; $("startAuthorizationButton").dataset.phase = 'check'; $("deviceWebApiControl").disabled = false; $("deviceWebApiControl").checked = true; webApi.enable = true; homebridge.hideSpinner(); await homebridge.updatePluginConfig(pluginConfig); await homebridge.savePluginConfig(pluginConfig); break; } } catch (error) { updateInfo('info', "Authorization error.", "red"); updateInfo('info1', JSON.stringify(error), "red"); } finally { homebridge.hideSpinner(); $("startAuthorizationButton").disabled = false; } }); })(); </script>