diff --git a/package-lock.json b/package-lock.json index a00bee1..108787e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2715,6 +2715,12 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", diff --git a/package.json b/package.json index 4ea4ccc..281d94f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Lab/Devices/CoreLight/index.ts b/src/Lab/Devices/CoreLight/index.ts index a1ce46f..8962ea6 100644 --- a/src/Lab/Devices/CoreLight/index.ts +++ b/src/Lab/Devices/CoreLight/index.ts @@ -89,6 +89,9 @@ export class CoreLight extends BaseDevice { break; case CommandType.appSelect: console.log('Select application: ', command); + break; } + + return this.getStatus(); }; } diff --git a/src/Lab/Devices/Jumper/index.ts b/src/Lab/Devices/Jumper/index.ts index 9adf119..1669e64 100644 --- a/src/Lab/Devices/Jumper/index.ts +++ b/src/Lab/Devices/Jumper/index.ts @@ -28,6 +28,8 @@ export class Jumper extends BaseDevice { swVersion: '0.0.1', }; + public willReportState = true; + public name: BaseDevice['name'] = { name: 'Jumper', defaultNames: ['Jumper'], @@ -108,6 +110,7 @@ export class Jumper extends BaseDevice { public startTimer(timerTimeSec: number): void { this.customData.timerRemainingSec = timerTimeSec; + this.customData.timerPaused = false; this.startInterval(); } @@ -122,6 +125,8 @@ export class Jumper extends BaseDevice { } public pauseTimer(): void { + console.log('pause timer: ', this.customData); + if (!this.customData.timer) { return; } @@ -144,11 +149,14 @@ export class Jumper extends BaseDevice { >['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: @@ -163,6 +171,13 @@ export class Jumper extends BaseDevice { 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; }; } diff --git a/src/Lab/index.ts b/src/Lab/index.ts index 626c0d3..4266dd5 100644 --- a/src/Lab/index.ts +++ b/src/Lab/index.ts @@ -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 })); diff --git a/src/Modules/Command/BaseCommand.ts b/src/Modules/Command/BaseCommand.ts index e1cfe69..adacfe8 100644 --- a/src/Modules/Command/BaseCommand.ts +++ b/src/Modules/Command/BaseCommand.ts @@ -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 { diff --git a/src/Modules/Command/Commands.ts b/src/Modules/Command/Commands.ts index 39d71f3..28f0eec 100644 --- a/src/Modules/Command/Commands.ts +++ b/src/Modules/Command/Commands.ts @@ -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 @@ -20,4 +21,5 @@ export type Commands = | TimerResumeCommand | TimerCancelCommand | TimerAdjustCommand - | SetModesCommand; + | SetModesCommand + | LocateCommand; diff --git a/src/Modules/Command/Commands/LocateCommand.ts b/src/Modules/Command/Commands/LocateCommand.ts new file mode 100644 index 0000000..65a3e4a --- /dev/null +++ b/src/Modules/Command/Commands/LocateCommand.ts @@ -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; +} diff --git a/src/Modules/Device/BaseDevice.ts b/src/Modules/Device/BaseDevice.ts index 45b2257..6c30122 100644 --- a/src/Modules/Device/BaseDevice.ts +++ b/src/Modules/Device/BaseDevice.ts @@ -52,5 +52,9 @@ export abstract class BaseDevice> { public abstract executeCommand( command: T[number]['commands'][number], - ): Promise; + ): Promise< + Partial< + Omit[number]>, 'commands' | 'type' | 'attributes'> + > + >; } diff --git a/src/Modules/SmartHome/SmartHomeController.ts b/src/Modules/SmartHome/SmartHomeController.ts index 4b3b0b1..4f81d09 100644 --- a/src/Modules/SmartHome/SmartHomeController.ts +++ b/src/Modules/SmartHome/SmartHomeController.ts @@ -13,6 +13,7 @@ import { SmartHomeV1SyncResponse, SmartHomeV1DisconnectResponse, SmartHomeV1DisconnectRequest, + SmartHomeJwt, } from 'actions-on-google'; import type { BuiltinFrameworkMetadata, @@ -26,16 +27,25 @@ interface CreateControllerOptions { } export class SmartHomeController< - T extends BaseDevice>[] + T extends ReadonlyArray> > { public devices: T; public smartHome: AppHandler & SmartHomeApp; - public static async createController[]>({ - devices, - }: CreateControllerOptions): Promise> { - const smartHome = smarthome(); + public static async createController< + T extends ReadonlyArray> + >({ devices }: CreateControllerOptions): Promise> { + 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(); smartHomeController.smartHome = smartHome; @@ -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; } @@ -56,11 +80,15 @@ export class SmartHomeController< headers: Headers, framework?: BuiltinFrameworkMetadata, ): Promise { + 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>( async ({ id }) => { + let states = {}; + const localDevice = this.devices.find( (device) => device.id === id, ); @@ -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, }; }, ); @@ -108,27 +137,45 @@ export class SmartHomeController< }; } - public async onQuery( - body: SmartHomeV1QueryRequest, - headers: Headers, - framework?: BuiltinFrameworkMetadata, - ): Promise { - 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>[]; + + 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>(async (localDevice) => [ + localDevice.id, + await localDevice.getStatus(), + ]), ); + } + + public async onQuery( + body: SmartHomeV1QueryRequest, + headers: Headers, + framework?: BuiltinFrameworkMetadata, + ): Promise { + console.log('onQuery', body); return { requestId: body.requestId, payload: { - devices: Object.fromEntries(await Promise.all(devicePromises)), + devices: Object.fromEntries(await this.getDeviceStatus()), }, }; } @@ -164,11 +211,10 @@ export class SmartHomeController< }, ); - console.log('onSync: ', devices); - return { requestId: body.requestId, payload: { + agentUserId: '544845', devices, }, }; diff --git a/src/Modules/Trait/Trait.ts b/src/Modules/Trait/Trait.ts index 81ce015..2fd2243 100644 --- a/src/Modules/Trait/Trait.ts +++ b/src/Modules/Trait/Trait.ts @@ -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 @@ -16,4 +17,5 @@ export type Traits = | SensorStateTrait | RunCycleTrait | TimerTrait - | ModesTrait; + | ModesTrait + | LocatorTrait; diff --git a/src/Modules/Trait/Traits/LocatorTrait.ts b/src/Modules/Trait/Traits/LocatorTrait.ts new file mode 100644 index 0000000..7dd5cab --- /dev/null +++ b/src/Modules/Trait/Traits/LocatorTrait.ts @@ -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; +}