Skip to content

Commit

Permalink
feat: Locate Command & Better SmartHomeController types
Browse files Browse the repository at this point in the history
  • Loading branch information
EntraptaJ committed Jun 11, 2020
1 parent b1f5cb7 commit b06a809
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 23 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@typescript-eslint/parser": "^3.0.2",
"actions-on-google": "^2.12.0",
"actions-on-google-testing": "^0.4.0",
"dotenv": "^8.2.0",
"eslint": "^7.2.0",
"eslint-config-prettier": "6.11.0",
"eslint-config-standard": "14.1.1",
Expand Down
3 changes: 3 additions & 0 deletions src/Lab/Devices/CoreLight/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export class CoreLight extends BaseDevice<CoreLight['traits']> {
break;
case CommandType.appSelect:
console.log('Select application: ', command);
break;
}

return this.getStatus();
};
}
15 changes: 15 additions & 0 deletions src/Lab/Devices/Jumper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export class Jumper extends BaseDevice<Jumper['traits']> {
swVersion: '0.0.1',
};

public willReportState = true;

public name: BaseDevice<Jumper['traits']>['name'] = {
name: 'Jumper',
defaultNames: ['Jumper'],
Expand Down Expand Up @@ -108,6 +110,7 @@ export class Jumper extends BaseDevice<Jumper['traits']> {

public startTimer(timerTimeSec: number): void {
this.customData.timerRemainingSec = timerTimeSec;
this.customData.timerPaused = false;

this.startInterval();
}
Expand All @@ -122,6 +125,8 @@ export class Jumper extends BaseDevice<Jumper['traits']> {
}

public pauseTimer(): void {
console.log('pause timer: ', this.customData);

if (!this.customData.timer) {
return;
}
Expand All @@ -144,11 +149,14 @@ export class Jumper extends BaseDevice<Jumper['traits']> {
>['executeCommand'] = async (command) => {
switch (command.type) {
case CommandType.TimerStart:
console.log('Start Timer: ', command);
this.startTimer(command.timerTimeSec);
break;
case CommandType.TimerCancel:
this.stopTimer();
break;
case CommandType.TimerPause:
console.log('pause timer: ');
this.pauseTimer();
break;
case CommandType.TimerResume:
Expand All @@ -163,6 +171,13 @@ export class Jumper extends BaseDevice<Jumper['traits']> {
case CommandType.OnOff:
this.customData.on = command.on;
break;
// case CommandType.Locate:
// console.log('Locate Shit', command);
// break;
}

const states = await this.getStatus();

return states;
};
}
6 changes: 6 additions & 0 deletions src/Lab/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import bodyParser from 'body-parser';
import { registerAuthEndpoints } from './Auth';
import { Jumper } from './Devices/Jumper';

if (process.env.NODE_ENV !== 'production') {
const { config } = await import('dotenv');

config();
}

const webServer = express();
webServer.use(bodyParser.json());
webServer.use(bodyParser.urlencoded({ extended: true }));
Expand Down
5 changes: 5 additions & 0 deletions src/Modules/Command/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export enum CommandType {
* https://developers.google.com/assistant/smarthome/traits/modes#device-commands
*/
SetMode = 'action.devices.commands.SetModes',

/**
* https://developers.google.com/assistant/smarthome/traits/locator#device-commands
*/
Locate = 'action.devices.commands.Locate',
}

export abstract class BaseComamnd {
Expand Down
4 changes: 3 additions & 1 deletion src/Modules/Command/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TimerResumeCommand } from './Commands/TimerResumeCommand';
import { TimerCancelCommand } from './Commands/TimerCancelCommand';
import { SetModesCommand } from './Commands/SetModesCommand';
import { TimerAdjustCommand } from './Commands/TimerAdjustCommand';
import { LocateCommand } from './Commands/LocateCommand';

export type Commands =
| OnOffCommand
Expand All @@ -20,4 +21,5 @@ export type Commands =
| TimerResumeCommand
| TimerCancelCommand
| TimerAdjustCommand
| SetModesCommand;
| SetModesCommand
| LocateCommand;
22 changes: 22 additions & 0 deletions src/Modules/Command/Commands/LocateCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// src/Modules/Command/Commands/TimerStartCommand.ts
import { BaseComamnd, CommandType } from '../BaseCommand';

/**
* Locate the Device
*
* https://developers.google.com/assistant/smarthome/traits/locator#device-commands
*/
export class LocateCommand extends BaseComamnd {
public type = CommandType.Locate as const;

/**
* For use on devices that make an audible response on Locate and report information. If set to true, should silence an already in-progress alarm if one is occurring.
*/
public silence: boolean;

/**
* Default is "en". Current language of query/display,
* for return of localized location strings if needed
*/
public lang: string;
}
6 changes: 5 additions & 1 deletion src/Modules/Device/BaseDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,9 @@ export abstract class BaseDevice<T extends ReadonlyArray<Traits>> {

public abstract executeCommand(
command: T[number]['commands'][number],
): Promise<void>;
): Promise<
Partial<
Omit<Intersect<ObjectGet<T>[number]>, 'commands' | 'type' | 'attributes'>
>
>;
}
86 changes: 66 additions & 20 deletions src/Modules/SmartHome/SmartHomeController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SmartHomeV1SyncResponse,
SmartHomeV1DisconnectResponse,
SmartHomeV1DisconnectRequest,
SmartHomeJwt,
} from 'actions-on-google';
import type {
BuiltinFrameworkMetadata,
Expand All @@ -26,16 +27,25 @@ interface CreateControllerOptions<T> {
}

export class SmartHomeController<
T extends BaseDevice<ReadonlyArray<Traits>>[]
T extends ReadonlyArray<BaseDevice<Traits[]>>
> {
public devices: T;

public smartHome: AppHandler & SmartHomeApp;

public static async createController<T extends BaseDevice<Traits[]>[]>({
devices,
}: CreateControllerOptions<T>): Promise<SmartHomeController<T>> {
const smartHome = smarthome();
public static async createController<
T extends ReadonlyArray<BaseDevice<any>>
>({ devices }: CreateControllerOptions<T>): Promise<SmartHomeController<T>> {
let jwt: SmartHomeJwt | undefined;

if (process.env.JWT_PATH) {
jwt = (await import(process.env.JWT_PATH)).default;
}

const smartHome = smarthome({
key: process.env.KEY,
jwt,
});

const smartHomeController = new SmartHomeController<T>();
smartHomeController.smartHome = smartHome;
Expand All @@ -48,6 +58,20 @@ export class SmartHomeController<
smartHomeController.onDisconnect(...args),
);

setInterval(async () => {
const data = await smartHomeController.getDeviceStatus();

smartHomeController.smartHome.reportState({
requestId: Math.random().toString(),
agentUserId: '544845',
payload: {
devices: {
states: Object.fromEntries(data),
},
},
});
}, 5000);

return smartHomeController;
}

Expand All @@ -56,11 +80,15 @@ export class SmartHomeController<
headers: Headers,
framework?: BuiltinFrameworkMetadata,
): Promise<SmartHomeV1ExecuteResponse> {
console.log('onExec: ', body.inputs[0].payload.commands[0], headers);

const commands = await Promise.all(
body.inputs.flatMap((execInput) => {
return execInput.payload.commands.flatMap(({ devices, execution }) => {
return devices.flatMap<Promise<SmartHomeV1ExecuteResponseCommands>>(
async ({ id }) => {
let states = {};

const localDevice = this.devices.find(
(device) => device.id === id,
);
Expand All @@ -86,13 +114,14 @@ export class SmartHomeController<

Object.assign(deviceCommand, exec.params);

await localDevice.executeCommand(deviceCommand);
states = await localDevice.executeCommand(deviceCommand);
break;
}

return {
ids: [id],
status: 'SUCCESS',
states,
};
},
);
Expand All @@ -108,27 +137,45 @@ export class SmartHomeController<
};
}

public async onQuery(
body: SmartHomeV1QueryRequest,
headers: Headers,
framework?: BuiltinFrameworkMetadata,
): Promise<SmartHomeV1QueryResponse> {
const devicePromises = body.inputs.flatMap(({ intent, payload }) =>
payload.devices.map(async ({ id }) => {
const localDevice = this.devices.find((device) => device.id === id);
public async getDeviceStatus(
deviceIds?: string[],
): Promise<[string, unknown][]> {
let devices: BaseDevice<ReadonlyArray<Traits>>[];

if (deviceIds) {
devices = deviceIds.flatMap((deviceId) => {
const localDevice = this.devices.find(
(device) => device.id === deviceId,
);
if (!localDevice) {
console.log(`Can't find ${id}`);
return [];
}

return [id, await localDevice.getStatus()] as [string, unknown];
}),
return localDevice;
});
} else {
devices = this.devices;
}

return Promise.all(
devices.map<Promise<[string, unknown]>>(async (localDevice) => [
localDevice.id,
await localDevice.getStatus(),
]),
);
}

public async onQuery(
body: SmartHomeV1QueryRequest,
headers: Headers,
framework?: BuiltinFrameworkMetadata,
): Promise<SmartHomeV1QueryResponse> {
console.log('onQuery', body);

return {
requestId: body.requestId,
payload: {
devices: Object.fromEntries(await Promise.all(devicePromises)),
devices: Object.fromEntries(await this.getDeviceStatus()),
},
};
}
Expand Down Expand Up @@ -164,11 +211,10 @@ export class SmartHomeController<
},
);

console.log('onSync: ', devices);

return {
requestId: body.requestId,
payload: {
agentUserId: '544845',
devices,
},
};
Expand Down
4 changes: 3 additions & 1 deletion src/Modules/Trait/Trait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RunCycleTrait } from './Traits/RunCycleTrait';
import { TimerTrait } from './Traits/TimerTrait';
import { ModesTrait } from './Traits/ModeTrait';
import { SensorStateTrait } from './Traits/SensorStateTrait';
import { LocatorTrait } from './Traits/LocatorTrait';

export type Traits =
| OnOffTrait
Expand All @@ -16,4 +17,5 @@ export type Traits =
| SensorStateTrait
| RunCycleTrait
| TimerTrait
| ModesTrait;
| ModesTrait
| LocatorTrait;
27 changes: 27 additions & 0 deletions src/Modules/Trait/Traits/LocatorTrait.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// src/Modules/Trait/Traits/LocatorTrait.ts
import { BaseTrait } from '../BaseTrait';
import { TraitType } from '../TraitType';
import { LocateCommand } from '../../Command/Commands/LocateCommand';

interface Attributes {}

/**
* This trait is used for devices that can be "found". This includes phones, robots
* (including vacuums and mowers), drones, and tag-specific products that attach to other devices.
* Devices can be found via a local indicator (for example, beeping, ringing, flashing or shrieking).
* Requests to Find my [device] result in the device attempting to indicate its location.
*
* https://developers.google.com/assistant/smarthome/traits/locator
*/
export class LocatorTrait extends BaseTrait {
public type = TraitType.Locator;

public attributes: Attributes;

public commands = [new LocateCommand()] as const;

/**
* Set to true if an alert (audible or visible) was successfully generated on the device.
*/
public generatedAlert?: boolean;
}

0 comments on commit b06a809

Please sign in to comment.