Skip to content

Commit

Permalink
Rework MQTT and fix Homebridge 1.3 warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
Sunoo committed Feb 21, 2021
1 parent d3ec745 commit 46f70f4
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 44 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,39 @@ Other users have been sharing configurations that work for them on our GitHub si
}
```

### Camera MQTT Parameters

- `motionTopic`: The MQTT topic to watch for motion alerts.
- `motionMessage`: The message to watch for to trigger motion alerts. Will use the name of the camera if blank.
- `motionResetTopic`: The MQTT topic to watch for motion resets.
- `motionResetMessage`: The message to watch for to trigger motion resets. Will use the name of the camera if blank.
- `doorbellTopic`: The MQTT topic to watch for doorbell alerts.
- `doorbellMessage`: The message to watch for to trigger doorbell alerts. Will use the name of the camera if blank.

#### Camera MQTT Example

```json
{
"platform": "Camera-ffmpeg",
"cameras": [
{
"name": "Camera Name",
"videoConfig": {
"source": "-i rtsp://myfancy_rtsp_stream"
},
"mqtt": {
"motionTopic": "home/camera",
"motionMessage": "ON",
"motionResetTopic": "home/camera",
"motionResetMessage": "OFF",
"doorbellTopic": "home/doobell",
"doorbellMessage": "ON"
}
}
]
}
```

### Automation Parameters

- `mqtt`: Defines the hostname or IP of the MQTT broker to connect to for MQTT-based automation. If not set, MQTT support is not started. See the project site for [more information on using MQTT](https://sunoo.github.io/homebridge-camera-ffmpeg/automation/mqtt.html).
Expand Down
56 changes: 55 additions & 1 deletion config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,45 @@
"description": "Includes debugging output from the FFmpeg process used for two-way audio in the Homebridge log."
}
}
},
"mqtt": {
"title": "MQTT Configuration",
"type": "object",
"properties": {
"motionTopic": {
"title": "Motion Topic",
"type": "string",
"placeholder": "homebridge/motion",
"description": "The MQTT topic to watch for motion alerts."
},
"motionMessage": {
"title": "Motion Message",
"type": "string",
"description": "The message to watch for to trigger motion alerts. Will use the name of the camera if blank."
},
"motionResetTopic": {
"title": "Motion Reset Topic",
"type": "string",
"placeholder": "homebridge/motion/reset",
"description": "The MQTT topic to watch for motion resets."
},
"motionResetMessage": {
"title": "Motion Reset Message",
"type": "string",
"description": "The message to watch for to trigger motion resets. Will use the name of the camera if blank."
},
"doorbellTopic": {
"title": "Doorbell Topic",
"type": "string",
"placeholder": "homebridge/doorbell",
"description": "The MQTT topic to watch for doorbell alerts."
},
"doorbellMessage": {
"title": "Doorbell Message",
"type": "string",
"description": "The message to watch for to trigger doorbell alerts. Will use the name of the camera if blank."
}
}
}
}
}
Expand Down Expand Up @@ -344,7 +383,22 @@
"cameras[].doorbell",
"cameras[].switches",
"cameras[].motionTimeout",
"cameras[].motionDoorbell"
"cameras[].motionDoorbell",
{
"key": "cameras[]",
"type": "section",
"title": "MQTT Settings",
"expandable": true,
"expanded": false,
"items": [
"cameras[].mqtt.motionTopic",
"cameras[].mqtt.motionMessage",
"cameras[].mqtt.motionResetTopic",
"cameras[].mqtt.motionResetMessage",
"cameras[].mqtt.doorbellTopic",
"cameras[].mqtt.doorbellMessage"
]
}
]
}
]
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge Camera FFmpeg",
"name": "homebridge-camera-ffmpeg",
"version": "3.0.6",
"version": "3.1.0",
"description": "Homebridge Plugin Providing FFmpeg-based Camera Support",
"main": "dist/index.js",
"license": "ISC",
Expand Down
12 changes: 11 additions & 1 deletion src/configTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type CameraConfig = {
switches?: boolean;
motionTimeout?: number;
motionDoorbell?: boolean;
mqtt?: MqttCameraConfig;
unbridge?: boolean;
videoConfig?: VideoConfig;
};
Expand All @@ -49,4 +50,13 @@ export type VideoConfig = {
audio?: boolean;
debug?: boolean;
debugReturn?: boolean;
};
};

export type MqttCameraConfig = {
motionTopic?: string;
motionMessage?: string;
motionResetTopic?: string;
motionResetMessage?: string;
doorbellTopic?: string;
doorbellMessage?: string;
};
124 changes: 85 additions & 39 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from 'homebridge';
import http from 'http';
import mqtt from 'mqtt';
import url from 'url';
import { AutomationReturn } from './autoTypes';
import { CameraConfig, FfmpegPlatformConfig } from './configTypes';
import { Logger } from './logger';
Expand All @@ -26,6 +25,12 @@ let Accessory: typeof PlatformAccessory;
const PLUGIN_NAME = 'homebridge-camera-ffmpeg';
const PLATFORM_NAME = 'Camera-ffmpeg';

type MqttAction = {
accessory: PlatformAccessory;
active: boolean;
doorbell: boolean;
};

class FfmpegPlatform implements DynamicPlatformPlugin {
private readonly log: Logger;
private readonly api: API;
Expand All @@ -35,6 +40,7 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
private readonly accessories: Array<PlatformAccessory> = [];
private readonly motionTimers: Map<string, NodeJS.Timeout> = new Map();
private readonly doorbellTimers: Map<string, NodeJS.Timeout> = new Map();
private readonly mqttActions: Map<string, Map<string, Array<MqttAction>>> = new Map();

constructor(log: Logging, config: PlatformConfig, api: API) {
this.log = new Logger(log);
Expand Down Expand Up @@ -86,6 +92,14 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
api.on(APIEvent.DID_FINISH_LAUNCHING, this.didFinishLaunching.bind(this));
}

addMqttAction(topic: string, message: string, details: MqttAction): void {
const messageMap = this.mqttActions.get(topic) || new Map();
const actionArray = messageMap.get(message) || [];
actionArray.push(details);
messageMap.set(message, actionArray);
this.mqttActions.set(topic, messageMap);
}

setupAccessory(accessory: PlatformAccessory, cameraConfig: CameraConfig): void {
accessory.on(PlatformAccessoryEvent.IDENTIFY, () => {
this.log.info('Identify requested.', accessory.displayName);
Expand Down Expand Up @@ -130,7 +144,7 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
.getCharacteristic(hap.Characteristic.On)
.on(CharacteristicEventTypes.SET, (state: CharacteristicValue, callback: CharacteristicSetCallback) => {
this.doorbellHandler(accessory, state as boolean);
callback(null, state);
callback();
});
accessory.addService(doorbellTrigger);
}
Expand All @@ -144,7 +158,7 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
.getCharacteristic(hap.Characteristic.On)
.on(CharacteristicEventTypes.SET, (state: CharacteristicValue, callback: CharacteristicSetCallback) => {
this.motionHandler(accessory, state as boolean, 1);
callback(null, state);
callback();
});
accessory.addService(motionTrigger);
}
Expand All @@ -153,6 +167,31 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
const delegate = new StreamingDelegate(this.log, cameraConfig, this.api, hap, this.config.videoProcessor);

accessory.configureController(delegate.controller);

if (this.config.mqtt) {
if (cameraConfig.mqtt) {
if (cameraConfig.mqtt.motionTopic) {
this.addMqttAction(cameraConfig.mqtt.motionTopic, cameraConfig.mqtt.motionMessage || cameraConfig.name!,
{accessory: accessory, active: true, doorbell: false});
}
if (cameraConfig.mqtt.motionResetTopic) {
this.addMqttAction(cameraConfig.mqtt.motionResetTopic, cameraConfig.mqtt.motionResetMessage || cameraConfig.name!,
{accessory: accessory, active: false, doorbell: false});
}
if (cameraConfig.mqtt.doorbellTopic) {
this.addMqttAction(cameraConfig.mqtt.doorbellTopic, cameraConfig.mqtt.doorbellMessage || cameraConfig.name!,
{accessory: accessory, active: true, doorbell: true});
}
}
if (this.config.topic) {
this.addMqttAction(this.config.topic + '/motion', cameraConfig.name!,
{accessory: accessory, active: true, doorbell: false});
this.addMqttAction(this.config.topic + '/motion/reset', cameraConfig.name!,
{accessory: accessory, active: false, doorbell: false});
this.addMqttAction(this.config.topic + '/doorbell', cameraConfig.name!,
{accessory: accessory, active: true, doorbell: true});
}
}
}

configureAccessory(accessory: PlatformAccessory): void {
Expand Down Expand Up @@ -271,7 +310,7 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
}
}

private automationHandler(fullpath: string, name: string): AutomationReturn {
private httpHandler(fullpath: string, name: string): AutomationReturn {
const accessory = this.accessories.find((curAcc: PlatformAccessory) => {
return curAcc.displayName == name;
});
Expand Down Expand Up @@ -299,26 +338,54 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
}

didFinishLaunching(): void {
for (const [uuid, cameraConfig] of this.cameraConfigs) {
if (cameraConfig.unbridge) {
const accessory = new Accessory(cameraConfig.name!, uuid);
this.log.info('Configuring unbridged accessory...', accessory.displayName);
this.setupAccessory(accessory, cameraConfig);
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
this.accessories.push(accessory);
} else {
const cachedAccessory = this.cachedAccessories.find((curAcc: PlatformAccessory) => curAcc.UUID === uuid);
if (!cachedAccessory) {
const accessory = new Accessory(cameraConfig.name!, uuid);
this.log.info('Configuring bridged accessory...', accessory.displayName);
this.setupAccessory(accessory, cameraConfig);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.accessories.push(accessory);
} else {
this.accessories.push(cachedAccessory);
}
}
}

if (this.config.mqtt) {
const portmqtt = this.config.portmqtt || '1883';
let mqtttopic = 'homebridge';
if (this.config.topic && this.config.topic != 'homebridge/motion') {
mqtttopic = this.config.topic;
}
this.log.info('Setting up MQTT connection with topic ' + mqtttopic + '...');
this.log.info('Setting up MQTT connection...');
const client = mqtt.connect((this.config.tlsmqtt ? 'mqtts://' : 'mqtt://') + this.config.mqtt + ':' + portmqtt, {
'username': this.config.usermqtt,
'password': this.config.passmqtt
});
client.on('connect', () => {
this.log.info('MQTT connected.');
client.subscribe(mqtttopic + '/#');
for (const [topic] of this.mqttActions) {
this.log.debug('Subscribing to MQTT topic: ' + topic);
client.subscribe(topic);
}
});
client.on('message', (topic: string, message: Buffer) => {
if (topic.startsWith(mqtttopic)) {
const path = topic.substr(mqtttopic.length);
const name = message.toString();
this.automationHandler(path, name);
const messageMap = this.mqttActions.get(topic);
if (messageMap) {
const actionArray = messageMap.get(message.toString());
if (actionArray) {
for (const action of actionArray) {
if (action.doorbell) {
this.doorbellHandler(action.accessory, action.active);
} else {
this.motionHandler(action.accessory, action.active);
}
}
}
}
});
}
Expand All @@ -334,10 +401,10 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
message: 'Malformed URL.'
};
if (request.url) {
const parseurl = url.parse(request.url);
if (parseurl.pathname && parseurl.query) {
const name = decodeURIComponent(parseurl.query).split('=')[0];
results = this.automationHandler(parseurl.pathname, name);
const spliturl = request.url.split('?');
if (spliturl.length == 2) {
const name = decodeURIComponent(spliturl[1]).split('=')[0];
results = this.httpHandler(spliturl[0], name);
}
}
response.writeHead(results.error ? 500 : 200);
Expand All @@ -346,27 +413,6 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
});
}

for (const [uuid, cameraConfig] of this.cameraConfigs) {
if (cameraConfig.unbridge) {
const accessory = new Accessory(cameraConfig.name!, uuid);
this.log.info('Configuring unbridged accessory...', accessory.displayName);
this.setupAccessory(accessory, cameraConfig);
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
this.accessories.push(accessory);
} else {
const cachedAccessory = this.cachedAccessories.find((curAcc: PlatformAccessory) => curAcc.UUID === uuid);
if (!cachedAccessory) {
const accessory = new Accessory(cameraConfig.name!, uuid);
this.log.info('Configuring bridged accessory...', accessory.displayName);
this.setupAccessory(accessory, cameraConfig);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.accessories.push(accessory);
} else {
this.accessories.push(cachedAccessory);
}
}
}

this.cachedAccessories.forEach((accessory: PlatformAccessory) => {
const cameraConfig = this.cameraConfigs.get(accessory.UUID);
if (!cameraConfig || cameraConfig.unbridge) {
Expand Down

0 comments on commit 46f70f4

Please sign in to comment.