homebridge-unifi-protect
Version:
Homebridge UniFi Protect plugin providing complete HomeKit integration for the entire UniFi Protect ecosystem with full support for most features including HomeKit Secure Video, multiple controllers, blazing fast performance, and much more.
345 lines (279 loc) • 19 kB
HTML
<!-- Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
index.html: homebridge-unifi-protect webUI template.
-->
<style>
.hbup-blockquote {
background-color: #E6EEF9;
border-left: 5px solid #0559C9;
color: #536878;
font-size: 0.8rem;
font-weight: 550;
}
</style>
<p class="text-center">
<img src="https://raw.githubusercontent.com/hjdhjd/homebridge-unifi-protect/main/images/homebridge-unifi-protect.svg" alt="homebridge-unifi-protect logo" class="w-50" />
</p>
<div id="pageFirstRun" style="display: none;">
<div class="mx-auto text-left" style="width: 90%;">
<p>Please enter the address of your UniFi Protect controller (e.g. unvr.local or 10.0.0.1) and the login credentials for a <strong>local user</strong> to get started with <strong>homebridge-unifi-protect</strong>.</p>
<table class="table table-sm table-borderless text-center">
<tr>
<td>
<input type="text" autocomplete="off" placeholder="Protect controller hostname or IP address" size="40" id="address" />
</td>
</tr>
<tr>
<td>
<input type="text" autocomplete="username" placeholder="Protect controller username" size="40" id="username" />
</td>
</tr>
<tr>
<td>
<input type="password" autocomplete="current-password" placeholder="Protect controller password" size="40" id="password" />
</td>
</tr>
<tr>
<td class="m-0 p-2 text-center font-weight-bold text-danger" id="loginError">
</td>
</tr>
</table>
<blockquote class="blockquote hbup-blockquote mb-0 mt-0 pb-0 pt-0">
Things to keep in mind regarding UniFi Protect:
<ul dir="auto">
<li style="font-weight: bold;">A local user account is required to use this plugin. Ubiquiti.com/UI.com accounts are not supported. You can create an account by accessing the <i>Admins & Users</i> tab in the UniFi console webUI.</li>
<li>If you have a large number of cameras, make sure you are running this plugin on a machine that has sufficient CPU resources for the best performance. The recommended configuration is to use any Apple Silicon-based macOS environment.</li>
<li>You may run into networking issues when using Docker-based environments. These are Homebridge-specific issues, not HBUP-specific ones. You can access the <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Troubleshooting.md">Troubleshooting</a> documentation for more information.</li>
</ul>
</blockquote>
</div>
<div class="text-center">
<br>
<button type="button" class="btn btn-primary" id="firstRun">Configure Plugin →</button>
<br>
To optimize performance and responsiveness, please make this plugin a <a target="_blank" href="https://github.com/homebridge/homebridge/wiki/Child-Bridges">child bridge</a> once you've completed configuration.
</div>
</div>
<div id="menuWrapper" class="btn-group w-100 mb-0" role="group" aria-label="UI Menu" style="display: none;">
<button type="button" class="btn btn-primary" id="menuSettings">Settings</button>
<button type="button" class="btn btn-primary" id="menuFeatureOptions">Feature Options</button>
<button type="button" class="btn btn-primary mr-0" id="menuHome">Support</button>
</div>
<div id="pageFeatureOptions" class="mt-2" style="display: none;" tabindex="0">
<div id="headerInfo" class="alert alert-info bg-transparent py-2 px-3 mb-2"></div>
<div class="feature-main-content">
<aside id="sidebar" class="me-3">
<div class="sidebar-content">
<nav id="controllersContainer" class="p-2"></nav>
<nav id="devicesContainer" class="p-2"></nav>
</div>
</aside>
<main class="feature-content">
<div id="deviceStatsContainer"></div>
<div id="search" class="mb-3"></div>
<div id="optionsContainer" class="options-content alert-info rounded p-3">
<div id="configTable"></div>
</div>
</main>
</div>
</div>
<div id="pageSupport" class="mt-4" style="display: none;">
<h5>Introduction</h5>
<p class="px-4">I hope you enjoy this plugin as much as I enjoy developing it. All my projects are labors of love. If you'd like to show your appreciation - <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect">star this project on GitHub</A> and do some good in your community, either financially or with your time: a food bank, an animal shelter (two of my passions), or whatever resonates with you that can give something back to the world around you. If you're looking for a couple of my faves: <a target="_blank" href="https://wck.org/donate">World Central Kitchen</a> and <a target="_blank" href="https://bestfriends.org/donate">Best Friends</a>.</p>
<div class="px-4">
Projects by <a target="_blank" href="https://github.com/hjdhjd">HJD</a>:
<!-- PROJECTS:BEGIN - Auto-generated by homebridge-plugin-utils. Do not edit this region by hand. -->
<ul dir="auto">
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-comed-hourlypricing">homebridge-comed-hourlypricing: HomeKit integration for ComEd Hourly Pricing customers.</a></li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-hunter-hydrawise">homebridge-hunter-hydrawise: Hunter Hydrawise integration for HomeKit.</a></li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-ratgdo">homebridge-ratgdo: Ratgdo (non-myQ Liftmaster and Chamberlain) garage door and gate opener support for HomeKit</a></li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access">homebridge-unifi-access: HomeKit integration for the UniFi Access ecosystem</a></li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect">homebridge-unifi-protect: Complete HomeKit integration for the entire UniFi Protect ecosystem</a></li>
<li><a target="_blank" href="https://github.com/hjdhjd/prismcast">prismcast: Chrome-based streaming server that delivers live TV from web sources to Channels DVR and Plex.</a></li>
</ul>
<!-- PROJECTS:END -->
</div>
<!-- DOCUMENTATION:BEGIN - Auto-generated by homebridge-plugin-utils. Do not edit this region by hand. -->
<h5>Getting Started</h5>
<ul dir="auto">
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/README.md#installation">Installation</a>: installing this plugin, including system requirements.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/README.md#getting-started">Plugin Configuration</a>: how to quickly get up and running.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/BestPractices.md">Best Practices</a>: best practices for getting the most of your HomeKit setup and UniFi Protect.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Troubleshooting.md">Troubleshooting</a>: running into login problems or streaming issues? Give this a read before looking anywhere else.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Changelog.md">Changelog</a>: changes and release history of this plugin, starting with v3.0.</li>
</ul>
<h5>Additional Topics</h5>
<ul dir="auto">
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/FeatureOptions.md">Feature Options</a>: granular options to allow you to set the camera quality individually, show or hide specific cameras, controllers, and more.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Autoconfiguration.md">Autoconfiguration</a>: how HBUP sources and selects your video (the timeshift buffer, stream selection, transcoding, and transmuxing), autoconfigures your controller, and why.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/AudioOptions.md">Audio Options</a>: options to further tailor how audio is handled from Protect, such as background noise reduction.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Doorbell.md">Doorbells</a>: how UniFi Protect doorbell support works in this plugin, and how to use all the available features including doorbell messages.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/HomeKitSecureVideo.md">HomeKit Secure Video</a>: how HomeKit Secure Video support works in this plugin with UniFi Protect.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Liveviews.md">Liveview Scenes</a>: use the UniFi Protect liveviews feature (available in the UniFi Protect controller webUI) to create motion-detection scenes.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Viewports.md">Viewports</a>: control what a UniFi Protect Viewport displays on screen, right from HomeKit.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/MQTT.md">MQTT</a>: how to configure MQTT support.</li>
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/ConfigurationReference.md">Plugin Configuration Reference</a>: complete list of configuration options available in this plugin.</li>
</ul>
<!-- DOCUMENTATION:END -->
<h5>Support</h5>
<ul>
<li>
<a target="_blank" href="https://discord.gg/QXqfHEW">Discord Support Channel</a>
</li>
<li>
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/issues/new/choose">Create a Developer Support Request</a>
</li>
<li>
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Changelog.md">View the Changelog and Release Notes</a>
</li>
</ul>
</div>
<!-- WEBUI LOADER CONFIG {"bust":["./protect-config.mjs"],"entry":"./ui.mjs"} -->
<!-- WEBUI LOADER:BEGIN - Auto-generated by homebridge-plugin-utils. Do not edit this region by hand. -->
<div id="pageBootError" class="mt-4 text-center" style="display: none;">
<h5>The settings interface couldn't load.</h5>
<div id="bootErrorMessage">
<p data-boot-bucket="browser" style="display: none;">Your browser doesn't support features this interface requires. Please use a current version of Safari or Chrome.</p>
<p data-boot-bucket="delivery" style="display: none;">The interface files couldn't be retrieved from the Homebridge server. Reload the page to try again. If this keeps happening, log out of the Homebridge interface and log back in.</p>
<p data-boot-bucket="generic" style="display: none;">An unexpected error occurred while starting the interface.</p>
</div>
<details open>
<summary>Technical details</summary>
<div id="bootErrorDetails"></div>
</details>
</div>
<div id="bootSlowNotice" class="mt-4 text-center" style="display: none;">This is taking longer than expected. If nothing appears shortly, reload the page.</div>
<script>
/* Boot monitor for the plugin webUI, generated by homebridge-plugin-utils - do not edit this region by hand. This is a classic inline script, so it runs during
* HTML parse ahead of the deferred module loader below and can report a failure anywhere along the boot path. It reveals the hidden status panel with the message
* bucket the failing stage names, arms a ten-second watchdog for a boot that hangs, and stands down when webUi.show() signals the app rendered or displayed its
* own error. The classic-versus-module split is for execution ordering alone: a classic inline script runs during parse while a module script is deferred.
*/
(function() {
// A second execution in the same window - a reused iframe across repeated settings-panel opens - returns immediately, so the first run's listeners and watchdog
// are never duplicated into an orphaned registration that could resurrect the panel over a working app.
if(window.webUiBoot) {
return;
}
const watchdogMs = 10000;
// The stage-to-bucket map. The browser stage stands alone, the manifest and import delivery stages share the delivery bucket, and every other stage - an
// uncaught window error or unhandled rejection included - falls through the nullish default to the generic bucket.
const buckets = { browser: "browser", import: "delivery", manifest: "delivery" };
let settled = false;
let watchdog;
// Set an element's display by id, tolerating an absent node so a stamped region missing a panel element never throws from the monitor.
const setDisplay = (id, value) => {
const element = document.getElementById(id);
if(element) {
element.style.display = value;
}
};
// Stop watching: clear the watchdog and remove both window listeners. Called from the first failure and from ready(), so nothing is left open once the boot has
// resolved one way or the other.
const stop = () => {
clearTimeout(watchdog);
window.removeEventListener("error", onError);
window.removeEventListener("unhandledrejection", onRejection);
};
// The first failure wins. Reveal the panel with the classified bucket message and the technical details, retract any slow notice the watchdog raised, stop
// watching, and drop the host spinner so it cannot mask the panel. A later failure, and any failure after ready(), is ignored. The handlers never call
// preventDefault(), so the browser console keeps every error and the panel only complements it.
const fail = (stage, error) => {
if(settled) {
return;
}
settled = true;
stop();
// Reveal the one pre-rendered message the failing stage's bucket names; the bucket keys are internal constants, never user input, so the selector is safe.
const chosen = document.querySelector("#bootErrorMessage [data-boot-bucket='" + (buckets[stage] ?? "generic") + "']");
if(chosen) {
chosen.style.display = "block";
}
const details = document.getElementById("bootErrorDetails");
if(details) {
// The error text is untrusted, so each detail line is assigned as plain text through textContent and is never parsed as markup.
const lines = [ "Stage: " + stage, "Error: " + ((error instanceof Error) ? error.message : String(error)), "Browser: " + navigator.userAgent ];
details.textContent = "";
for(const line of lines) {
const row = document.createElement("div");
row.textContent = line;
details.appendChild(row);
}
}
setDisplay("bootSlowNotice", "none");
setDisplay("pageBootError", "block");
globalThis.homebridge?.hideSpinner?.();
};
// The app is authoritative and supersedes anything the monitor showed. webUi.show() calls this from its finally once it has rendered or displayed its own toast,
// so an earlier boot-phase error was evidently non-fatal - browser-extension noise on window.onerror is real. Stop watching, retract both the slow notice and
// the panel, and mark settled so a later fail() is a no-op.
const ready = () => {
settled = true;
stop();
setDisplay("bootSlowNotice", "none");
setDisplay("pageBootError", "none");
};
const onError = (event) => fail("uncaught error", event.error ?? event.message);
const onRejection = (event) => fail("unhandled rejection", event.reason);
window.webUiBoot = Object.freeze({ fail, ready });
window.addEventListener("error", onError);
window.addEventListener("unhandledrejection", onRejection);
// Arm the watchdog. Nothing reported by the deadline means the boot has stalled, so show the slow notice in the spinner's place and drop the spinner; a boot
// that finishes later retracts the notice through ready(). The watchdog tears nothing down - boot may still succeed.
watchdog = setTimeout(() => {
setDisplay("bootSlowNotice", "block");
globalThis.homebridge?.hideSpinner?.();
}, watchdogMs);
})();
</script>
<script type="module">
/* Plugin webUI loader, generated by homebridge-plugin-utils from the loader-config comment above - do not edit this region by hand. It reads the
* homebridge-plugin-utils manifest at load time and injects an importmap mapping the bare "homebridge-plugin-utils/" specifier to the hashed-versioned subdir the
* prepare-ui CLI mirrors into place; the trailing-slash prefix carries transitive imports through relative-URL resolution, and each hand-authored module carries
* a Date.now() cache-bust so a single page load stays coherent across them. The body is stage-instrumented: a fetch, importmap, or entry-import failure is
* reported to the boot monitor above with the stage that failed, which reveals the on-page status panel. The import-stage probe proves the injected importmap
* applied - a browser that ignored it lands in the browser bucket - but it cannot tell a delivery failure from a top-level throw inside a delivered module, so
* that case knowingly folds into the delivery bucket and the panel's raw error line is what disambiguates it for support.
*/
const cb = Date.now();
const bust = (path) => new URL(path, import.meta.url).href + "?cb=" + cb;
let stage = "manifest";
try {
const response = await fetch("./lib/manifest.json", { cache: "no-store" });
if(!response.ok) {
throw new Error("The manifest request failed with HTTP status " + response.status + ".");
}
const manifest = await response.json();
const importMap = {
imports: {
"./protect-config.mjs": bust("./protect-config.mjs"),
"./ui.mjs": bust("./ui.mjs"),
"homebridge-plugin-utils/": new URL("./lib/" + manifest.subdir + "/", import.meta.url).href
}
};
const mapScript = document.createElement("script");
mapScript.type = "importmap";
mapScript.textContent = JSON.stringify(importMap);
document.head.appendChild(mapScript);
stage = "import";
await import("./ui.mjs");
} catch(error) {
// The monitor owns the on-page display, so after classifying we report and swallow: rethrowing would only add a duplicate uncaught error to the console the
// original failure already produced.
if(stage === "import") {
if(typeof import.meta.resolve !== "function") {
window.webUiBoot.fail("browser", error);
} else {
try {
import.meta.resolve("homebridge-plugin-utils/webUi.mjs");
window.webUiBoot.fail("import", error);
} catch {
window.webUiBoot.fail("browser", error);
}
}
} else {
window.webUiBoot.fail("manifest", error);
}
}
</script>
<!-- WEBUI LOADER:END -->