Smart home Actions use device types to let the Google Assistant know what grammar should be used with a device. Device traits define the capabilities of a device type. A device inherits the the states of each device trait added to an Action.

Developers can connect any supported traits to their chosen device type to customize the functionality of their devices. For developers who wish to implement custom traits in their Actions that are not currently available in the existing device schema, the Modes and Toggles traits allow specific settings' control with a custom name defined by the developer.

Beyond the basic control capability provided by types and traits, the Smart Home API has additional features to enhance the user experience. Error responses provide detailed user feedback when intents don't succeed. Two-factor authentication (2FA) extends these responses and adds additional security to the device trait of your choice. By sending specific error responses to challenge blocks issued from the Google Assistant, your smart home Action can require additional authorization to complete a command.

What you'll build

In this codelab, you will deploy a pre-built smart home integration with Firebase, then learn how to add non-standard traits to the Smart Home Washer for load size and turbo mode. You'll also learn how to enforce a verbal acknowledgement to turn on the washer using 2FA. Additionally, you'll implement error and exception reporting.

For more general information about building a smart home Action, see Create a smart home Action.

What you'll learn

What you'll need

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've set up your development environment, let's 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 https://github.com/googlecodelabs/smarthome-traits.git

Unpack the downloaded zip file.

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 washer-start directory then set up the Firebase CLI with your Actions Project:

cd washer-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 using 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 that you have a basic washer deployed, you can customize the modes available on your device.

The action.devices.traits.Modes trait enables a device to have an arbitrary number of settings for a mode, of which only one can be set at a time. You add a mode to the washer to define the size of the laundry load: small, medium, or large.

Update SYNC response

You need to add information about this new trait to your SYNC response in functions/index.js. This data appears in the traits array and attributes object as shown below.

functions/index.js

app.onSync(body => {
  return {
    requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
    payload: {
      agentUserId: '123',
      devices: [{
        id: 'washer',
        type: 'action.devices.types.WASHER',
        traits: [
          'action.devices.traits.OnOff',
          'action.devices.traits.StartStop',
          'action.devices.traits.RunCycle',
          // Add Modes trait          
          'action.devices.traits.Modes',
        ],
        name: { ... },
        deviceInfo: { ... },
        attributes: {
          pausable: true,
          //Add availableModes
          availableModes: [{
              name: 'load',
              name_values: [{
                  name_synonym: ['load'],
                  lang: 'en'
                }],
              settings: [{
                  setting_name: 'small',
                  setting_values: [{
                      setting_synonym: ['small'],
                      lang: 'en'
                    }]
                  }, {
                  setting_name: 'medium',
                  setting_values: [{
                      setting_synonym: ['medium'],
                      lang: 'en'
                   }]
                  }, {
                  setting_name: 'large',
                  setting_values: [{
                      setting_synonym: ['large'],
                      lang: 'en'
                    }]
                }],
              ordered: true
            }],
        }
    }]
    }
  };
});

Add new EXECUTE intent commands

In your EXECUTE intent, add the action.devices.commands.SetModes command, as shown below.

functions/index.js

const updateDevice = async (execution,deviceId) => {
  const {params,command} = execution; 
  let state, ref;
  switch (command) {
    case 'action.devices.commands.OnOff':
      state = {on: params.on};
      ref = firebaseRef.child(deviceId).child('OnOff');
      break;
    case 'action.devices.commands.StartStop':
      state = {isRunning: params.start};
      ref = firebaseRef.child(deviceId).child('StartStop');
      break;
    case 'action.devices.commands.PauseUnpause':
      state = {isPaused: params.pause};
      ref = firebaseRef.child(deviceId).child('StartStop');
      Break;
    // Add SetModes command
    case 'action.devices.commands.SetModes':
      state = {load: params.updateModeSettings.load};
      ref = firebaseRef.child(deviceId).child('Modes');
      break;
}

Update QUERY response

Next, update your QUERY response to report the washer's current state. Add the updated changes to the queryFirebase and queryDevice functions to obtain the state as stored in the Firebase database.

functions/index.js

const queryFirebase = async (deviceId) => {
  const snapshot = await firebaseRef.child(deviceId).once('value');
  const snapshotVal = snapshot.val();
  return {
    on: snapshotVal.OnOff.on,
    isPaused: snapshotVal.StartStop.isPaused,
    isRunning: snapshotVal.StartStop.isRunning,
    // Add Modes snapshot
    load: snapshotVal.Modes.load,
  };
}

const queryDevice = async (deviceId) => {
  const data = await queryFirebase(deviceId);
  return {
    on: data.on,
    isPaused: data.isPaused,
    isRunning: data.isRunning,
    currentRunCycle: [{ ... }],
    currentTotalRemainingTime: 1212,
    currentCycleRemainingTime: 301,
    // Add currentModeSettings
    currentModeSettings: {
      load: data.load,
    },
  };
};

Update Report State

Finally, update your reportstate function to report the washer's current load setting to Home Graph.

functions/index.js

const requestBody = {
    requestId: 'ff36a3cc', /* Any unique ID */
    agentUserId: '123', /* Hardcoded user ID */
    payload: {
      devices: {
        states: {
          /* Report the current state of our washer */
          [context.params.deviceId]: {
            on: snapshot.OnOff.on,
            isPaused: snapshot.StartStop.isPaused,
            isRunning: snapshot.StartStop.isRunning,
            // Add currentModeSettings
            currentModeSettings: {
              load: snapshot.Modes.load,
            },
          },
        },
      },
    },
  };

Deploy to Firebase

Now deploy the updated action.

firebase deploy --only functions

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

You can then give a command to set the mode of the washer.

"Set the washer load to large"

Additionally, you are able to ask questions about your washer such as:

"What is the washer load?"

Toggles represent named aspects of a device that have a true/false state, such as whether the washer is in Turbo mode.

Update SYNC response

In your SYNC response, you need to add information about this new device trait. This will appear in the traits array and attributes object as shown in the response below.

functions/index.js

app.onSync(body => {
  return {
    requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
    payload: {
      agentUserId: '123',
      devices: [{
        id: 'washer',
        type: 'action.devices.types.WASHER',
        traits: [
          'action.devices.traits.OnOff',
          'action.devices.traits.StartStop',
          'action.devices.traits.RunCycle',
          'action.devices.traits.Modes',
          // Add Toggles trait
          'action.devices.traits.Toggles',
        ],
        name: { ... },
        deviceInfo: { ... },
        attributes: {
          pausable: true,
          availableModes: [{
              name: 'load',
              name_values: [{
                  name_synonym: ['load'],
                  lang: 'en'
                }],
              settings: [{ ... }],
              ordered: true
            }],
          //Add availableToggles
          availableToggles: [{
              name: 'Turbo',
              name_values: [{
                  name_synonym: ['turbo'],
                  lang: 'en'
              }]
          }],
        }
    }]
    }
  };
});

Add new EXECUTE intent commands

In your EXECUTE intent, add the action.devices.commands.SetToggles command, as shown below.

functions/index.js

const updateDevice = async (execution,deviceId) => {
  const {params,command} = execution;
  let state, ref;
  switch (command) {
    case 'action.devices.commands.OnOff':
      state = {on: params.on};
      ref = firebaseRef.child(deviceId).child('OnOff');
      break;
    case 'action.devices.commands.StartStop':
      state = {isRunning: params.start};
      ref = firebaseRef.child(deviceId).child('StartStop');
      break;
    case 'action.devices.commands.PauseUnpause':
      state = {isPaused: params.pause};
      ref = firebaseRef.child(deviceId).child('StartStop');
      break;
    case 'action.devices.commands.SetModes':
      state = {load: params.updateModeSettings.load};
      ref = firebaseRef.child(deviceId).child('Modes');
      break;
    // Add SetToggles command
    case 'action.devices.commands.SetToggles':
      state = {Turbo: params.updateToggleSettings.Turbo};
      ref = firebaseRef.child(deviceId).child('Toggles');
      break;
  }

Update QUERY response

Finally, you will need to update your QUERY response to report the washer's turbo mode. Add the updated changes to the queryFirebase and queryDevice functions to obtain the toggle state as stored in the Firebase database.

functions/index.js

const queryFirebase = async (deviceId) => {
  const snapshot = await firebaseRef.child(deviceId).once('value');
  const snapshotVal = snapshot.val();
  return {
    on: snapshotVal.OnOff.on,
    isPaused: snapshotVal.StartStop.isPaused,
    isRunning: snapshotVal.StartStop.isRunning,
    load: snapshotVal.Modes.load,
    // Add Toggles snapshot
    Turbo: snapshotVal.Toggles.Turbo,
  };
}

const queryDevice = async (deviceId) => {
  const data = queryFirebase(deviceId);
  return {
    on: data.on,
    isPaused: data.isPaused,
    isRunning: data.isRunning,
    currentRunCycle: [{ ... }],
    currentTotalRemainingTime: 1212,
    currentCycleRemainingTime: 301,
    currentModeSettings: {
      load: data.load,
    },
    // Add currentToggleSettings
    currentToggleSettings: {
      Turbo: data.Turbo,
    },
  };
};

Update Report State

Finally, update your reportstate function to report whether the washer is set to turbo to Home Graph.

functions/index.js

const requestBody = {
    requestId: 'ff36a3cc', /* Any unique ID */
    agentUserId: '123', /* Hardcoded user ID */
    payload: {
      devices: {
        states: {
          /* Report the current state of our washer */
          [context.params.deviceId]: {
            on: snapshot.OnOff.on,
            isPaused: snapshot.StartStop.isPaused,
            isRunning: snapshot.StartStop.isRunning,
            currentModeSettings: {
              load: snapshot.Modes.load,
            },
            // Add currentToggleSettings
            currentToggleSettings: {
              Turbo: snapshot.Toggles.Turbo,
            },
          },
        },
      },
    },
  };

Deploy to Firebase

Now deploy the updated functions.

firebase deploy --only functions

Click the refresh icon in the web UI to trigger a Request Sync once deployment completes.

You can now give a command to set the washer to turbo mode, or check if your washer is already in turbo mode.

"Turn on turbo for the washer"

"Is my washer in turbo mode?

Error handling within your smart home Action enables you to report to users when issues cause execute and query responses to fail. These notifications help create a more positive user experience for your customers as they interact with your smart device and Action.

Any time an execute or query request fails, your Action should return an error code. If for example you wanted to throw an error when a user attempts to start the washer with the lid open, your EXECUTE response would look like the following:

{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "payload": {
    "commands": [
      {
        "ids": [
          "456"
        ],
        "status": "ERROR",
        "errorCode": "deviceLidOpen"
      }
    ]
  }
}

Now when a user asks to start the washer, the Assistant responds as follows:

"The lid is open on the washer. Please close it and try again."

Exceptions are similar to errors, but indicate when an alert is associated with a command which may or may not block successful execution. An exception can provide related information using the StatusReport trait, such as battery level or recent state change. Non-blocking exception codes are returned along with a SUCCESS status, while blocking exception codes are returned with an EXCEPTIONS status.

An example response with an exception is as follows:

{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "payload": {
    "commands": [{
      "ids": ["123"],
      "status": "SUCCESS",
      "states": {
        "online": true,
        "isPaused": false,
        "isRunning": false,
        "exceptionCode": "runCycleFinished"
      }
    }]
  }
}

The Assistant responds as follows:

"The washer has finished running."

To add error reporting for your washer, open the functions/index.js file, and add the error class definition:

functions/index.js

app.onQuery(async (body) => {...});

// Add SmartHome error handling
class SmartHomeError extends Error {
  constructor(errorCode, message) {
    super(message);
    this.name = this.constructor.name;
    this.errorCode = errorCode;
  }
}

Update the execute response to return the error code and the error status:

functions/index.js

const executePromises = [];
  const intent = body.inputs[0];
  for (const command of intent.payload.commands) {
    for (const device of command.devices) {
      for (const execution of command.execution) {
        executePromises.push( ... )
            //Add error response handling
            .catch((error) => {
              console.error(`Unable to update ${device.id}.`, error);
              result.ids.push(device.id);
              if(error instanceof SmartHomeError) {
                result.status = 'ERROR';
                result.errorCode = error.errorCode;
              }
            })
        );
      }
    }
  }

The Google Assistant can now tell your users about any error code you report. Let's look at a specific example in the next section.

You should implement two factor authentication within your Action if your device has any modes that need to be secured or should be limited to a particular group of authorized users, such as a software update or lock disengage.

You can implement 2FA on all device types and traits, and can customize whether the security challenge occurs every time, or if specific criteria need to be met first.

There are three supported challenge types:

For this codelab, add an ackNeeded challenge to the command for turning on the washer, and the functionality to return an error if the 2FA challenge fails.

Open the functions/index.js file, and add an error class definition that returns both the error code and challenge type:

functions/index.js

class SmartHomeError extends Error { ... }

// Add 2FA error handling
class ChallengeNeededError extends SmartHomeError {
  constructor(tfaType) {
    super('challengeNeeded', tfaType);
    this.tfaType = tfaType;
  }
}

You also need to update the execution response to return the challengeNeeded error:

functions/index.js

const executePromises = [];
  const intent = body.inputs[0];
  for (const command of intent.payload.commands) {
    for (const device of command.devices) {
      for (const execution of command.execution) {
        executePromises.push( ... )
            .catch((error) => {
              console.error(`Unable to update ${device.id}.`, error);
              result.ids.push(device.id);
              if(error instanceof SmartHomeError) {
                result.status = 'ERROR';
                result.errorCode = error.errorCode;
                //Add 2FA error handling
                if(error instanceof ChallengeNeededError) {
                  result.challengeNeeded = {
                    type: error.tfaType
                  };
                }
              }
            })
        );
      }
    }
  }

Finally, modify updateDevice to require the explicit acknowledgment to turn the washer on or off.

functions/index.js

const updateDevice = async (execution,deviceId) => {
  const {challenge,params,command} = execution; //Add 2FA challenge
  let state, ref;
  switch (command) {
    case 'action.devices.commands.OnOff':
      //Add 2FA challenge
      if (!challenge || !challenge.ack) {
        throw new ChallengeNeededError('ackNeeded');
      }
      state = {on: params.on};
      ref = firebaseRef.child(deviceId).child('OnOff');
      break;
    ...
  }

  return ref.update(state)
        .then(() => state);
};

Deploy to Firebase

Next, deploy the updated function.

firebase deploy --only functions

After deploying the updated code, you must verbally acknowledge the action when you ask the Assistant to turn your washer on or off.

"Turn on the washer."

"Are you sure you want to turn on the washer?"

"Yes."

You can also see a detailed response for each step of the two-factor flow by opening up your Firebase logs.

Congratulations! You've successfully extended the features of Smart Home Actions through Modes and Toggles, and secured their execution through two-factor authentication. Here are some ideas you can implement to go deeper:

What we've covered