WIP adding custom event classes
This commit is contained in:
@ -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> => {
|
||||
|
@ -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
12
src/events/event.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
1
src/events/eventDelegate.ts
Normal file
1
src/events/eventDelegate.ts
Normal file
@ -0,0 +1 @@
|
||||
export type EventDelegate<T, K> = (sender: T, args: K) => void;
|
91
src/locationAccessory.ts
Normal file
91
src/locationAccessory.ts
Normal 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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -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
151
src/monitor.ts
Normal 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}`);
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user