Smart home integrations allow the Google Assistant to control connected devices in users' homes. To build a smart home Action, you need to provide a cloud webhook endpoint capable of handling smart home intents. For instance, when a user says, "Hey Google, turn on the lights,'' the Assistant sends the command to your cloud fulfillment to update the state of the device.

The Local Home SDK enhances your smart home integration by adding a local path to route smart home intents directly to a Google Home device, which enhances reliability and reduces latency in processing users' commands. It allows you to write and deploy a local fulfillment app in TypeScript or JavaScript that identifies devices and executes commands on any Google Home smart speaker or Google Nest smart display. Your app then communicates directly with users' existing smart devices over the local area network by using existing standard protocols to fulfill commands.

Prerequisites

What you'll build

In this codelab, you will deploy a previously built smart home integration with Firebase, then apply a scan configuration in the Actions console, and build a local app using TypeScript to send commands written in Node.js to a virtual washer device.

What you'll learn

What you'll need

This codelab works with the M5Stack Core IoT development kit running MicroPython. You will use the M5Stack to simulate the use of local fulfillment to control a smart clothes washer.

You will connect the M5Stack to the local network and start a local UDP server to listen for incoming commands. However, you can also control the washer through the buttons on the device. The washer's state will display on the device.

Enable Activity controls

Enable the following Activity controls in the Google Account you plan to use with the Assistant:

Create an Actions project

  1. Go to the Actions on Google Developer Console.
  2. Click New Project, enter a name for the project, and click CREATE PROJECT.

Select the Smart Home App

On the Overview screen in the Actions console, select Smart home.

Choose the Smart home experience card, and you will then be directed to your project console.

Install the Firebase CLI

The Firebase Command Line Interface (CLI) will allow you to serve your web apps locally and deploy your web app to Firebase hosting.

To install the CLI, run the following npm command from the terminal:

npm install -g firebase-tools

To verify that the CLI has been installed correctly, run:

firebase --version

Authorize the Firebase CLI with your Google account by running:

firebase login

Enable the HomeGraph API

The HomeGraph API enables the storage and querying of devices and their states within a user's Home Graph. To use this API, you must first open the Google Cloud console and enable the HomeGraph API.

In the Google Cloud console, make sure to select the project that matches your Actions <project-id>. Then, in the API Library screen for the HomeGraph API, click Enable.

Now that you set up your development environment, you can deploy the starter project to verify everything is configured properly.

Get the source code

Click the following link to download the sample for this codelab on your development machine:

Download source code

...or you can clone the GitHub repository from the command line:

git clone -b device https://github.com/googlecodelabs/smarthome-local.git

About the project

The starter project contains the following subdirectories:

The provided cloud fulfillment includes the following functions in index.js:

Connect to Firebase

Navigate to the app-start directory, then set up the Firebase CLI with your Actions project:

cd app-start
firebase use <project-id>

Deploy to Firebase

Navigate to the functions folder and install all the necessary dependencies using npm.

cd functions
npm install

Now that you have installed the dependencies and configured your project, you are ready to run the app for the first time.

firebase deploy

This is the console output you should see:

...

✔ Deploy complete!

Project Console: https://console.firebase.google.com/project/<project-id>/overview
Hosting URL: https://<project-id>.firebaseapp.com

This command deploys a web app, along with several Cloud Functions for Firebase.

Open the Hosting URL in your browser (https://<project-id>.firebaseapp.com) to view the web app. You will see the following interface:

This web UI represents a third-party platform to view or modify device states. To begin populating your database with device information, click UPDATE. You won't see any changes on the page, but the current state of your washer will be stored in the database.

Now it's time to connect the cloud service you've deployed to the Google Assistant using the Actions console.

Configure your Actions console project

Under Overview > Build your Action, select Add Action(s). Enter the URL for your cloud function that provides fulfillment for the smart home intents and click Save.

https://us-central1-<project-id>.cloudfunctions.net/smarthome

On the Develop > Invocation tab, add a Display Name for your Action, and click Save. This name will appear in the Google Home app.

To enable Account linking, select the Develop > Account linking option in the left navigation. Use these account linking settings:

Client ID

ABC123

Client secret

DEF456

Authorization URL

https://us-central1-<project-id>.cloudfunctions.net/fakeauth

Token URL

https://us-central1-<project-id>.cloudfunctions.net/faketoken

Click Save to save your account linking configuration, then click Test to enable testing on your project.

You will be redirected to the Simulator. Verify that testing has been enabled for your project by moving your mouse over the Testing on Device ( ) icon.

Link to Google Assistant

In order to test your smart home Action, you need to link your project with a Google account. This enables testing through Google Assistant surfaces and the Google Home app that are signed in to the same account.

  1. On your phone, open the Google Assistant settings. Note that you should be logged in as the same account as in the console.
  2. Navigate to Google Assistant > Settings > Home Control (under Assistant).
  3. Select the plus (+) icon in the bottom right corner
  4. You should see your test app with the [test] prefix and the display name you set.
  5. Select that item. The Google Assistant will then authenticate with your service and send a SYNC request, asking your service to provide a list of devices for the user.

Open the Google Home app and verify that you can see your washer device.

Verify that you can control the washer with voice commands in the Google Home app. You should also see the device state change in the frontend web UI of your cloud fulfillment.

Now you can begin adding local fulfillment to your Action.

To support local fulfillment, you need to add a new per-device field called otherDeviceIds to the cloud SYNC response containing a unique local identifier for the device. This field also indicates the ability to locally control that device.

Add the otherDeviceIds field to the SYNC response as shown in the following code snippet:

functions/index.js

app.onSync((body) => {
  return {
    requestId: body.requestId,
    payload: {
      agentUserId: '123',
      devices: [{
        id: 'washer',
        type: 'action.devices.types.WASHER',
        traits: [ ... ],
        name: { ... },
        deviceInfo: { ... },
        willReportState: true,
        attributes: {
          pausable: true,
        },
        otherDeviceIds: [{
          deviceId: 'deviceid123',
        }],
      }],
    },
  };
});

Deploy the updated project to Firebase:

firebase deploy --only functions

After deployment completes, navigate to the web UI and click the Refresh button in the toolbar. This triggers a Request Sync operation so that the Assistant receives the updated SYNC response data.

In this section, you will add the necessary configuration options for local fulfillment to your smart home Action. During development, you will publish the local fulfillment app to Firebase Hosting, where the Google Home device can access and download it.

In the Actions console, select Develop > Actions and find the Configure local home SDK section. Enter the following URL into the test URL field, insert your project ID, and click Save:

https://<project-id>.firebaseapp.com/local-home/index.html

Next, we need to define how the Google Home device should discover the local smart devices. The Local Home platform supports several protocols for device discovery, including mDNS, UPnP, and UDP broadcast. You will use UDP broadcast to discover the smart washer.

Click New scan config under Device scan configuration to add a new scan configuration. Select UDP as the protocol, and fill in the following attributes:

Field

Description

Suggested value

Broadcast address

UDP broadcast address

255.255.255.255

Broadcast port

Port where Google Home sends

the UDP broadcast

3311

Listen port

Port where Google Home listens

for a response

3312

Discovery packet

UDP broadcast data payload

48656c6c6f4c6f63616c486f6d6553444b

Finally, click Save at the top of the window to publish your changes.

You will develop your local fulfillment app in TypeScript using the Local Home SDK typings package. Take a look at the skeleton provided in the starter project:

local/index.ts

/// <reference types="@google/local-home-sdk" />

import App = smarthome.App;
import Constants = smarthome.Constants;
import DataFlow = smarthome.DataFlow;
import Execute = smarthome.Execute;
import Intents = smarthome.Intents;
import IntentFlow = smarthome.IntentFlow;

...

class LocalExecutionApp {

  constructor(private readonly app: App) { }

  identifyHandler(request: IntentFlow.IdentifyRequest):
      Promise<IntentFlow.IdentifyResponse> {
    // TODO: Implement device identification
  }

  executeHandler(request: IntentFlow.ExecuteRequest):
      Promise<IntentFlow.ExecuteResponse> {
    // TODO: Implement local fulfillment
  }

  ...
}

const localHomeSdk = new App('1.0.0');
const localApp = new LocalExecutionApp(localHomeSdk);
localHomeSdk
  .onIdentify(localApp.identifyHandler.bind(localApp))
  .onExecute(localApp.executeHandler.bind(localApp))
  .listen()
  .then(() => console.log('Ready'))
  .catch((e: Error) => console.error(e));

The core component of local fulfillment is the smarthome.App class. The starter project attaches handlers for the IDENTIFY and EXECUTE intents, then calls the listen() method to inform Local Home SDK that the app is ready.

Add the IDENTIFY handler

The Local Home SDK triggers your IDENTIFY handler when the Google Home device discovers unverified devices on the local network based on the scan configuration provided in the Actions console.

Meanwhile, the platform invokes the identifyHandler method with the resulting scan data when Google discovers a matching device. In your app, scanning takes place using a UDP broadcast and the scan data provided to the IDENTIFY handler includes the response payload sent by the local device.

The handler returns an IdentifyResponse instance containing a unique identifier for the local device. Add the following code to your identifyHandler method to process the UDP response coming from the local device and determine the appropriate local device ID:

local/index.ts

identifyHandler(request: IntentFlow.IdentifyRequest):
    Promise<IntentFlow.IdentifyResponse> {
  console.log("IDENTIFY intent: " + JSON.stringify(request, null, 2));

  const scanData = request.inputs[0].payload.device.udpScanData;
  if (!scanData) {
    const err = new IntentFlow.HandlerError(request.requestId,
        'invalid_request', 'Invalid scan data');
    return Promise.reject(err);
  }

  // In this codelab, the scan data contains only local device id.
  const localDeviceId = Buffer.from(scanData.data, 'hex');

  const response: IntentFlow.IdentifyResponse = {
    intent: Intents.IDENTIFY,
    requestId: request.requestId,
    payload: {
      device: {
        id: 'washer',
        verificationId: localDeviceId.toString(),
      }
    }
  };
  console.log("IDENTIFY response: " + JSON.stringify(response, null, 2));

  return Promise.resolve(response);
}

Note that the verificationId must match one of the otherDeviceIds in your SYNC response, which flags the device as available for local fulfillment in the user's Home Graph. After Google finds a match, that device is considered verified and ready for local fulfillment.

Add the EXECUTE handler

The Local Home SDK triggers your EXECUTE handler when a device that supports local fulfillment receives a command. The content of the local intent is equivalent to the EXECUTE intent sent to your cloud fulfillment, so the logic for locally processing the intent resembles how you handle it in the cloud.

The app can use TCP/UDP sockets or HTTP(S) requests to communicate with local devices. In this codelab, UDP serves as the protocol used to control the virtual device. The port number is defined in index.ts as the SERVER_PORT variable.

Add the following code to your executeHandler method to process incoming commands and send them to the local device over UDP:

local/index.ts

executeHandler(request: IntentFlow.ExecuteRequest):
    Promise<IntentFlow.ExecuteResponse> {
  console.log("EXECUTE intent: " + JSON.stringify(request, null, 2));

  const command = request.inputs[0].payload.commands[0];
  const execution = command.execution[0];
  const response = new Execute.Response.Builder()
    .setRequestId(request.requestId);

  const promises: Array<Promise<void>> = command.devices.map((device) => {
    console.log("Handling EXECUTE intent for device: " + JSON.stringify(device));

    // Convert execution params to a string for the local device
    const params = execution.params as IWasherParams;
    const payload = this.getDataForCommand(execution.command, params);

    // Create a command to send over the local network
    const radioCommand = new DataFlow.UdpRequestData();
    radioCommand.requestId = request.requestId;
    radioCommand.deviceId = device.id;
    radioCommand.data = Buffer.from(payload).toString('hex');
    radioCommand.port = SERVER_PORT;

    console.log("Sending request to the smart home device:", payload);

    return this.app.getDeviceManager()
      .send(radioCommand)
      .then(() => {
        const state = {online: true};
        response.setSuccessState(device.id, Object.assign(state, params));
        console.log(`Command successfully sent to ${device.id}`);
      })
      .catch((e: IntentFlow.HandlerError) => {
        e.errorCode = e.errorCode || 'invalid_request';
        response.setErrorState(device.id, e.errorCode);
        console.error('An error occurred sending the command', e.errorCode);
      });
  });

  return Promise.all(promises)
    .then(() => {
      return response.build();
    })
    .catch((e) => {
      const err = new IntentFlow.HandlerError(request.requestId,
          'invalid_request', e.message);
      return Promise.reject(err);
    });
}

Update the server port

The execution commands are sent over UDP to the port specified by the SERVER_PORT constant. Update this value to match the UDP Broadcast port value entered previously in the Actions console scan configuration.

local/index.ts

const SERVER_PORT = 3311;

Compile the TypeScript app

Navigate to the local/ directory and run the following commands to download the TypeScript compiler and compile the app:

cd local
npm install
npm run build

This compiles the index.ts (TypeScript) source and places the following contents into the public/local-home/ directory:

Deploy the test project

Deploy the updated project files to Firebase Hosting so you can access them from the Google Home device.

firebase deploy --only hosting

Now you can test the communication between your local fulfillment app and the smart washer! The codelab starter project includes a MicroPython smart washer, which emulates a smart washer that users can locally control.

Connect to the device

Connect the M5Stack device to your development machine using the provided USB cable and discover the name of the serial port that it presents. For example, on Macintosh and Linux, you can search for a new TTY device in /dev:

macOS

ls /dev/{tty,cu}.*

Linux

ls /dev/ttyUSB*

Open a serial terminal session to the device using the serial settings 115200, 8N1. For example, on macOS and Linux, you can do this with the screen command:

screen /dev/ttyUSB0 115200

Configure network connection

From the serial console, use the connect() command to associate your device with the same WiFi network as your development machine and Google Home device.

>>> wifi.connect('wifi-ssid', 'passphrase')

The console log reports the IP address of the device once connected.

Start local server

Use the listen() command to start a local UDP server on the device using the following parameters:

Parameter

Suggested value

device_id

deviceid123

udp_port

3311

project_id

Your Actions project ID

This server handles local device discovery and execution of commands.

>>> washer.listen(3311, 'deviceid123', project_id='project-id')

Alternative—use a virtual device

If you do not have access to a physical MicroPython device or cannot connect to it, then you can also use the virtual device—written in Node.js—to test local fulfillment.

Navigate to the virtual-device/ directory and run the device script, passing the configuration parameters as arguments:

cd virtual-device
npm install
npm start -- \
  --deviceId=deviceid123 --projectId=<project-id> \
  --udpPort=3311 --discoveryPacket=HelloLocalHomeSDK

Verify that the device script runs with the expected parameters:

(...): UDP Server listening on 3311
(...): Report State successful

In the following section, you will verify that the Google Home device can properly scan, identify, and send commands to the virtual smart washer over the local network. You can use Google Chrome Developer Tools to connect to the Google Home device, view the console logs, and debug the TypeScript app.

Connect Chrome Developer Tools

To connect the debugger to your local fulfillment app, follow these steps:

  1. Make sure that you linked your Google Home device to a user with permission to access the Actions console project.
  2. Reboot your Google Home device, which enables it to get the URL of your HTML as well as the scan configuration that you put in the Actions console.
  3. Launch Chrome on your development machine.
  4. Open a new Chrome tab and enter chrome://inspect in the address field to launch the inspector.

You should see a list of devices on the page and your app URL should appear under the name of your Google Home device.

Launch the inspector

Click Inspect under your app URL to launch Chrome Developer Tools. Select the Console tab and verify that you can see the content of IDENTIFY intent printed by your TypeScript app.

This output means that your local fulfillment app successfully discovered and identified the virtual device.

Test local fulfillment

Send commands to your device using the touch controls in the Google Home app or through voice commands to the Google Home device, such as:

"Hey Google, turn on my washer."

"Hey Google, start my washer."

"Hey Google, stop my washer."

This should trigger the platform to send an EXECUTE intent to your TypeScript app.

Verify that you can see the local smart washer state change with each command.

...
***** The washer is RUNNING *****
...
***** The washer is STOPPED *****

Congratulations! You used the Local Home SDK to integrate local fulfillment into a smart home Action.

Learn more

Here are some additional things you can try: