Wip
This commit is contained in:
27
src/config.ts
Normal file
27
src/config.ts
Normal 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"])
|
||||
);
|
||||
};
|
12
src/index.ts
12
src/index.ts
@ -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
39
src/monitor.ts
Normal 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
190
src/monitorAccessory.ts
Normal 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
307
src/platform.ts
Normal 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
9
src/settings.ts
Normal 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";
|
Reference in New Issue
Block a user