From b48f711971c219fb52962839be9d8a6b8c1d83b5 Mon Sep 17 00:00:00 2001 From: Loek Sangers <9132841+LoekSangers@users.noreply.github.com> Date: Sun, 23 Jan 2022 20:45:46 +0100 Subject: [PATCH] Enable google assistant local fulfillment (#20987) --- .../_integrations/google_assistant.markdown | 15 ++ .../integrations/google_assistant/app.js | 189 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 source/assets/integrations/google_assistant/app.js diff --git a/source/_integrations/google_assistant.markdown b/source/_integrations/google_assistant.markdown index f1b89077976..33586aafb43 100644 --- a/source/_integrations/google_assistant.markdown +++ b/source/_integrations/google_assistant.markdown @@ -107,6 +107,21 @@ If you want to support active reporting of state to Google's server (configurati 3. Click Enable HomeGraph API. 3. Try "OK Google, sync my devices" - the Google Home app should import your exposed Home Assistant devices and prompt you to assign them to rooms. +### Enable Local Fulfillment + +1. Open the project you created in the [Actions on Google console](https://console.actions.google.com/). +2. Click `Develop` on the top of the page, then click `Actions` located in the hamburger menu on the top left. +3. Upload [this Javascript file](/assets/integrations/google_assistant/app.js) for both Node and Chrome by clicking the `Upload Javascript files` button. +4. Add device scan configuration: + 1. Click `+ New scan config` + 2. Select `MDNS` + 3. set mDNS service name to `_home-assistant._tcp.local` +5. `Save` your changes. +6. Either wait for 30 minutes, or restart your connected Google device. +7. Restart Home Assistant Core. + +You can debug the setup by following [these instructions](https://developers.google.com/assistant/smarthome/develop/local#debugging_from_chrome) + ### YAML Configuration Now add your setup to your `configuration.yaml` file, such as: diff --git a/source/assets/integrations/google_assistant/app.js b/source/assets/integrations/google_assistant/app.js new file mode 100644 index 00000000000..7a9a7ff1a20 --- /dev/null +++ b/source/assets/integrations/google_assistant/app.js @@ -0,0 +1,189 @@ +"use strict"; +/// +/* +BASED ON: https://github.com/NabuCasa/home-assistant-google-assistant-local-sdk +Only removed the fart sound at the end. +For license information please check the repository. +*/ +var App = smarthome.App; +var Constants = smarthome.Constants; +var DataFlow = smarthome.DataFlow; +var Execute = smarthome.Execute; +var Intents = smarthome.Intents; +var IntentFlow = smarthome.IntentFlow; +const findHassCustomDeviceDataByMdnsData = (requestId, devices, mdnsScanData) => { + let device; + device = devices.find((dev) => { + const customData = dev.customData; + return (customData && + "webhookId" in customData && + (!mdnsScanData.uuid || customData.uuid === mdnsScanData.uuid) && + (!mdnsScanData.baseUrl || customData.baseUrl === mdnsScanData.baseUrl)); + }); + // backwards compatibility for HA < 0.109 + if (!device) { + device = devices.find((dev) => dev.customData && + "webhookId" in dev.customData); + } + if (!device) { + console.log(requestId, "Unable to find HASS connection info.", devices); + throw new IntentFlow.HandlerError(requestId, "invalidRequest", "Unable to find HASS connection info."); + } + return device.customData; +}; +const findHassCustomDeviceDataByDeviceId = (requestId, devices, deviceId) => { + let device; + device = devices.find((dev) => { + const customData = dev.customData; + return (customData && + "webhookId" in customData && + customData.proxyDeviceId === deviceId); + }); + if (!device) { + console.log(requestId, "Unable to find HASS connection info.", devices); + throw new IntentFlow.HandlerError(requestId, "invalidRequest", "Unable to find HASS connection info."); + } + return device.customData; +}; +const createResponse = (request, payload) => ({ + intent: request.inputs[0].intent, + requestId: request.requestId, + payload, +}); +class UnknownInstance extends Error { + constructor(requestId) { + super(); + this.requestId = requestId; + } + throwHandlerError() { + throw new IntentFlow.HandlerError(this.requestId, "invalidRequest", "Unknown Instance"); + } +} +const forwardRequest = async (hassDeviceData, targetDeviceId, request) => { + const command = new DataFlow.HttpRequestData(); + command.method = Constants.HttpOperation.POST; + command.requestId = request.requestId; + command.deviceId = targetDeviceId; + command.isSecure = hassDeviceData.httpSSL; + command.port = hassDeviceData.httpPort; + command.path = `/api/webhook/${hassDeviceData.webhookId}`; + command.data = JSON.stringify(request); + command.dataType = "application/json"; + console.log(request.requestId, "Sending", command); + const deviceManager = await app.getDeviceManager(); + let resp; + try { + resp = await new Promise((resolve, reject) => { + setTimeout(() => reject(-1), 10000); + deviceManager + .send(command) + .then((response) => resolve(response), reject); + }); + // resp = (await deviceManager.send(command)) as HttpResponseData; + console.log(request.requestId, "Raw Response", resp); + } + catch (err) { + console.error(request.requestId, "Error making request", err); + throw new IntentFlow.HandlerError(request.requestId, "invalidRequest", err === -1 ? "Timeout" : err.message); + } + // Response if the webhook is not registered. + if (resp.httpResponse.statusCode === 200 && !resp.httpResponse.body) { + throw new UnknownInstance(request.requestId); + } + try { + const response = JSON.parse(resp.httpResponse.body); + // Local SDK wants this. + response.intent = request.inputs[0].intent; + console.log(request.requestId, "Response", response); + return response; + } + catch (err) { + console.error(request.requestId, "Error parsing body", err); + throw new IntentFlow.HandlerError(request.requestId, "invalidRequest", err.message); + } +}; +const identifyHandler = async (request) => { + console.log("IDENTIFY intent:", request); + const deviceToIdentify = request.inputs[0].payload.device; + if (!deviceToIdentify.mdnsScanData) { + console.error(request.requestId, "No usable mdns scan data"); + return createResponse(request, {}); + } + if (!deviceToIdentify.mdnsScanData.serviceName.endsWith("._home-assistant._tcp.local")) { + console.error(request.requestId, "Not Home Assistant type"); + return createResponse(request, {}); + } + try { + const hassCustomData = findHassCustomDeviceDataByMdnsData(request.requestId, request.devices, deviceToIdentify.mdnsScanData.txt); + return await forwardRequest(hassCustomData, "", request); + } + catch (err) { + if (err instanceof UnknownInstance) { + return createResponse(request, {}); + } + throw err; + } +}; +const reachableDevicesHandler = async (request) => { + console.log("REACHABLE_DEVICES intent:", request); + const hassCustomData = findHassCustomDeviceDataByDeviceId(request.requestId, request.devices, request.inputs[0].payload.device.id); + try { + return forwardRequest(hassCustomData, + // Old code would sent it to the proxy ID: hassCustomData.proxyDeviceId + // But tutorial claims otherwise, but maybe it is not for hub devices?? + // https://developers.google.com/assistant/smarthome/develop/local#implement_the_execute_handler + // Sending it to the device that has to receive the command as per the tutorial + request.inputs[0].payload.device.id, request); + } + catch (err) { + if (err instanceof UnknownInstance) { + err.throwHandlerError(); + } + throw err; + } +}; +const executeHandler = async (request) => { + console.log("EXECUTE intent:", request); + const device = request.inputs[0].payload.commands[0].devices[0]; + try { + return forwardRequest(device.customData, device.id, request); + } + catch (err) { + if (err instanceof UnknownInstance) { + err.throwHandlerError(); + } + throw err; + } +}; +const app = new App("1.0.0"); +app + .onIdentify(identifyHandler) + .onReachableDevices(reachableDevicesHandler) + .onExecute(executeHandler) + // Undocumented in TypeScript + // Suggested by Googler, seems to work :shrug: + // https://github.com/actions-on-google/smart-home-local/issues/1#issuecomment-515706997 + // @ts-ignore + .onProxySelected((req) => { + console.log("ProxySelected", req); + return createResponse(req, {}); +}) + // @ts-ignore + .onIndicate((req) => console.log("Indicate", req)) + // @ts-ignore + .onParseNotification((req) => console.log("ParseNotification", req)) + // @ts-ignore + .onProvision((req) => console.log("Provision", req)) + // @ts-ignore + .onQuery((req) => console.log("Query", req)) + // @ts-ignore + .onRegister((req) => console.log("Register", req)) + // @ts-ignore + .onUnprovision((req) => console.log("Unprovision", req)) + // @ts-ignore + .onUpdate((req) => console.log("Update", req)) + .listen() + .then(() => { + console.log("Ready!"); +}) + .catch((e) => console.error(e));