WIP adding custom event classes

This commit is contained in:
watsonb8
2020-12-10 12:51:35 -05:00
parent e047ef6549
commit 65f11bec09
10 changed files with 435 additions and 314 deletions

View File

@ -15,13 +15,12 @@ export const getFaceDetectorOptions = (net: faceapi.NeuralNetwork<any>) => {
: new faceapi.TinyFaceDetectorOptions({ inputSize, scoreThreshold });
};
export function saveFile(fileName: string, buf: Buffer) {
const baseDir = process.env.OUT_DIR as string;
if (!fs.existsSync(baseDir)) {
fs.mkdirSync(baseDir);
export function saveFile(basePath: string, fileName: string, buf: Buffer) {
if (!fs.existsSync(basePath)) {
fs.mkdirSync(basePath);
}
fs.writeFileSync(path.resolve(baseDir, fileName), buf, "base64");
fs.writeFileSync(path.resolve(basePath, fileName), buf, "base64");
}
export const delay = (ms: number): Promise<void> => {

View File

@ -6,6 +6,7 @@ export interface IConfig extends PlatformConfig {
weightDirectory: string;
trainOnStartup: boolean;
rooms: Array<IRoom>;
detectionTimeout: number;
}
export interface IRoom {

12
src/events/event.ts Normal file
View File

@ -0,0 +1,12 @@
import { EventDelegate } from "./eventDelegate";
export class Event<T, K> extends Array<EventDelegate<T, K>> {
constructor() {
super();
}
public fire = (source: T, args: K) => {
for (const delegate of this) {
delegate(source, args);
}
};
}

View File

@ -0,0 +1 @@
export type EventDelegate<T, K> = (sender: T, args: K) => void;

91
src/locationAccessory.ts Normal file
View File

@ -0,0 +1,91 @@
import {
Service,
CharacteristicGetCallback,
PlatformAccessory,
} from "homebridge";
import { Monitor, IStateChangeEventArgs } from "./monitor";
import { HomeLocationPlatform } from "./platform";
import { IRoom } from "./config";
/**
* 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 LocationAccessory {
private _services: Array<Service>;
constructor(
private readonly _platform: HomeLocationPlatform,
private readonly _accessory: PlatformAccessory,
private _monitor: Monitor,
private _room: IRoom
) {
this._services = [];
// set accessory information
this._accessory
.getService(this._platform.Service.AccessoryInformation)!
.setCharacteristic(
this._platform.Characteristic.Manufacturer,
"Brandon Watson"
)
.setCharacteristic(
this._platform.Characteristic.Model,
"Person Location Sensor"
)
.setCharacteristic(
this._platform.Characteristic.SerialNumber,
"123-456-789"
);
//Init motion services
for (const label of this._monitor.labels) {
const newService =
this._accessory.getService(label) ||
this._accessory.addService(
this._platform.Service.MotionSensor,
label,
this._room + label
);
newService
.getCharacteristic(this._platform.Characteristic.MotionDetected)
.on("get", (callback: CharacteristicGetCallback) =>
this.onMotionDetectedGet(label, callback)
);
this._services.push(newService);
}
//Register monitor state change events
this._monitor.stateChangedEvent.push(this.onMonitorStateChange.bind(this));
}
private onMotionDetectedGet = (
label: string,
callback: CharacteristicGetCallback
) => {
this._platform.log.debug("Triggered GET MotionDetected");
// set this to a valid value for MotionDetected
const currentValue =
this._monitor.getState(label) === this._room.name ? 1 : 0;
callback(null, currentValue);
};
private onMonitorStateChange = (
sender: Monitor,
args: IStateChangeEventArgs
) => {
const service = this._services.find(
(service) => service.displayName == args.label
);
if (service) {
service.setCharacteristic(
this._platform.Characteristic.MotionDetected,
args.new === this._room.name
);
}
};
}

View File

@ -1,39 +0,0 @@
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() {}
}

151
src/monitor.ts Normal file
View File

@ -0,0 +1,151 @@
import { FaceMatcher } from "@vladmandic/face-api";
import { IRoom } from "./config";
import { Rtsp } from "rtsp-stream/lib";
import canvas from "canvas";
import * as faceapi from "@vladmandic/face-api";
import { getFaceDetectorOptions, saveFile } from "./common";
import { nets } from "@vladmandic/face-api";
import { Logger } from "homebridge";
import { Event } from "./events/event";
const { Canvas, Image, ImageData } = canvas;
export type MonitorState = { [label: string]: string | null };
export interface IStateChangeEventArgs {
label: string;
old: string | null;
new: string;
}
export class Monitor {
private _state: MonitorState = {};
private _streamsByRoom: { [roomName: string]: Array<Rtsp> } = {};
private _faceDetectionNet = nets.ssdMobilenetv1;
private _stateChangedEvent: Event<this, IStateChangeEventArgs>;
constructor(
private _rooms: Array<IRoom>,
private _matcher: FaceMatcher,
private _logger: Logger
) {
this._stateChangedEvent = new Event();
//Initialize state
for (const room of this._rooms) {
this._streamsByRoom[room.name] = [
...room.rtspConnectionStrings.map((connectionString) => {
return new Rtsp(connectionString, {
rate: 0.5,
image: true,
})
.on("data", async (data: Buffer) => this.onData(room.name, data))
.on("error", async (error: string) =>
this.onError(error, connectionString)
);
}),
];
_matcher.labeledDescriptors.forEach((descriptor) => {
this._state[descriptor.label] = null;
});
}
}
/**
* @method getState
*
* @param label The name of the label to retrieve state for
*
* The last known room of the requested label
*/
public getState(label: string): string | null {
return this._state[label];
}
/**
* @property labels
*
* Gets the list of labels associated with the monitor
*/
public get labels(): Array<string> {
return this._matcher.labeledDescriptors
.map((descriptor) => descriptor.label)
.filter(
(label: string, index: number, array: Array<string>) =>
array.indexOf(label) === index
);
}
public get stateChangedEvent(): Event<this, IStateChangeEventArgs> {
return this._stateChangedEvent;
}
/**
* @method startStreams
*
* Starts monitoring rtsp streams
*/
public startStreams() {
for (const key in this._streamsByRoom) {
for (const stream of this._streamsByRoom[key]) {
stream.start();
}
}
}
/**
* @method closeStreams
*
* Stops monitoring rtsp streams
*/
public closeStreams() {
for (const key in this._streamsByRoom) {
for (const stream of this._streamsByRoom[key]) {
stream.close();
}
}
}
private onData = async (room: string, data: Buffer) => {
const input = ((await canvas.loadImage(data)) as unknown) as ImageData;
const out = faceapi.createCanvasFromMedia(input);
const resultsQuery = await faceapi
.detectAllFaces(out, getFaceDetectorOptions(this._faceDetectionNet))
.withFaceLandmarks()
.withFaceDescriptors();
switch (room) {
case "Kitchen": {
saveFile(
"/Users/brandonwatson/Documents/Git/Gitea/homebridge-face-location/out",
"Kitchen.jpg",
data
);
break;
}
case "LivingRoom": {
saveFile(
"/Users/brandonwatson/Documents/Git/Gitea/homebridge-face-location/out",
"LivingRoom.jpg",
data
);
break;
}
}
for (const res of resultsQuery) {
const bestMatch = this._matcher.matchDescriptor(res.descriptor);
const old = this._state[bestMatch.label];
this._state[bestMatch.label] = room;
this._stateChangedEvent.fire(this, {
old: old,
new: room,
label: bestMatch.label,
});
this._logger.info(`Face Detected: ${bestMatch.label} in room ${room}`);
}
};
private onError = async (error: string, streamName: string) => {
this._logger.info(`[${streamName}] ${error}`);
};
}

View File

@ -1,191 +0,0 @@
import {
Service,
PlatformAccessory,
CharacteristicValue,
CharacteristicSetCallback,
CharacteristicGetCallback,
} from "homebridge";
import { LocationMonitor } from "./locationMonitor";
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,
private monitor: LocationMonitor
) {
// 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 MotionSensor service if it exists, otherwise create a new MotionSensor service
// you can create multiple services for each accessory
this.service =
this.accessory.getService(this.platform.Service.MotionSensor) ||
this.accessory.addService(this.platform.Service.MotionSensor);
// 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["DeviceName"]
);
// 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);
}
}

View File

@ -19,7 +19,7 @@ import {
FaceMatcher,
} from "@vladmandic/face-api";
import * as mime from "mime-types";
import { LocationMonitor } from "./locationMonitor";
import { Monitor } from "./monitor";
import { getFaceDetectorOptions } from "./common";
require("@tensorflow/tfjs-node");
@ -28,7 +28,7 @@ const { Canvas, Image, ImageData } = canvas;
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings";
import { MonitorAccessory } from "./monitorAccessory";
import { LocationAccessory } from "./locationAccessory";
/**
* HomebridgePlatform
@ -100,43 +100,44 @@ export class HomeLocationPlatform implements DynamicPlatformPlugin {
faceMatcher = FaceMatcher.fromJSON(JSON.parse(raw));
}
const locationMonitor = new LocationMonitor(this.config.rooms, faceMatcher);
const locationMonitor = new Monitor(
this.config.rooms,
faceMatcher,
this.log
);
locationMonitor.startStreams();
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 uuid = this.api.hap.uuid.generate(room.name);
const existingAccessory = this.accessories.find((e) => e.UUID === uuid);
if (existingAccessory) {
this.log.info(
"Restoring existing accessory from cache: ",
existingAccessory.displayName
);
const existingAccessory = this.accessories.find((e) => e.UUID === uuid);
if (existingAccessory) {
this.log.info(
"Restoring existing accessory from cache: ",
existingAccessory.displayName
);
new MonitorAccessory(this, existingAccessory, locationMonitor);
new LocationAccessory(this, existingAccessory, locationMonitor, room);
this.api.updatePlatformAccessories([existingAccessory]);
} else {
this.log.info("Adding new accessory:", `${room.name}+${label}`);
this.api.updatePlatformAccessories([existingAccessory]);
} else {
this.log.info("Adding new accessory:", `${room.name}`);
// create a new accessory
const accessory = new this.api.platformAccessory(
`${room.name} ${label}`,
uuid
);
// create a new accessory
const accessory = new this.api.platformAccessory(`${room.name}`, uuid);
accessory.context["DeviceName"] = `${room.name} ${label}`;
accessory.context["DeviceName"] = `${room.name}`;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new MonitorAccessory(this, accessory, locationMonitor);
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new LocationAccessory(this, accessory, locationMonitor, room);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
}
}