Google Smart Home Assistant Cloud-To-Cloud connection with Azure Function

Bartłomiej Leja
11 min readMay 21, 2023

--

https://developers.home.google.com/cloud-to-cloud/get-started

If you are interested in smart home devices, you might have heard of Google Smart Home Assistant. This is a platform that allows you to control compatible devices with your voice or your phone. But did you know that Google also offers a Cloud-To-Cloud (C2C) integration option for device makers? In this article, we will follow the official Cloud-To-Cloud code lab with one major difference. We will use Azure Functions instead of Google Functions. You can ask yourself why do such weird thing and the answer is pretty easy, MONEY. In order to finish this tutorial you need to upgrade your Firebase free plan to paid one. In most cases, you will not use up your limits in a paid plan. So in the end it will be still free. But if you do not want to spread your credit card data across the whole internet and for example, you have free Azure cloud credits. Or you are just a fanboy of Azure you can follow this tutorial 😁

Prerequisites

  • Google account
  • Google Home application
  • Free Firebase account
  • Possibility to create Azure Functions

The aim of this article is to follow this code lab but, instead of Google Functions use Azure Functions. In order to do that we need to create 4 cloud resources:

  • Google Action Project (place where magic happens)
  • Firebase Realtime Database (store IoT device state)
  • Azure Function with four functions (webhook for sync, query, execute and disconnect actions)
  • (Optional) A web dashboard to display the real-time status of our Iot device (visualization of the IoT device state)

Google Action Project

Enable Activity controls

In order to use Google Assistant, you must share certain activity data with Google. The Google Assistant needs this data to function properly; however, the requirement to share data is not specific to the SDK. To share this data, create a Google account if you don’t already have one. You can use any Google account — it does not need to be your developer account.

Open the Activity Controls page for the Google account that you want to use with the Assistant.

Ensure the following toggle switches are enabled:

  • Web & App Activity — In addition, be sure to select the Include Chrome history and activity from sites, apps, and devices that use Google services checkbox.
  • Device Information
  • Voice & Audio Activity

You will need the same Google account on your phone and in the Actions console to fully run this project.

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.
https://developers.home.google.com/cloud-to-cloud/get-started

Select the Smart Home App

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

https://developers.home.google.com/codelabs/smarthome-washer#1

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

Google Firebase resources => RealtimeDatabase

We are creating Realtime database in order to store the state of our IoT device. Additionally, we can listen for changes on db and send this information to the Google project. Go to your Firebase console and create a new project.

Click add project

Provide the project name and click continue, in the second step click continue

In step 3 click Create Project button

After project creation go to the newly created project and click Build Section after that choose RealtimeDatabase from the list.

Now you can click Create Database button

In the second step choose Start in Test mode. Thanks to that all internet resources can use your DB if they have needed credentials.

In the end, click Enable. Thanks to that we have a place to store IoT device state. Additionally, real-time database is sending notifications when something was changed on the db so we can send that information to the Google project.

Azure Function

We need to create an Azure function with four functions

  • fakeauth
  • faketoken
  • login
  • smarthome

Listed functions are similar to methods in this index.js file

Smarthome function consists following intents:

  • A SYNC intent occurs when the Assistant wants to know what devices the user has connected. This is sent to your service when the user links an account. You should respond with a JSON payload of all the user's devices and their capabilities.
  • A QUERY intent occurs when the Assistant wants to know the current state or status of a device. You should respond with a JSON payload with the state of each requested device.
  • An EXECUTE intent occurs when the Assistant wants to control a device on a user's behalf. You should respond with a JSON payload with the execution status of each requested device.
  • A DISCONNECT intent occurs when the user unlinks their account from the Assistant. You should stop sending events for this user's devices to the Assistant.

In order to create Azure Function follow this article. In this tutorial, I will use visual studio code approach.

Your Functions tree in vs code should look like that:

And in file explorer, it should look like that:

First, we need to add some dependencies to package.json, so go to package.json and add the missing dependencies, it should look like that.

{
"name": "smart-washer-azure-function",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "func start",
"test": "echo \"No tests yet...\""
},
"dependencies": {
"actions-on-google": "^2.12.0",
"cors": "^2.8.5",
"firebase-admin": "^8.0.0",
"firebase-functions": "^3.8.0",
"googleapis": "^43.0.0"
},
"devDependencies": {
"eslint": "^6.3.0",
"eslint-config-google": "^0.14.0"
}
}

After saving the package.json execute npm install in a folder with package.json file.

OK now we will add code to our functions. First, we will handle fakeauth.

In order to do that open fakeauth folder like this

And add to index.js this code

'use strict';

const util = require('util');

module.exports = function fakeauth(context, req) {
const responseurl = util.format('%s?code=%s&state=%s', decodeURIComponent(req.query.redirect_uri), 'xxxxxx',req.query.state);

context.log(`Set redirect as ${responseurl}`);

let redirect = `login?responseurl=${encodeURIComponent(responseurl)}`;

context.res.status(302)
context.res.header('Location', redirect)

return context.done()
};

After that go to faketoken folder and add this code into index.js

module.exports = async function (context, req) {
const grantType = 'authorization_code';

// TODO find out from where I get grant_type t
// grantType = request.query.grant_type ?
// request.query.grant_type : request.body.grant_type;

const secondsInDay = 86400; // 60 * 60 * 24
const HTTP_STATUS_OK = 200;
context.log(`Grant type ${grantType}`);

let obj;
if (grantType === 'authorization_code') {
obj = {
token_type: 'bearer',
access_token: '123access',
refresh_token: '123refresh',
expires_in: secondsInDay,
};
}
else if (grantType === 'refresh_token') {
obj = {
token_type: 'bearer',
access_token: '123access',
expires_in: secondsInDay,
};
}

context.log(obj);

context.res = {
status: HTTP_STATUS_OK, /* Defaults to 200 */
body: obj
};
}

Login function

module.exports = async function (context, req) {

if (req.method === 'GET') {
context.log('Requesting login page');
let response = `
<html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<body>
<form action="login" method="post">
<input type="hidden"
name="responseurl" value="${req.query.responseurl}" />
<button type="submit" style="font-size:14pt">
Link this service to Google
</button>
</form>
</body>
</html>
`
context.res = {
headers: { 'Content-Type': 'text/html' },
body: response
};

} else if (req.method === 'POST') {
// Here, you should validate the user account.
// In this sample, we do not do that.
const responseurl = decodeURIComponent(req.body.substring(12));
context.log(`Redirect to ${responseurl}`);

context.res.status(302);
context.res.header('Location', responseurl);
return context.done();
} else {
// Unsupported method
context.send(405, 'Method Not Allowed');
}
}

And finally crème de la crème smarthome function. Remeber to change database url in line number 10.

'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');

var serviceAccount = require("./serviceAccountKey.json");

if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://{yourAdress}.firebaseio.com"
});
}

const firebaseRef = admin.database().ref('/');

module.exports = async function (context, req) {
// Hardcoded user ID
const USER_ID = '124';

let intent = req.body.inputs[0].intent;

context.log(intent);

if(intent === 'action.devices.SYNC') {
let resp = {
requestId: req.body.requestId,
payload: {
agentUserId: USER_ID,
devices: [{
id: 'washer',
type: 'action.devices.types.WASHER',
traits: [
'action.devices.traits.OnOff',
'action.devices.traits.StartStop',
'action.devices.traits.RunCycle',
],
name: {
defaultNames: ['My Washer'],
name: 'Washer',
nicknames: ['Washer'],
},
deviceInfo: {
manufacturer: 'Acme Co',
model: 'acme-washer',
hwVersion: '1.0',
swVersion: '1.0.1',
},
willReportState: true,
attributes: {
pausable: true,
},
}],
},
};

context.log(resp);
return context.res = {
status: 200, /* Defaults to 200 */
body: resp
};
}

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,
};
};

const queryDevice = async (deviceId) => {
const data = await queryFirebase(deviceId);
return {
on: data.on,
isPaused: data.isPaused,
isRunning: data.isRunning,
currentRunCycle: [{
currentCycle: 'rinse',
nextCycle: 'spin',
lang: 'en',
}],
currentTotalRemainingTime: 1212,
currentCycleRemainingTime: 301,
};
};

if(intent === 'action.devices.QUERY') {
let body =req.body;
context.log(body);
const {requestId} = body;
const payload = {
devices: {},
};
const queryPromises = [];
const intent = body.inputs[0];
for (const device of intent.payload.devices) {
context.log(device);
const deviceId = device.id;
queryPromises.push(
queryDevice(deviceId)
.then((data) => {
// Add response to device payload
payload.devices[deviceId] = data;
})
);
}

// Wait for all promises to resolve
await Promise.all(queryPromises);
let resp = {
requestId: requestId,
payload: payload,
};

context.log(resp);

return context.res = {
status: 200, /* Defaults to 200 */
body: resp
};
}

const updateDevice = async (execution, deviceId) => {
const {params, command} = execution;
let state; let 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;
}

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

if(intent === 'action.devices.EXECUTE') {
let body = req.body;
const {requestId} = body;
// Execution results are grouped by status
const result = {
ids: [],
status: 'SUCCESS',
states: {
online: true,
},
};

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(
updateDevice(execution, device.id)
.then((data) => {
result.ids.push(device.id);
Object.assign(result.states, data);
})
.catch(() => functions.logger.error('EXECUTE', device.id)));
}
}
}

await Promise.all(executePromises);

let resp = {
requestId: requestId,
payload: {
commands: [result],
},
};

context.log(resp);

return context.res = {
status: 200, /* Defaults to 200 */
body: resp
};
}

if(intent === 'action.devices.DISCONNECT') {
context.log('User account unlinked from Google Assistant');

return context.res = {
status: 200, /* Defaults to 200 */
body: null
};
}
}

In order to run this function we need to add real-time database configuration. This configuration will be placed in serviceAccountKey.json file like in this photo

We can get the configuration from Firebase console. Go to Firebase project with realtime database next click the gear icon go to Service accounts tab and click Generate new private key button.

Next, rename the configuration file to the serviceAccountKey.json and place it into smarthome (or whatever you called function with all the intents) folder.

Deploy all functions to Azure Cloud and collect the function's URLs.

Google Action Project => configuration

In created Google action under Overview => Build your Action section, select Add Action(s). Enter the URL for your cloud function that provides fulfillment for the smart home intents and click Save.

https://{yourAddresOfSmarthomeFunction}/api/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://{yourAddresOffakeauthFunction}/api/fakeauth

Token URL

https://{yourAddresOffaketokenFunction}/api/faketoken

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

Now everything should be set up to register your new IoT device in Google smart home application.

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.

Important: This codelab includes an implementation of account linking that does not actually check user credentials. In a production system, you should implement the OAuth 2.0 protocol to keep devices secure.

  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.

(Optional) A web dashboard to display the real-time status of our Iot device

Clone this repo after that remove function folder and database.rules.json from washer-done folder. Replace code in firebase.json for this

{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

After that deploy this site to Firebase project with realtime database created previously. If you do not know how to do it here is instruction.

After deployment, you should have a site like this

You can change washer state in Google smart home app and on this site, you should see changes. And this is the END of our beautiful journey 😋

As we can see the road to achieve Cloud to Cloud connection with GoogleSmart Home using Azure Function is long but it is possible. I recommend comparing the code in individual functions with the originals from Google documentation. The big change here is just not using SmartHome lib from Google. The only thing that is not implemented like in the documentation is Report State.

If you enjoy the instruction please follow me for more interesting topics, if you have any questions please do not hesitate to ask 😋

--

--

Bartłomiej Leja
Bartłomiej Leja

Written by Bartłomiej Leja

I am a programming enthusiast. Mainly focus on C# and Javascripts programming languages. In my free time, I like to ride a bike and listen to music.

No responses yet