diff --git a/README.md b/README.md index aeb0724..011a6a5 100755 --- a/README.md +++ b/README.md @@ -5,9 +5,7 @@ [![donate](https://img.shields.io/badge/donate-paypal-green)](https://paypal.me/MrBeele?locale.x=nl_NL) This Homebridge plugin allows you to add your Unifi Protect Cameras (and their Motion Sensors) to Homekit. - -It is based on the very popular [FFmpeg Homebridge plugin](https://github.com/KhaosT/homebridge-camera-ffmpeg#readme) plugin, with Unifi-specific conveniences added to it. -It is not necessary to have that plugin installed alongside this one, though they can be installed at the same time if you have non-Unifi Protect cameras as well. +It adds smart detection by using a machine learning model to detect specific classes of objects in the camera view. # How it Works This plugin will automatically discover all the Unifi cameras from your Protect installation, and provide the following sensors for each one it finds: @@ -18,11 +16,11 @@ This plugin will automatically discover all the Unifi cameras from your Protect * A Switch, to trigger a motion event manually, forcing a rich notification * (if enabled) A switch, that acts as a doorbell trigger, to manually trigger a rich doorbell notification -# Motion Events +# Motion Events and object detection The plugin uses the Unifi Protect API to get motion events on a per camera basis. When motion has been detected one of the two methods below will be used to generate a motion notification in Homekit: -- The basic method: The "score" of the Unifi Protect motion event (which currently has a bug and is 0 as long as the motion is ongoing!) -- The advanced method: Object detection by use of a Tensorflow model. +- The basic method: The "score" of the Unifi Protect motion event. (Which currently has a bug and is 0 as long as the motion is ongoing.) +- The advanced method: Object detection by use of a Tensorflow model. (recommended) This logic/model runs on-device, and no data will be sent to any online/external/cloud source or service. It is based on the [coco ssd](https://github.com/tensorflow/tfjs-models/tree/master/coco-ssd) project. @@ -74,18 +72,17 @@ Next, open the `config.json` that contains your Homebridge configuration, and ad "id-of-camera-to-act-as-doorbell-1" ], "save_snapshot": true, - "upload_gphotos": false, "debug": false, "debug_network_traffic": false, }, + "upload_gphotos": false, "googlePhotos": { - "upload_gphotos": false, "auth_clientId": "CLIENT-ID", "auth_clientSecret": "CLIENT-SECRET", "auth_redirectUrl": "http://localhost:8888/oauth2-callback" }, + "mqtt_enabled": false, "mqtt": { - "enabled": false, "broker": "mqtt://broker-ip", "username": "MQTT-BROKER-USERNAME", "password": "MQTT-BROKER-PASSWORD", @@ -104,7 +101,9 @@ If you are using Homebridge Config X, it will do its best to alert you to any sy |-----|----|--------|-------------|-----------| |platform|string|yes|/|UnifiProtectMotion| |name|string|yes|/|Name of the plugin that shows up in the Homebridge logs| -|[unifi](https://github.com/beele/homebridge-unifi-protect-camera-motion#unifi-config-fields)|object|yes|/|Wrapper object containing the unifi configuration| +|[unifi](https://github.com/beele/homebridge-unifi-protect-camera-motion#unifi-configuration-fields)|object|yes|/|Wrapper object containing the unifi configuration| +|[upload_gphotos](https://github.com/beele/homebridge-unifi-protect-camera-motion#google-photos-configuration)|boolean|no|false|Contains a boolean indicating whether or not to upload each detection to a google photos album. When using enhanced mode the image is annotated with the class/score that was detected.| +|[mqtt_enabled](https://github.com/beele/homebridge-unifi-protect-camera-motion#mqtt-configuration)|boolean|no|false|Set this to true to enable MQTT support. Additional configuration required!| |videoProcessor|string|no|ffmpeg|Contains the path to an custom FFmpeg binary| @@ -125,7 +124,6 @@ If you are using Homebridge Config X, it will do its best to alert you to any sy |enable_motion_trigger|boolean|no|false|Contains a boolean that when set to true will enable an extra button for each camera to manually trigger a motion notification| |enable_doorbell_for|string[]|no|[]|Contains the id of the cameras for which the doorbell functionality should be enabled, all available IDs are printed during startup| |save_snapshot|boolean|no|false|Contains a boolean indicating whether or not to save each detection to a jpg file in the `.homebridge` directory. When using enhanced mode the image is annotated with the class/score that was detected.| -|upload_gphotos|boolean|no|false|Contains a boolean indicating whether or not to upload each detection to a google photos album. When using enhanced mode the image is annotated with the class/score that was detected.| |debug|boolean|no|false|Contains a boolean indicating whether or not to enable debug logging for the plugin and FFmpeg| |debug_network_traffic|boolean|no|false|Contains a boolean indication whether or not to enable logging of all network requests| @@ -133,7 +131,6 @@ If you are using Homebridge Config X, it will do its best to alert you to any sy |Field|Type|Required|Default value|Description| |-----|----|--------|-------------|-----------| -|upload_gphotos|boolean|no|false|Set this to true to enable uploading of snapshots to Google Photos| |auth_clientId|string|sometimes|/|This field is required when the `upload_gphotos` is set to true. Fill in the Client ID you generated for OAuth2 authentication| |auth_clientSecret|string|sometimes|/|This field is required when the `upload_gphotos` is set to true. Fill in the Client Secret you generated for OAuth2 authentication| |auth_redirectUrl|string|sometimes|/|Fill in 'http://localhost:8888/oauth2-callback' as a default, if you change this value to something else, also change it when creating the OAuth2 credentials! The port should always be 8888!| @@ -144,7 +141,6 @@ To enable the upload to Google Photos functionality please [read the relevant wi |Field|Type|Required|Default value|Description| |-----|----|--------|-------------|-----------| -|enabled|boolean|no|false|Set this to true to enable MQTT support| |broker|string|no|/|This field is required when the enabled field is set to true. Fill in your MQTT broker url, without the port| |username|string|no|/|This field contains the username for the MQTT broker connection, if any| |password|string|no|/|This field contains the password for the MQTT broker connection, if any| @@ -196,8 +192,6 @@ Tap on a camera preview to open the camera feed, click the settings icon and scr - Running this plugin on CPUs that do not support AVX (Celerons in NAS systems, ...) is not supported because there are no prebuilt Tensorflow binaries. Compiling Tensorflow from scratch is out of scope for this project! - Run it on a Raspberry Pi or machine with macOS / Windows / Linux (Debian based) -- ~~Previews in notifications are requested by the Home app, and can thus be "after the fact" and show an image with nothing of interest on it.~~ - - ~~The actual motion detection is done with the snapshot that is requested internally.~~ - Unifi Protect has a snapshot saved for every event, and there is an API to get these (with Width & Height), but the actual saved image is pretty low res and is scaled up to 1080p. Using the Anonymous snapshot actually gets a full resolution snapshot which is better for object detection. - There is no way to know what motion zone (from Unifi) a motion has occurred in. diff --git a/config.schema.json b/config.schema.json index 955e0eb..c4e1567 100644 --- a/config.schema.json +++ b/config.schema.json @@ -628,17 +628,17 @@ } } }, + "upload_gphotos": { + "title": "Enable upload to Google Photos?", + "type": "boolean", + "default": false, + "description": "If enabled every time a motion event is generated the snapshot used for detection will be uploaded to Google Photos", + "required": false + }, "googlePhotos": { "type": "object", "title": "Google Photos specific configuration", "properties": { - "upload_gphotos": { - "title": "Enable upload to Google Photos?", - "type": "boolean", - "default": false, - "description": "If enabled every time a motion event is generated the snapshot used for detection will be uploaded to Google Photos", - "required": true - }, "auth_clientId": { "title": "Google OAuth2 Client ID", "type": "string", @@ -662,76 +662,44 @@ } } }, - "videoConfig": { + "mqtt_enabled": { + "title": "Enable MQTT broker connection?", + "type": "boolean", + "default": false, + "description": "If enabled every time a motion event is generated a new message will be sent over MQTT", + "required": false + }, + "mqtt": { "type": "object", - "title": "FFmpeg specific configuration", + "title": "MQTT specific configuration", "properties": { - "maxStreams": { - "title": "Maximum Number of Streams", - "type": "integer", - "default": 2, - "minimum": 1, - "description": "The maximum number of streams that will be generated for this camera", - "required": true - }, - "maxWidth": { - "title": "Maximum Width", - "type": "integer", - "default": 1024, - "minimum": 1, - "description": "The maximum width reported to HomeKit", - "required": true - }, - "maxHeight": { - "title": "Maximum Height", - "type": "integer", - "default": 576, - "minimum": 1, - "description": "The maximum height reported to HomeKit", - "required": true - }, - "maxFPS": { - "title": "Maximum FPS", - "type": "integer", - "default": 15, - "minimum": 1, - "description": "The maximum frame rate of the stream", - "required": true - }, - "maxBitrate": { - "title": "Maximum Bitrate", - "type": "integer", - "default": 3000, - "minimum": 1, - "description": "The maximum bit rate of the stream", - "required": true - }, - "vcodec": { - "title": "Video Codec", + "broker": { + "title": "URL for the MQTT broker", "type": "string", - "default": "libx264", - "description": "The codec to use, use h264_omx on Raspberry Pi", + "default": "", + "description": "must include the mqtt:// protocol prefix, port is optional.", "required": true }, - "packetSize": { - "title": "Packet Size", - "type": "number", - "default": 376, - "multipleOf": 188.0 + "username": { + "title": "MQTT username", + "type": "string", + "default": "", + "description": "Enter the username to connect to the MQTT broker with, if any.", + "required": false }, - "audio": { - "title": "Enable Audio?", - "type": "boolean", - "default": false, - "description": "Only enable when your installed FFmpeg supports audio!", - "required": true + "password": { + "title": "MQTT password", + "type": "string", + "default": "", + "description": "Enter the password to connect to the MQTT broker with, if any.!", + "required": false }, - "additionalCommandline": { - "title": "Additional commandline parameters", + "topicPrefix": { + "title": "MQTT topic prefix", "type": "string", - "default": "-protocol_whitelist https,crypto,srtp,rtp,udp", - "description": "Any additional commandline parameters to pass to FFmpeg", - "required": true + "default": "", + "description": "Prefix that is added before every topic.", + "required": false } } }, diff --git a/src/motion/motion.ts b/src/motion/motion.ts index 376ad24..5a24dd0 100644 --- a/src/motion/motion.ts +++ b/src/motion/motion.ts @@ -5,12 +5,13 @@ import {Canvas, Image} from "canvas"; import {ImageUtils} from "../utils/image-utils"; import {GooglePhotos, GooglePhotosConfig} from "../utils/google-photos"; import type {API, Logging, PlatformAccessory, PlatformConfig} from 'homebridge'; -import {Mqtt, MqttConfig} from "../utils/mqtt"; +import {Mqtt} from "../utils/mqtt"; export class MotionDetector { private readonly api: API; + private readonly config: PlatformConfig; private readonly unifiConfig: UnifiConfig; private readonly googlePhotosConfig: GooglePhotosConfig; private readonly flows: UnifiFlows; @@ -26,6 +27,7 @@ export class MotionDetector { constructor(api: API, config: PlatformConfig, unifiFlows: UnifiFlows, cameras: UnifiCamera[], log: Logging) { this.api = api; + this.config = config; this.unifiConfig = config.unifi as UnifiConfig; this.googlePhotosConfig = config.googlePhotos as GooglePhotosConfig; this.flows = unifiFlows; @@ -38,8 +40,8 @@ export class MotionDetector { const userStoragePath: string = this.api.user.storagePath(); ImageUtils.userStoragePath = userStoragePath; - this.gPhotos = this.googlePhotosConfig && this.googlePhotosConfig.upload_gphotos ? new GooglePhotos(config.googlePhotos as GooglePhotosConfig, userStoragePath, log) : null; - this.mqtt = new Mqtt(config.mqtt as MqttConfig, log); + this.gPhotos = config.upload_gphotos && this.googlePhotosConfig ? new GooglePhotos(config.googlePhotos as GooglePhotosConfig, userStoragePath, log) : null; + this.mqtt = new Mqtt(config.mqtt_enabled ? config.mqtt : null , log); } public async setupMotionChecking(configuredAccessories: PlatformAccessory[]): Promise { @@ -161,11 +163,11 @@ export class MotionDetector { let annotatedImage: Canvas = await ImageUtils.generateAnnotatedImage(snapshot, detections); //Save image locally - if ((this.unifiConfig.save_snapshot || this.googlePhotosConfig.upload_gphotos) && annotatedImage) { + if ((this.unifiConfig.save_snapshot || this.config.upload_gphotos) && annotatedImage) { const fileLocation: string = await ImageUtils.saveCanvasToFile(annotatedImage); this.log.debug('The snapshot has been saved to: ' + fileLocation); - if (this.googlePhotosConfig.upload_gphotos) { + if (this.config.upload_gphotos) { const fileName: string = fileLocation.split('/').pop(); this.gPhotos diff --git a/src/unifi/unifi.ts b/src/unifi/unifi.ts index 1e0cbea..c59474c 100644 --- a/src/unifi/unifi.ts +++ b/src/unifi/unifi.ts @@ -306,7 +306,6 @@ export interface UnifiConfig { enable_motion_trigger: boolean; enable_doorbell_for: string[]; save_snapshot: boolean; - upload_gphotos: boolean; debug: boolean; debug_network_traffic: boolean; } diff --git a/src/utils/google-photos.ts b/src/utils/google-photos.ts index f57d8ab..740e083 100644 --- a/src/utils/google-photos.ts +++ b/src/utils/google-photos.ts @@ -161,7 +161,6 @@ export class GooglePhotos { } export interface GooglePhotosConfig { - upload_gphotos: boolean; auth_clientId: string; auth_clientSecret: string; auth_redirectUrl: string; @@ -170,4 +169,4 @@ export interface GooglePhotosConfig { export interface GooglePhotosPersistData { auth_refresh_token: string; albumId: string; -} \ No newline at end of file +} diff --git a/src/utils/mqtt.ts b/src/utils/mqtt.ts index 6e586b5..8bdbde3 100644 --- a/src/utils/mqtt.ts +++ b/src/utils/mqtt.ts @@ -13,11 +13,20 @@ export class Mqtt { this.log = log; this.config = config; - if (!config || !config.enabled) { + if (!config || !config.broker) { return; } - this.client = mqtt.connect(config.broker, {username: config.username, password: config.password}); + const options: {username?: string, password?: string} = {}; + if (config.username) { + options.username = config.username; + + if (config.password) { + options.password = config.password; + } + } + + this.client = mqtt.connect(config.broker, options); this.client.on('connect', () => { this.log.debug('Connected to MQTT broker'); }); @@ -34,9 +43,8 @@ export class Mqtt { } export interface MqttConfig { - enabled: boolean; broker: string; username: string; password: string; topicPrefix: string; -} \ No newline at end of file +}