Skip to content

Commit

Permalink
Fixed some more issues
Browse files Browse the repository at this point in the history
- Camera ips are now updated at startup
- Reimplemented network logic for the new node-fetch logic
- Accessories that are unregistered are also no longer used to init motion detection (previously these would only not be used after a second reboot of homebridge)
  • Loading branch information
Kevin Van den Abeele committed Aug 6, 2020
1 parent ce81fc6 commit 87e21dc
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 33 deletions.
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "homebridge-unifi-protect-camera-motion",
"version": "0.4.2-beta.1",
"version": "0.4.2-beta.3",
"description": "Unifi Protect cameras & motion sensors for Homekit with Homebridge",
"main": "src/index.js",
"scripts": {
Expand Down
39 changes: 22 additions & 17 deletions src/unifi-protect-motion-platform.ts
Expand Up @@ -26,8 +26,7 @@ export class UnifiProtectMotionPlatform implements DynamicPlatformPlugin {
public readonly hap: HAP = this.api.hap;
public readonly Accessory: typeof PlatformAccessory = this.api.platformAccessory;

private readonly accessories: Array<PlatformAccessory> = [];

private accessories: Array<PlatformAccessory> = [];
private unifi: Unifi;
private uFlows: UnifiFlows;

Expand Down Expand Up @@ -56,6 +55,7 @@ export class UnifiProtectMotionPlatform implements DynamicPlatformPlugin {
});
}

// This is called by us and is executed after existing accessories have been restored.
public async didFinishLaunching(): Promise<void> {
let cameras: UnifiCamera[] = [];
try {
Expand All @@ -81,32 +81,39 @@ export class UnifiProtectMotionPlatform implements DynamicPlatformPlugin {
// Camera names must be unique
const uuid = this.hap.uuid.generate(camera.id);
camera.uuid = uuid;
const cameraAccessory = new this.Accessory(camera.name, uuid);

cameraAccessory.context.cameraConfig = {
uuid: uuid,
name: camera.name,
camera: camera
} as CameraConfig;
// Only add new cameras that are not cached
const existingAccessory: PlatformAccessory = this.accessories.find((x: PlatformAccessory) => x.UUID === uuid);
if (!existingAccessory) {
const cameraAccessory = new this.Accessory(camera.name, uuid);
cameraAccessory.context.cameraConfig = {
uuid: uuid,
name: camera.name,
camera: camera
} as CameraConfig;

UnifiCameraAccessoryInfo.createAccessoryInfo(camera, cameraAccessory, this.hap);
UnifiCameraAccessoryInfo.createAccessoryInfo(camera, cameraAccessory, this.hap);

// Only add new cameras that are not cached
if (!this.accessories.find((x: PlatformAccessory) => x.UUID === uuid)) {
this.log.info('Adding ' + cameraAccessory.context.cameraConfig.uuid + ' (' + cameraAccessory.context.cameraConfig.name + ')');
this.configureAccessory(cameraAccessory); // abusing the configureAccessory here
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cameraAccessory]);
} else {
// Update the ip of the camera
existingAccessory.context.cameraConfig.camera.ip = cameras.find((cam: UnifiCamera) => cam.id === existingAccessory.context.cameraConfig.camera.id).ip;
}
});

// Remove cameras that were not in previous call
this.accessories.forEach((accessory: PlatformAccessory) => {
this.accessories = this.accessories.filter((accessory: PlatformAccessory) => {
if (!cameras.find((x: UnifiCamera) => x.uuid === accessory.context.cameraConfig.uuid)) {
this.log.info('Removing ' + accessory.context.cameraConfig.uuid + ' (' + accessory.context.cameraConfig.name + ')');
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
} else {
return accessory;
}
});

// Set up the motion detection for all valid accessories
try {
const motionDetector: MotionDetector = new MotionDetector(this.api, this.config, this.uFlows, cameras, this.log);
await motionDetector.setupMotionChecking(this.accessories);
Expand All @@ -117,6 +124,7 @@ export class UnifiProtectMotionPlatform implements DynamicPlatformPlugin {
}
}

// This is called manually by us for newly added accessories, and is called automatically by Homebridge for accessories that have already been added!
public configureAccessory(cameraAccessory: PlatformAccessory): void {
this.log.info('Configuring accessory ' + cameraAccessory.displayName);

Expand All @@ -125,12 +133,9 @@ export class UnifiProtectMotionPlatform implements DynamicPlatformPlugin {
});

const cameraConfig: CameraConfig = cameraAccessory.context.cameraConfig;

//TODO: Update the ip of the camera since that could have changed!

//Update the camera config!
// Update the camera config!
const videoConfigCopy: VideoConfig = JSON.parse(JSON.stringify(this.config.videoConfig));
//Assign stillImageSource, source and debug (overwrite if they are present from the videoConfig, which they should not be!)
// Assign stillImageSource, source and debug (overwrite if they are present from the videoConfig, which they should not be!)
videoConfigCopy.stillImageSource = '-i http://' + cameraConfig.camera.ip + '/snap.jpeg';
videoConfigCopy.source = '-rtsp_transport tcp -re -i ' + this.config.unifi.controller_rtsp + '/' + Unifi.pickHighestQualityAlias(cameraConfig.camera.streams);
videoConfigCopy.debug = this.config.unifi.debug;
Expand Down
32 changes: 20 additions & 12 deletions src/unifi/unifi.ts
Expand Up @@ -10,17 +10,15 @@ export class Unifi {
private readonly initialBackoffDelay: number;
private readonly maxRetries: number;
private readonly log: Logging;
private readonly networkLogger: Logging;

constructor(config: UnifiConfig, initialBackoffDelay: number, maxRetries: number, log: Logging) {
this.config = config;
this.initialBackoffDelay = initialBackoffDelay;
this.maxRetries = maxRetries;

this.log = log;

if (this.config.debug_network_traffic) {
//TODO: Fix network debugging
}
this.networkLogger = this.config.debug_network_traffic ? this.log : undefined;
}

public static async determineEndpointStyle(baseControllerUrl: string, log: Logging): Promise<UnifiEndPointStyle> {
Expand All @@ -30,7 +28,10 @@ export class Unifi {

const headers: Headers = new Headers();
headers.set('Content-Type', 'application/json');
const response: Response = await Utils.fetch(baseControllerUrl, {method: 'GET'}, headers);
const response: Response = await Utils.fetch(baseControllerUrl,
{method: 'GET'},
headers
);

const csrfToken = response.headers.get('X-CSRF-Token');
if (csrfToken) {
Expand Down Expand Up @@ -64,8 +65,9 @@ export class Unifi {

const loginPromise: Promise<Response> = Utils.fetch(endpointStyle.authURL, {
body: JSON.stringify({username: username, password: password}),
method: 'POST'
}, headers);
method: 'POST'},
headers, this.networkLogger
);
const response: Response = await Utils.backOff(this.maxRetries, loginPromise, this.initialBackoffDelay);

this.log.debug('Authenticated, returning session');
Expand All @@ -84,7 +86,7 @@ export class Unifi {
}

public isSessionStillValid(session: UnifiSession): boolean {
//Validity duration for now set at 12 hours!
// Validity duration for now set at 12 hours!
if (session) {
if ((session.timestamp + (12 * 3600 * 1000)) >= Date.now()) {
return true;
Expand All @@ -107,7 +109,10 @@ export class Unifi {
headers.set('Authorization', 'Bearer ' + session.authorization)
}

const bootstrapPromise: Promise<Response> = Utils.fetch(endPointStyle.apiURL + '/bootstrap', {method: 'GET'}, headers);
const bootstrapPromise: Promise<Response> = Utils.fetch(endPointStyle.apiURL + '/bootstrap',
{method: 'GET'},
headers, this.networkLogger
);
const response: Response = await Utils.backOff(this.maxRetries, bootstrapPromise, this.initialBackoffDelay);
const cams = (await response.json()).cameras;

Expand All @@ -131,7 +136,7 @@ export class Unifi {
}
}

//Sort streams on highest res!
// Sort streams on highest res!
streams.sort((a: UnifiCameraStream, b: UnifiCameraStream): number => {
return (a.height * a.width) - (b.height * b.width);
});
Expand Down Expand Up @@ -161,7 +166,10 @@ export class Unifi {
} else {
headers.set('Authorization', 'Bearer ' + session.authorization)
}
const eventsPromise: Promise<Response> = Utils.fetch(endPointStyle.apiURL + '/events?end=' + endEpoch + '&start=' + startEpoch + '&type=motion', {method: 'GET'}, headers);
const eventsPromise: Promise<Response> = Utils.fetch(endPointStyle.apiURL + '/events?end=' + endEpoch + '&start=' + startEpoch + '&type=motion',
{method: 'GET'},
headers, this.networkLogger
);
const response: Response = await Utils.backOff(this.maxRetries, eventsPromise, this.initialBackoffDelay);

const events: any[] = await response.json();
Expand All @@ -175,7 +183,7 @@ export class Unifi {
cameraId: event.camera,
camera: null,
score: event.score,
timestamp: event.start //event.end is null when the motion is still ongoing!
timestamp: event.start // event.end is null when the motion is still ongoing!
}
});
}
Expand Down
27 changes: 24 additions & 3 deletions src/utils/utils.ts
@@ -1,9 +1,10 @@
import * as https from "https";
import fetch, { Headers, Response, RequestInfo, RequestInit } from "node-fetch";
import {Logging, LogLevel} from "homebridge";

export class Utils {

//Since Protect often uses self-signed certificates, we need to disable TLS validation.
// Since Protect often uses self-signed certificates, we need to disable TLS validation.
private static httpsAgent = new https.Agent({
rejectUnauthorized: false
});
Expand All @@ -24,11 +25,20 @@ export class Utils {
}
}

public static async fetch(url: RequestInfo, options: RequestInit, headers: Headers): Promise<Response> {
public static async fetch(url: RequestInfo, options: RequestInit, headers: Headers, networkLogger: Logging = this.fakeLogging()): Promise<Response> {
options.agent = this.httpsAgent;
options.headers = headers;

networkLogger.debug('Calling: ' + url);
networkLogger.debug('Method: ' + options.method);
networkLogger.debug('With headers: ' + JSON.stringify(options.headers, null, 4));
if (options.body) {
networkLogger.debug('Body: ' + JSON.stringify(options.body, null, 4));
}

let response: Response = await fetch(url, options);
networkLogger.debug('Response: \n' + JSON.stringify(response, null, 4));

if (response.status === 401) {
throw new Error('Invalid credentials');
}
Expand All @@ -38,7 +48,18 @@ export class Utils {
if (!response.ok) {
throw new Error('Invalid response: ' + response);
}

return response;
}

public static fakeLogging(): Logging {
// @ts-ignore
return {
prefix: "FAKE_LOGGING",
error(message: string, ...parameters: any[]): void {},
info(message: string, ...parameters: any[]): void {},
log(level: LogLevel, message: string, ...parameters: any[]): void {},
warn(message: string, ...parameters: any[]): void {},
debug(message: string, ...parameters: any[]): void {},
}
}
}

0 comments on commit 87e21dc

Please sign in to comment.