Skip to content

Commit

Permalink
add doorbell support #97
Browse files Browse the repository at this point in the history
  • Loading branch information
andrei-tatar committed Apr 29, 2022
1 parent dad1ad2 commit f8bf9ea
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 20 deletions.
18 changes: 18 additions & 0 deletions doc/nodes/doorbell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Fan

Represents a [Google Home Doorbell](https://developers.google.com/assistant/smarthome/guides/doorbell) device.

Node attributes:
- [Common](../common.md)

Input payload is an object with the following properties:
- `online` - boolean: true/false, default: true. Flag that indicates if a device is online.
- `named` - List of objects recognized by the user that have been tagged with a label.
- `familiar` - number: Count of objects recognized by the user that have no label.
- `unfamiliar` - number: Count of objects detected by the device that the user may not recognize.
- `unclassified` - number: Count of objects detected that the device was unable to classify.

Example flow:
```
[{"id":"bbbdecb1cda7db5e","type":"noraf-doorbell","z":"244894166ed790d5","devicename":"Doorbell","roomhint":"","name":"","nora":"c38ae3d9.b9765","topic":"","filter":false,"x":880,"y":480,"wires":[]},{"id":"7dc0804a1cdeba54","type":"inject","z":"244894166ed790d5","name":"\"Alice\" is at the doorbell","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"named\":[\"Alice\"]}","payloadType":"json","x":560,"y":440,"wires":[["bbbdecb1cda7db5e"]]},{"id":"1b08bca9fd2551e9","type":"inject","z":"244894166ed790d5","name":"someone's at the doorbell (unclassified)","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"unclassified\":1}","payloadType":"json","x":610,"y":520,"wires":[["bbbdecb1cda7db5e"]]},{"id":"71629549d9fd9dc9","type":"inject","z":"244894166ed790d5","name":"someone you know is at the doorbell","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"familiar\":1}","payloadType":"json","x":600,"y":480,"wires":[["bbbdecb1cda7db5e"]]},{"id":"aeeedb84a9e88d39","type":"inject","z":"244894166ed790d5","name":"someone's at the doorbell (unfamiliar)","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"unfamiliar\":1}","payloadType":"json","x":610,"y":560,"wires":[["bbbdecb1cda7db5e"]]},{"id":"c38ae3d9.b9765","type":"noraf-config","name":"Firebase [test group]","group":"test","twofactor":"off","twofactorpin":"","localexecution":true,"structure":"","storeStateInContext":true,"disableValidationErrors":false}]
```
30 changes: 15 additions & 15 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"node": ">=12.19"
},
"dependencies": {
"@andrei-tatar/nora-firebase-common": "^1.9.1",
"@andrei-tatar/nora-firebase-common": "^1.10.1",
"cbor": "^8.0.2",
"firebase": "^9.6.5",
"node-fetch": "^2.6.7",
Expand Down Expand Up @@ -52,6 +52,7 @@
"noraf-camera": "build/nodes/nora-camera.js",
"noraf-charger": "build/nodes/nora-charger.js",
"noraf-config": "build/nodes/nora-config.js",
"noraf-doorbell": "build/nodes/nora-doorbell.js",
"noraf-fan": "build/nodes/nora-fan.js",
"noraf-garage": "build/nodes/nora-garage.js",
"noraf-light": "build/nodes/nora-light.js",
Expand Down
68 changes: 68 additions & 0 deletions src/nodes/nora-doorbell.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script type="text/javascript">
RED.nodes.registerType('noraf-doorbell', {
category: 'nora',
color: 'rgb(235, 227, 141)',
icon: 'assistant.png',
defaults: {
devicename: {
value: 'Doorbell',
required: true,
},
roomhint: {
value: ''
},
name: {
value: ''
},
nora: {
type: 'noraf-config',
required: true
},
topic: {
value: ''
},
filter: {
value: false,
},
},
inputs: 1,
outputs: 0,
paletteLabel: 'doorbell',
label: function () {
return this.name || this.devicename || 'doorbell';
},
});
</script>

<script type="text/x-red" data-template-name="noraf-doorbell">
<div class="form-row">
<label for="node-input-nora"><i class="fa fa-table"></i> Config</label>
<input type="text" id="node-input-nora">
</div>
<div class="form-row">
<label for="node-input-devicename"><i class="fa fa-i-cursor"></i> Doorbell</label>
<input type="text" id="node-input-devicename">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-filter"><i class="fa fa-filter"></i> Ignore input messages that don't match the <code>topic</code> value: </label>
<input type="checkbox" id="node-input-filter" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-roomhint"><i class="fa fa-i-cursor"></i> Room Hint</label>
<input type="text" id="node-input-roomhint">
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left:25px; margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>

<script type="text/x-red" data-help-name="noraf-doorbell">
<p>
<a href="https://github.com/andrei-tatar/node-red-contrib-smartnora/blob/master/doc/nodes/doorbell/README.md">https://github.com/andrei-tatar/node-red-contrib-smartnora/blob/master/doc/nodes/doorbell/README.md</a>
</p>
</script>
68 changes: 68 additions & 0 deletions src/nodes/nora-doorbell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ObjectDetectionDevice, ObjectDetectionNotification, validateIndividual } from '@andrei-tatar/nora-firebase-common';
import { firstValueFrom } from 'rxjs';
import { ConfigNode, NodeInterface } from '..';
import { registerNoraDevice } from './util';

module.exports = function (RED: any) {
RED.nodes.registerType('noraf-doorbell', function (this: NodeInterface, config: any) {
RED.nodes.createNode(this, config);

const noraConfig: ConfigNode = RED.nodes.getNode(config.nora);
if (!noraConfig?.valid) {
return;
}

registerNoraDevice<ObjectDetectionDevice>(this, RED, config, {
deviceConfig: {
type: 'action.devices.types.DOORBELL',
traits: ['action.devices.traits.ObjectDetection'],
name: {
name: config.devicename,
},
roomHint: config.roomhint,
willReportState: true,
notificationSupportedByAgent: true,
state: {
online: true,
},
attributes: {
},
noraSpecific: {
},
},
handleNodeInput: async ({ msg, updateState, device$ }) => {
const { online, named, familiar, unfamiliar, unclassified } = msg.payload;
if (typeof online !== 'undefined') {
await updateState({ online });
}

const objects = { named, familiar, unfamiliar, unclassified };
for (const key of Object.keys(objects) as (keyof typeof objects)[]) {
if (objects[key] === void 0) {
delete objects[key];
}
}

if (Object.entries(objects).length) {
const notification: ObjectDetectionNotification = {
// eslint-disable-next-line @typescript-eslint/naming-convention
ObjectDetection: {
priority: 0,
detectionTimestamp: new Date().getTime(),
objects,
}
};

const result = validateIndividual('object-detection-notification', notification);
if (!result.valid) {
throw new Error(`Invalid notification object ${result.errors}`);
}

const device = await firstValueFrom(device$);
await device.sendNotification(notification);
}
},
});
});
};

2 changes: 2 additions & 0 deletions src/nodes/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function registerNoraDevice<T extends Device>(node: NodeInterface, RED: a
handleNodeInput?: (opts: {
msg: NodeMessage;
updateState: FirebaseDevice<T>['updateState'];
device$: Observable<FirebaseDevice<T>>;
state$: Observable<T['state']>;
}) => Promise<void> | void;
customRegistration?: (device$: Observable<FirebaseDevice<T>>) => Observable<any>;
Expand Down Expand Up @@ -158,6 +159,7 @@ export function registerNoraDevice<T extends Device>(node: NodeInterface, RED: a
const device = await firstValueFrom(device$);
return await device.updateState(...args);
},
device$,
state$: device$.pipe(switchMap(d => d.state$)),
}),
});
Expand Down
5 changes: 5 additions & 0 deletions src/nora/device.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as common from '@andrei-tatar/nora-firebase-common';
import { ObjectDetectionNotification } from '@andrei-tatar/nora-firebase-common';
import { child, onValue } from 'firebase/database';
import { firstValueFrom, merge, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
Expand Down Expand Up @@ -91,6 +92,10 @@ export class FirebaseDevice<T extends common.Device = common.Device> {
return this.updateStateInternal(update, { mapping });
}

async sendNotification(notification: ObjectDetectionNotification) {
await this.sync.sendGoogleHomeNotification(this.device.id, notification);
}

async executeCommand(command: string, params: any): Promise<T['state']> {
this.local$.next(true);

Expand Down
33 changes: 29 additions & 4 deletions src/nora/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ export class FirebaseSync {
}
}

async sendGoogleHomeNotification(deviceId: string, notification: common.ObjectDetectionNotification) {
if (await this.hasDeviceTokens()) {
await this.queueJob({
type: 'notify-home',
deviceId,
notification,
});
}
}

watchForActions(identifier: string): Observable<string> {
const actionRef = ref(this.db, `user/${this.uid}/actions/${identifier}`);
return new Observable<string>(observer =>
Expand Down Expand Up @@ -208,9 +218,6 @@ export class FirebaseSync {
private mergeJob(current: JobInQueue, previous: JobInQueue): JobInQueue {
switch (current.job.type) {
case 'sync':
if (previous.job.type !== 'sync') {
throw new Error('can\'t merge jobs with different types');
}
previous.resolve();
return current;

Expand All @@ -235,6 +242,10 @@ export class FirebaseSync {
case 'notify':
previous.reject(new Error('too many notifications per sec'));
return current;

case 'notify-home':
previous.resolve();
return current;
}
}

Expand Down Expand Up @@ -262,6 +273,14 @@ export class FirebaseSync {
body: job.notification,
});
break;

case 'notify-home':
await this.doHttpCall({
path: 'home-notify',
body: job.notification,
query: `id=${encodeURIComponent(job.deviceId)}`,
});
break;
}

resolve();
Expand Down Expand Up @@ -356,7 +375,13 @@ interface SendNotificationJob {
notification: common.WebpushNotification;
}

type Job = SyncJob | ReportStateJob | SendNotificationJob;
interface SendHomeNotificationJob {
type: 'notify-home';
notification: common.ObjectDetectionNotification;
deviceId: string;
}

type Job = SyncJob | ReportStateJob | SendNotificationJob | SendHomeNotificationJob;

interface JobInQueue {
job: Job;
Expand Down

0 comments on commit f8bf9ea

Please sign in to comment.