unifi-protect
Version:
A complete implementation of the UniFi Protect API.
182 lines • 7.79 kB
JavaScript
/* Copyright(C) 2019-2025, HJD (https://github.com/hjdhjd). All rights reserved.
*
* ufp.ts: UniFi Protect API command line utility.
*/
import { ProtectApi } from "../index.js";
import { existsSync, readFileSync } from "fs";
import { homedir } from "os";
import util from "util";
// Create a new Protect API instance.
const ufp = new ProtectApi();
// Log utilities.
const log = {
/* eslint-disable no-console */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
debug: (message, ...parameters) => { },
error: (message, ...parameters) => console.error(util.format(message, ...parameters)),
info: (message, ...parameters) => console.log(util.format(message, ...parameters)),
warn: (message, ...parameters) => console.log(util.format(message, ...parameters))
/* eslint-enable no-console */
};
// Read our credentials.
let config;
try {
// We look for credentials in the local directory as well as in the user's home directory.
const configFile = ["ufp.json", homedir() + "/.ufp.json"].find(path => existsSync(path));
if (!configFile) {
throw new Error;
}
// Credentials must be in JSON form with properties for the controller, username, and password.
config = JSON.parse(readFileSync(configFile, "utf8"));
if (!config.controller || !config.username || !config.password) {
throw new Error;
}
}
catch (e) {
// Inform the user we don't know what to connect to.
log.error("No credentials found in ./ufp.json or ~/.ufp.json. Credentials must be in JSON form with properties for controller, username, and password defined.");
usage();
}
// Login to the Protect controller.
if (!(await ufp.login(config.controller, config.username, config.password))) {
log.error("Invalid login credentials.");
process.exit(0);
}
;
// Bootstrap the controller.
if (!(await ufp.getBootstrap())) {
log.error("Unable to bootstrap the Protect controller.");
process.exit(0);
}
// Silently exit on pipe errors but still process other errors.
process.stdout.on("error", (err) => {
if (err.code === "EPIPE") {
process.exit(0);
}
else {
throw err;
}
});
let camera;
let channel = 0;
let ls;
// Command line processing.
switch (process.argv.length) {
case 0:
case 1:
case 2:
usage();
break;
default:
switch (process.argv[2]?.toLowerCase()) {
// Output the bootstrap.
case "bootstrap":
// Output the bootstrap JSON and we're done.
process.stdout.write(util.inspect(ufp.bootstrap, { colors: true, depth: null, sorted: true }) + "\n", () => process.exit(0));
break;
// Output realtime events.
case "events":
ufp.on("message", (packet) => {
if (process.argv.length === 5) {
if (packet.header[process.argv[3]]?.toLowerCase() !== process.argv[4]?.toLowerCase()) {
return;
}
}
log.info(util.inspect(packet, { colors: true, depth: null, sorted: true }));
});
break;
// Set channel IDR intervals.
case "idr":
// We can only specify a valid number here.
if (Number.isNaN(Number(process.argv[3])) || !Number.isInteger(Number(process.argv[3])) || (Number(process.argv[3]) < 1) || (Number(process.argv[3]) > 5)) {
usage();
}
// Restart every camera.
for (const device of ufp.bootstrap?.cameras.filter(camera => !camera.isThirdPartyCamera) ?? []) {
// Update the Protect controller with the new channel map and IDR interval.
//eslint-disable-next-line no-await-in-loop
await ufp.updateDevice(device, { channels: device.channels.map(x => Object.assign(x, { idrInterval: Number(process.argv[3]) })) });
log.info("%s: IDR set.", ufp.getDeviceName(device));
}
process.exit(0);
break;
// Restart devices.
case "restart":
if (process.argv[3] !== "cameras") {
usage();
}
// Restart every camera.
for (const device of ufp.bootstrap?.cameras.filter(camera => !camera.isRebooting && camera.state === "CONNECTED") ?? []) {
//eslint-disable-next-line no-await-in-loop
const response = await ufp.retrieve(ufp.getApiEndpoint(device.modelKey) + "/" + device.id + "/reboot", { body: JSON.stringify({}), method: "POST" });
if (!response?.ok) {
log.error("%s: unable to reboot: %s", ufp.getDeviceName(device), response);
break;
}
log.info("%s: restarted.", ufp.getDeviceName(device));
}
process.exit(0);
break;
case "stream":
if (![4, 5].includes(process.argv.length)) {
usage();
break;
}
camera = ufp.bootstrap?.cameras.find(camera => camera.name?.toLowerCase() === process.argv[3].toLowerCase());
if (!camera) {
usage();
break;
}
if (process.argv.length === 5) {
channel = camera.channels.find(channel => channel.name?.toLowerCase() === process.argv[4].toLowerCase())?.id;
if (channel === undefined) {
usage();
break;
}
}
ls = ufp.createLivestream();
await ls.start(camera.id, channel, { useStream: true });
ls.stream?.pipe(process.stdout);
break;
// Unknown command.
default:
usage();
break;
}
;
break;
}
// Usage information.
function usage() {
log.error("Usage: %s bootstrap", process.argv[1]);
log.error("Usage: %s events [action | id | modelKey | other_event_header_property] [value] (all parameters are case-sensitive)", process.argv[1]);
log.error("Usage: %s restart cameras", process.argv[1]);
log.error("Usage: %s stream [camera name]", process.argv[1]);
log.error("");
// If we're bootstrapped, we also want to inform users what the various device identifiers are to make filtering events easier.
if (ufp.bootstrap) {
if (ufp.bootstrap.cameras.length) {
log.error("Cameras:");
ufp.bootstrap.cameras.map(device => log.error(" %s => %s", device.name ?? device.marketName, device.id));
}
if (ufp.bootstrap.chimes.length) {
log.error("Chimes:");
ufp.bootstrap.chimes.map(device => log.error(" %s => %s", device.name ?? device.marketName, device.id));
}
if (ufp.bootstrap.lights.length) {
log.error("Lights:");
ufp.bootstrap.lights.map(device => log.error(" %s => %s", device.name ?? device.marketName, device.id));
}
if (ufp.bootstrap.sensors.length) {
log.error("Sensors:");
ufp.bootstrap.sensors.map(device => log.error(" %s => %s", device.name ?? device.marketName, device.id));
}
if (ufp.bootstrap.viewers.length) {
log.error("Viewers:");
ufp.bootstrap.viewers.map(device => log.error(" %s => %s", device.name ?? device.marketName, device.id));
}
}
process.exit(1);
}
//# sourceMappingURL=ufp.js.map