From 42f80fed1dc140b95d2196ec6aafeb08039fd326 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 13 Dec 2021 18:07:30 -0800 Subject: [PATCH] homekit: fix missing opencv motion detector --- plugins/homekit/package-lock.json | 4 +- plugins/homekit/package.json | 2 +- plugins/homekit/src/camera-mixin.ts | 51 ++++++------ server/src/plugin/plugin-http.ts | 117 ++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 server/src/plugin/plugin-http.ts diff --git a/plugins/homekit/package-lock.json b/plugins/homekit/package-lock.json index 4f7d91691..7cfaa49c7 100644 --- a/plugins/homekit/package-lock.json +++ b/plugins/homekit/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/homekit", - "version": "0.0.134", + "version": "0.0.135", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/homekit", - "version": "0.0.134", + "version": "0.0.135", "dependencies": { "hap-nodejs": "file:../../external/HAP-NodeJS", "lodash": "^4.17.21", diff --git a/plugins/homekit/package.json b/plugins/homekit/package.json index 4a5d84299..22dca1325 100644 --- a/plugins/homekit/package.json +++ b/plugins/homekit/package.json @@ -40,5 +40,5 @@ "@types/qrcode": "^1.4.1", "@types/url-parse": "^1.4.3" }, - "version": "0.0.134" + "version": "0.0.135" } diff --git a/plugins/homekit/src/camera-mixin.ts b/plugins/homekit/src/camera-mixin.ts index 5e2d24518..4fc893d65 100644 --- a/plugins/homekit/src/camera-mixin.ts +++ b/plugins/homekit/src/camera-mixin.ts @@ -143,7 +143,7 @@ export class CameraMixin extends SettingsMixinDeviceBase implements Setting type: 'device', deviceFilter: 'interfaces.includes("MotionSensor")', value: this.storage.getItem('linkedMotionSensor') || null, - placeholder: this.providedInterfaces.includes(ScryptedInterface.MotionSensor) + placeholder: this.interfaces.includes(ScryptedInterface.MotionSensor) ? 'Built-In Motion Sensor' : 'None', description: "Link motion sensor used to trigger HomeKit Secure Video recordings.", }) @@ -161,34 +161,33 @@ export class CameraMixin extends SettingsMixinDeviceBase implements Setting if (this.interfaces.includes(ScryptedInterface.ObjectDetector)) { try { const types = await realDevice.getObjectTypes(); - const choices = types.people?.map(p => `Person: ${p.label}`) || []; - if (types.classes) - choices.push(...types.classes); + if (types.classes?.length) { + const value: string[] = []; + try { + value.push(...JSON.parse(this.storage.getItem('objectDetectionContactSensors'))); + } + catch (e) { + } - const value: string[] = []; - try { - value.push(...JSON.parse(this.storage.getItem('objectDetectionContactSensors'))); - } - catch (e) { + settings.push({ + title: 'Object Detection Contact Sensors', + type: 'string', + choices: types.classes, + multiple: true, + key: 'objectDetectionContactSensors', + description: 'Create HomeKit contact sensors that detect specific people or objects.', + value, + }); + + settings.push({ + title: 'Object Detection Contact Sensor Timeout', + type: 'number', + key: 'objectDetectionContactSensorTimeout', + description: 'Duration in seconds to keep the contact sensor open.', + value: this.storage.getItem('objectDetectionContactSensorTimeout') || defaultObjectDetectionContactSensorTimeout, + }); } - settings.push({ - title: 'Object Detection Contact Sensors', - type: 'string', - choices, - multiple: true, - key: 'objectDetectionContactSensors', - description: 'Create HomeKit contact sensors that detect specific people or objects.', - value, - }); - - settings.push({ - title: 'Object Detection Contact Sensor Timeout', - type: 'number', - key: 'objectDetectionContactSensorTimeout', - description: 'Duration in seconds to keep the contact sensor open.', - value: this.storage.getItem('objectDetectionContactSensorTimeout') || defaultObjectDetectionContactSensorTimeout, - }); } catch (e) { } diff --git a/server/src/plugin/plugin-http.ts b/server/src/plugin/plugin-http.ts new file mode 100644 index 000000000..d1a4f2a5d --- /dev/null +++ b/server/src/plugin/plugin-http.ts @@ -0,0 +1,117 @@ +import { Request, Response, Router } from 'express'; +import bodyParser from 'body-parser'; +import { HttpRequest } from '@scrypted/sdk/types'; +import WebSocket, { Server as WebSocketServer } from "ws"; +import { ServerResponse } from 'http'; + +export abstract class PluginHttp { + wss = new WebSocketServer({ noServer: true }); + + constructor(public app: Router) { + app.all(['/endpoint/@:owner/:pkg/public/engine.io/*', '/endpoint/:pkg/public/engine.io/*'], (req, res) => { + this.endpointHandler(req, res, true, true, this.handleEngineIOEndpoint.bind(this)) + }); + + app.all(['/endpoint/@:owner/:pkg/engine.io/*', '/endpoint/@:owner/:pkg/engine.io/*'], (req, res) => { + this.endpointHandler(req, res, false, true, this.handleEngineIOEndpoint.bind(this)) + }); + + // stringify all http endpoints + app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], bodyParser.text() as any); + + app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg/public', '/endpoint/:pkg/public/*'], (req, res) => { + this.endpointHandler(req, res, true, false, this.handleRequestEndpoint.bind(this)) + }); + + app.all(['/endpoint/@:owner/:pkg', '/endpoint/@:owner/:pkg/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], (req, res) => { + this.endpointHandler(req, res, false, false, this.handleRequestEndpoint.bind(this)) + }); + } + + abstract handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: T): Promise; + abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): Promise; + abstract getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise; + abstract handleWebSocket(endpoint: string, httpRequest: HttpRequest, ws: WebSocket, pluginData: T): Promise; + + async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean, + handler: (req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T) => void) { + + const isUpgrade = req.headers.connection?.toLowerCase() === 'upgrade'; + + const end = (code: number, message: string) => { + if (isUpgrade) { + const socket = res.socket; + socket.write(`HTTP/1.1 ${code} ${message}\r\n` + + '\r\n'); + socket.destroy(); + } + else { + res.status(code); + res.send(message); + } + }; + + if (!isPublicEndpoint && !res.locals.username) { + end(401, 'Not Authorized'); + return; + } + + const { owner, pkg } = req.params; + let endpoint = pkg; + if (owner) + endpoint = `@${owner}/${endpoint}`; + + if (isUpgrade && req.headers.upgrade?.toLowerCase() !== 'websocket') { + end(404, 'Not Found'); + return; + } + + const pluginData = await this.getEndpointPluginData(endpoint, isUpgrade, isEngineIOEndpoint); + if (!pluginData) { + end(404, 'Not Found'); + return; + } + + let rootPath = `/endpoint/${endpoint}`; + if (isPublicEndpoint) + rootPath += '/public' + + const body = req.body && typeof req.body !== 'string' ? JSON.stringify(req.body) : req.body; + + const httpRequest: HttpRequest = { + body, + headers: req.headers, + method: req.method, + rootPath, + url: req.url, + isPublicEndpoint, + username: res.locals.username, + }; + + if (isEngineIOEndpoint && !isUpgrade && isPublicEndpoint) { + res.header("Access-Control-Allow-Origin", '*'); + } + + if (!isEngineIOEndpoint && isUpgrade) { + this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, async (ws) => { + try { + await this.handleWebSocket(endpoint, httpRequest, ws, pluginData); + } + catch (e) { + console.error('websocket plugin error', e); + ws.close(); + } + }); + } + else { + try { + handler(req, res, httpRequest, pluginData); + } + catch (e) { + res.status(500); + res.send(e.toString()); + console.error(e); + } + } + } +}