homebridge-xbox-tv
Version:
Homebridge plugin to control Xbox game consoles.
316 lines (271 loc) • 11.9 kB
HTML
<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>