This commit is contained in:
watsonb8
2020-11-27 23:03:12 -05:00
parent 57eb43c4bc
commit 3ccf85cb00
11 changed files with 1178 additions and 2 deletions

27
src/config.ts Normal file
View File

@ -0,0 +1,27 @@
import { PlatformConfig } from "homebridge";
export interface IConfig extends PlatformConfig {
refImageDirectory: string;
trainedModelDirectory: string;
weightDirectory: string;
trainOnStartup: boolean;
rooms: Array<IRoom>;
}
export interface IRoom {
name: string;
rtspConnectionStrings: Array<string>;
}
export const isRoom = (object: any): object is IRoom => {
return "name" in object && "rtspCameraConnectionString" in object;
};
export const isConfig = (object: any): object is IConfig => {
return (
"refImageDirectory" in object &&
"trainedModelDirectory" in object &&
"rooms" in object &&
isRoom(object["rooms"])
);
};

View File

@ -1 +1,11 @@
console.log("Hello World");
import { API } from "homebridge";
import { PLATFORM_NAME } from "./settings";
import { HomeLocationPlatform } from "./platform";
/**
* This method registers the platform with Homebridge
*/
export = (api: API) => {
api.registerPlatform(PLATFORM_NAME, HomeLocationPlatform);
};

39
src/monitor.ts Normal file
View File

@ -0,0 +1,39 @@
import { FaceMatcher } from "@vladmandic/face-api";
import { IRoom } from "./config";
import { Rtsp } from "rtsp-stream/lib";
export interface ILabelState {
label: string;
detected: boolean;
}
export type IRoomState = { [roomName: string]: Array<ILabelState> };
export class LocationMonitor {
private _state: IRoomState = {};
private _streams: Array<Rtsp> = [];
constructor(private rooms: Array<IRoom>, private matcher: FaceMatcher) {
//Initialize state
for (const room of rooms) {
this._streams.push(
...room.rtspConnectionStrings.map(
(e) => new Rtsp(e, { rate: 0.5, image: true })
)
);
this._state[room.name] = matcher.labeledDescriptors.map((e) => {
return { label: e.label, detected: false };
});
}
}
public get state(): IRoomState {
return this._state;
}
private async startStreams() {}
private async stopStreams() {}
}

190
src/monitorAccessory.ts Normal file
View File

@ -0,0 +1,190 @@
import {
Service,
PlatformAccessory,
CharacteristicValue,
CharacteristicSetCallback,
CharacteristicGetCallback,
} from "homebridge";
import { HomeLocationPlatform } from "./platform";
/**
* Platform Accessory
* An instance of this class is created for each accessory your platform registers
* Each accessory may expose multiple services of different service types.
*/
export class MonitorAccessory {
private service: Service;
/**
* These are just used to create a working example
* You should implement your own code to track the state of your accessory
*/
private exampleStates = {
On: false,
Brightness: 100,
};
constructor(
private readonly platform: HomeLocationPlatform,
private readonly accessory: PlatformAccessory
) {
// set accessory information
this.accessory
.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(
this.platform.Characteristic.Manufacturer,
"Default-Manufacturer"
)
.setCharacteristic(this.platform.Characteristic.Model, "Default-Model")
.setCharacteristic(
this.platform.Characteristic.SerialNumber,
"Default-Serial"
);
// get the LightBulb service if it exists, otherwise create a new LightBulb service
// you can create multiple services for each accessory
this.service =
this.accessory.getService(this.platform.Service.Lightbulb) ||
this.accessory.addService(this.platform.Service.Lightbulb);
// set the service name, this is what is displayed as the default name on the Home app
// in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method.
this.service.setCharacteristic(
this.platform.Characteristic.Name,
accessory.context.device.exampleDisplayName
);
// each service must implement at-minimum the "required characteristics" for the given service type
// see https://developers.homebridge.io/#/service/Lightbulb
// register handlers for the On/Off Characteristic
this.service
.getCharacteristic(this.platform.Characteristic.On)
.on("set", this.setOn.bind(this)) // SET - bind to the `setOn` method below
.on("get", this.getOn.bind(this)); // GET - bind to the `getOn` method below
// register handlers for the Brightness Characteristic
this.service
.getCharacteristic(this.platform.Characteristic.Brightness)
.on("set", this.setBrightness.bind(this)); // SET - bind to the 'setBrightness` method below
/**
* Creating multiple services of the same type.
*
* To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error,
* when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id:
* this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID');
*
* The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory
* can use the same sub type id.)
*/
// Example: add two "motion sensor" services to the accessory
const motionSensorOneService =
this.accessory.getService("Motion Sensor One Name") ||
this.accessory.addService(
this.platform.Service.MotionSensor,
"Motion Sensor One Name",
"YourUniqueIdentifier-1"
);
const motionSensorTwoService =
this.accessory.getService("Motion Sensor Two Name") ||
this.accessory.addService(
this.platform.Service.MotionSensor,
"Motion Sensor Two Name",
"YourUniqueIdentifier-2"
);
/**
* Updating characteristics values asynchronously.
*
* Example showing how to update the state of a Characteristic asynchronously instead
* of using the `on('get')` handlers.
* Here we change update the motion sensor trigger states on and off every 10 seconds
* the `updateCharacteristic` method.
*
*/
let motionDetected = false;
setInterval(() => {
// EXAMPLE - inverse the trigger
motionDetected = !motionDetected;
// push the new value to HomeKit
motionSensorOneService.updateCharacteristic(
this.platform.Characteristic.MotionDetected,
motionDetected
);
motionSensorTwoService.updateCharacteristic(
this.platform.Characteristic.MotionDetected,
!motionDetected
);
this.platform.log.debug(
"Triggering motionSensorOneService:",
motionDetected
);
this.platform.log.debug(
"Triggering motionSensorTwoService:",
!motionDetected
);
}, 10000);
}
/**
* Handle "SET" requests from HomeKit
* These are sent when the user changes the state of an accessory, for example, turning on a Light bulb.
*/
setOn(value: CharacteristicValue, callback: CharacteristicSetCallback) {
// implement your own code to turn your device on/off
this.exampleStates.On = value as boolean;
this.platform.log.debug("Set Characteristic On ->", value);
// you must call the callback function
callback(null);
}
/**
* Handle the "GET" requests from HomeKit
* These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on.
*
* GET requests should return as fast as possbile. A long delay here will result in
* HomeKit being unresponsive and a bad user experience in general.
*
* If your device takes time to respond you should update the status of your device
* asynchronously instead using the `updateCharacteristic` method instead.
* @example
* this.service.updateCharacteristic(this.platform.Characteristic.On, true)
*/
getOn(callback: CharacteristicGetCallback) {
// implement your own code to check if the device is on
const isOn = this.exampleStates.On;
this.platform.log.debug("Get Characteristic On ->", isOn);
// you must call the callback function
// the first argument should be null if there were no errors
// the second argument should be the value to return
callback(null, isOn);
}
/**
* Handle "SET" requests from HomeKit
* These are sent when the user changes the state of an accessory, for example, changing the Brightness
*/
setBrightness(
value: CharacteristicValue,
callback: CharacteristicSetCallback
) {
// implement your own code to set the brightness
this.exampleStates.Brightness = value as number;
this.platform.log.debug("Set Characteristic Brightness -> ", value);
// you must call the callback function
callback(null);
}
}

307
src/platform.ts Normal file
View File

@ -0,0 +1,307 @@
import {
API,
DynamicPlatformPlugin,
Logger,
PlatformAccessory,
PlatformConfig,
Service,
Characteristic,
} from "homebridge";
import { IConfig, isConfig } from "./config";
import * as faceapi from "@vladmandic/face-api";
import canvas from "canvas";
import fs, { lstatSync } from "fs";
import * as path from "path";
import { nets } from "@vladmandic/face-api";
import {
LabeledFaceDescriptors,
TNetInput,
FaceMatcher,
} from "@vladmandic/face-api";
import * as mime from "mime-types";
import { getFaceDetectorOptions } from "../src/common";
require("@tensorflow/tfjs-node");
const { Canvas, Image, ImageData } = canvas;
//@ts-ignore
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings";
import { MonitorAccessory } from "./monitorAccessory";
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class HomeLocationPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap
.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
public config: IConfig;
constructor(
public readonly log: Logger,
config: PlatformConfig,
public readonly api: API
) {
this.log.debug("Finished initializing platform:", config.name);
if (!isConfig(config)) {
this.log.error("Configuration is incorrect or incomplete");
process.exit(1);
} else {
this.config = config;
}
this.api.on("didFinishLaunching", async () => {
log.debug("Executed didFinishLaunching callback");
// run the method to discover / register your devices as accessories
await this.discoverDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
public configureAccessory(accessory: PlatformAccessory) {
this.log.info("Loading accessory from cache:", accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
public async discoverDevices() {
//Train facial recognition model
let faceMatcher: FaceMatcher;
if (this.config.trainOnStartup) {
faceMatcher = await this.trainModels();
} else {
const faceDetectionNet = nets.ssdMobilenetv1;
await faceDetectionNet.loadFromDisk(this.config.weightDirectory);
await nets.faceLandmark68Net.loadFromDisk(this.config.weightDirectory);
await nets.faceRecognitionNet.loadFromDisk(this.config.weightDirectory);
const raw = fs.readFileSync(
path.join(this.config.trainedModelDirectory, "data.json"),
"utf-8"
);
faceMatcher = FaceMatcher.fromJSON(JSON.parse(raw));
}
const exampleDevices = [
{
exampleUniqueId: "ABCD",
exampleDisplayName: "Bedroom",
},
{
exampleUniqueId: "EFGH",
exampleDisplayName: "Kitchen",
},
];
const labels = faceMatcher.labeledDescriptors.map((e) => e.label);
for (const room of this.config.rooms) {
for (const label of labels) {
const uuid = this.api.hap.uuid.generate(room.name + label);
const existingAccessory = this.accessories.find((e) => e.UUID === uuid);
if (existingAccessory) {
this.log.info(
"Restoring existing accessory from cache: ",
existingAccessory.displayName
);
// this is imported from `platformAccessory.ts`
new MonitorAccessory(this, existingAccessory);
// update accessory cache with any changes to the accessory details and information
this.api.updatePlatformAccessories([existingAccessory]);
} else {
// the accessory does not yet exist, so we need to create it
this.log.info("Adding new accessory:", `${room}+${label}`);
// create a new accessory
const accessory = new this.api.platformAccessory(
`${room}+${label}`,
uuid
);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
// accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new MonitorAccessory(this, accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
}
}
// loop over the discovered devices and register each one if it has not already been registered
for (const device of exampleDevices) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.exampleUniqueId);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(
(accessory) => accessory.UUID === uuid
);
if (existingAccessory) {
// the accessory already exists
if (device) {
this.log.info(
"Restoring existing accessory from cache:",
existingAccessory.displayName
);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new MonitorAccessory(this, existingAccessory);
// update accessory cache with any changes to the accessory details and information
this.api.updatePlatformAccessories([existingAccessory]);
} else if (!device) {
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
existingAccessory,
]);
this.log.info(
"Removing existing accessory from cache:",
existingAccessory.displayName
);
}
} else {
// the accessory does not yet exist, so we need to create it
this.log.info("Adding new accessory:", device.exampleDisplayName);
// create a new accessory
const accessory = new this.api.platformAccessory(
device.exampleDisplayName,
uuid
);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new MonitorAccessory(this, accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
}
}
private async trainModels(): Promise<FaceMatcher> {
const faceDetectionNet = faceapi.nets.ssdMobilenetv1;
await faceDetectionNet.loadFromDisk(this.config.weightDirectory);
await faceapi.nets.faceLandmark68Net.loadFromDisk(
this.config.weightDirectory
);
await faceapi.nets.faceRecognitionNet.loadFromDisk(
this.config.weightDirectory
);
const options = getFaceDetectorOptions(faceDetectionNet);
const dirs = fs.readdirSync(this.config.refImageDirectory);
const refs: Array<LabeledFaceDescriptors> = [];
for (const dir of dirs) {
if (
!lstatSync(path.join(this.config.refImageDirectory, dir)).isDirectory()
) {
continue;
}
const files = fs.readdirSync(
path.join(this.config.refImageDirectory, dir)
);
let referenceResults = await Promise.all(
files.map(async (file: string) => {
const mimeType = mime.contentType(
path.extname(path.join(this.config.refImageDirectory, dir, file))
);
if (!mimeType || !mimeType.startsWith("image")) {
return;
}
console.log(path.join(this.config.refImageDirectory, dir, file));
try {
const referenceImage = (await canvas.loadImage(
path.join(this.config.refImageDirectory, dir, file)
)) as unknown;
const descriptor = await faceapi
.detectSingleFace(referenceImage as TNetInput, options)
.withFaceLandmarks()
.withFaceDescriptor();
if (!descriptor || !descriptor.descriptor) {
throw new Error("No face found");
}
const faceDescriptors = [descriptor.descriptor];
return new faceapi.LabeledFaceDescriptors(dir, faceDescriptors);
} catch (err) {
console.log(
"An error occurred loading image at path: " +
path.join(this.config.refImageDirectory, dir, file)
);
}
return undefined;
})
);
if (referenceResults) {
refs.push(
...(referenceResults.filter((e) => e) as LabeledFaceDescriptors[])
);
}
}
const faceMatcher = new faceapi.FaceMatcher(refs);
fs.writeFile(
path.join(this.config.trainedModelDirectory, "data.json"),
JSON.stringify(faceMatcher.toJSON()),
"utf8",
(err) => {
if (err) {
console.log(`An error occurred while writing data model to file`);
}
console.log(`Successfully wrote data model to file`);
}
);
return faceMatcher;
}
}

9
src/settings.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
*/
export const PLATFORM_NAME = "HomeLocation";
/**
* This must match the name of your plugin as defined the package.json
*/
export const PLUGIN_NAME = "homebridge-home-location";