Build your «Smart device» for Google Home
Tags: php, laravel, googlehome
Published: 2022-08-12 17:21:05

---
title: Build your «Smart device» for Google Home
published: true
description: In this article, we will create a "smart thermostat" for Google Home Application
tags: php, laravel, googlehome
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/31043oinwu49tichqwhy.png
 
# Use a ratio of 100:42 for best results.
---
## Creating Google Home project
We will use [Cloud-to-cloud](https://developers.home.google.com/cloud-to-cloud) integration method. So we need to navigate to [Actions on Google](https://console.actions.google.com/u/0/) console and create a new project:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jcp82r643bn1pua3djc3.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pyslbkgekwc46f74u0hn.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2lwnh8gjfnhcvhnaoob.png)
And we need to name our Smart Home Action:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2io3feakv27zwbnv4gb5.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f3k5br1nseft9ntbmrs0.png)
## Set up OAuth2 and backend server
[Cloud-to-cloud](https://developers.home.google.com/cloud-to-cloud) integration required [OAuth2](https://oauth.net/2/) server for linking your smart device service to Google Home application. In this article, we will implement it with [Laravel framework](https://laravel.com/) and [Passport package](https://laravel.com/docs/9.x/passport). You should pass base steps, like [Laravel](https://laravel.com/) and [Passport](https://laravel.com/docs/9.x/passport) installation, database setup, and make fake users or you can find example code [here](https://github.com/Davidnadejdin/smarthome-demo). All additional actions will be described next.
## Account linking
Move to your Laravel project and run the command to generate OAuth Client Information for Google Home.
```bash
$ php artisan passport:client

Which user ID should the client be assigned to?:
> 1

What should we name the client?:
> Google

Where should we redirect the request after authorization?:
> https://oauth-redirect.googleusercontent.com/r/{your project id}

New client created successfully.
Client ID: 9700039b-92b7-4a79-a421-152747b9a257
Client secret: 813PEwdTAq7kf7vRXuyd75dJEaSzAIZ1GDWjIyRM
```
Pass received data and oauth2 endpoints to Account Linking settings in your project:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3c3k7fm6czo6ilzusnkj.png)
## Implement backend
To notify Google Home about our device status, we need to return their data when Google Home requested data by Fulfillment URL. Google Home sends data in 3 types: SYNC, QUERY, and EXECUTE. 

Response for sync request will return a list of all devices and their capabilities:
```json
# Request example
{
    "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
    "inputs": [{
      "intent": "action.devices.SYNC"
    }]
}
```
```json
# Response example
{
    "requestId": "6894439706274654512",
    "payload": {
        "agentUserId": "user123",
        "devices": [
            {
                "id": 1,
                "type": "action.devices.types.THERMOSTAT",
                "traits": [
                    "action.devices.traits.TemperatureSetting"
                ],
                "name": {
                    "name": "Thermostat"
                },
                "willReportState": true,
                "attributes": {
                    "availableThermostatModes": [
                        "off",
                        "heat",
                        "cool"
                    ],
                    "thermostatTemperatureRange": {
                        "minThresholdCelsius": 18,
                        "maxThresholdCelsius": 30
                    },
                    "thermostatTemperatureUnit": "C"
                },
                "deviceInfo": {
                    "manufacturer": "smart-home-inc"
                }
            }
        ]
    }
}
```
Resposne for query request must include a full set of states for each of the traits supported by the requested devices:
```json
# Request example
{
    "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
    "inputs": [{
        "intent": "action.devices.QUERY",
        "payload": {
            "devices": [{
                "id": "1"
            }]
        }
    }]
}
```
```json
# Response example
{
    "requestId": "6894439706274654514",
    "payload": {
        "agentUserId": "user123",
        "devices": {
            "1": {
                "status": "SUCCESS",
                "online": true,
                "thermostatMode": "cool",
                "thermostatTemperatureSetpoint": 23,
                "thermostatTemperatureAmbient": 10,
                "thermostatHumidityAmbient": 10
            }
        }
    }
}
```
Execute request contain data as same as Query but can contain commands given to a group of devices(response is same as Query):

```json
# request example
{
    "inputs": [
        {
            "context": {
                "locale_country": "US",
                "locale_language": "en"
            },
            "intent": "action.devices.EXECUTE",
            "payload": {
                "commands": [
                    {
                        "devices": [
                            {
                                "id": "1"
                            }
                        ],
                        "execution": [
                            {
                                "command": "action.devices.commands.ThermostatTemperatureSetpoint",
                                "params": {
                                    "thermostatTemperatureSetpoint": 25.5
                                }
                            }
                        ]
                    }
                ]
            }
        }
    ],
    "requestId": "15039538743185198388"
}
```
## Understanding device object
A device object on Sync request must contain information about a device like its name, device info, including traits, and attributes based on included trains:
```json
# Device object on sync request
{
    "id": 1,
    "type": "action.devices.types.THERMOSTAT",
    "traits": [
        "action.devices.traits.TemperatureSetting"
    ],
    "name": {
        "name": "Thermostat"
    },
    "willReportState": true,
    "attributes": {
        "availableThermostatModes": [
            "off",
            "heat",
            "cool"
        ],
        "thermostatTemperatureRange": {
            "minThresholdCelsius": 18,
            "maxThresholdCelsius": 30
        },
        "thermostatTemperatureUnit": "C"
    },
    "deviceInfo": {
        "manufacturer": "smart-home-inc"
    }
}
```
And must contain a device state on Quest or Execute:
```json
# Device object on Query or Execute request
{
    "status": "SUCCESS",
    "online": true,
    "thermostatMode": "cool",
    "thermostatTemperatureSetpoint": 24,
    "thermostatTemperatureAmbient": 10,
    "thermostatHumidityAmbient": 10
}
```

## Return and save device state
Move to your Laravel project and create a Thermostat model:
```bash
php artisan make:model Thermostat -m
```
```php
# database/migrations/2022_08_11_154357_create_thermostats_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('thermostats', function (Blueprint $table) {
            $table->id();
            $table->boolean('online')->default(false);
            $table->string('mode');
            $table->unsignedInteger('current_temperature')->default(0);
            $table->unsignedInteger('expected_temperature')->default(15);
            $table->unsignedInteger('humidity')->default(0);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('thermostats');
    }
};
```
```php
# app/Models/Thermostate.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Thermostat extends Model
{
    protected $fillable = [
        'online',
        'mode',
        'current_temperature',
        'expected_temperature',
        'humidity',
    ];

    protected $casts = [
        'online' => 'boolean',
    ];
}
```
```bash
php artisan migrate
```

## Implement Fulfillment URL route
```php
# routes/api.php

<?php

use App\Http\Controllers\FulfillmentController;
use Illuminate\Support\Facades\Route;

Route::post('/', FulfillmentController::class);
```
```php
<?php

namespace App\Http\Controllers;

use App\Models\Thermostat;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;

class FulfillmentController extends Controller
{
    public function __invoke(Request $request)
    {
        $response = null;

        // Extract request type
        switch ($request->input('inputs.0.intent')) {
            case 'action.devices.QUERY':
                $response = $this->queryResponse();
                break;
            case 'action.devices.SYNC':
                $response = $this->syncRequest();
                break;
            case 'action.devices.EXECUTE':
                $response = $this->syncExecute($this->syncExecute($request->input('inputs.0.payload.commands'))); // Extract list of commands
                break;
        }

        return $response;
    }

    private function queryResponse()
    {
        $devices = [];

        // Extract our devices states
        foreach (Thermostat::all() as $thermostat) {
            $devices[$thermostat->id] = [
                'status' => 'SUCCESS',
                'online' => $thermostat->online,
                'thermostatMode' => $thermostat->mode,
                'thermostatTemperatureSetpoint' => $thermostat->expected_temperature,
                'thermostatTemperatureAmbient' => $thermostat->current_temperature,
                'thermostatHumidityAmbient' => $thermostat->humidity,
            ];
        }

        return response([
            'requestId' => "6894439706274654514",
            'payload' => [
                "agentUserId" => "user123",
                'devices' => $devices,
            ],
        ]);
    }

    private function syncRequest()
    {
        $devices = [];

        // Define our devices
        foreach (Thermostat::all() as $thermostat) {
            $devices[] = [
                'id' => $thermostat->id,
                'type' => "action.devices.types.THERMOSTAT",
                'traits' => [
                    "action.devices.traits.TemperatureSetting"
                ],
                'name' => [
                    'name' => 'Thermostat'
                ],
                'willReportState' => true,
                'attributes' => [
                    'availableThermostatModes' => [
                        'off',
                        'heat',
                        'cool',
                    ],
                    'thermostatTemperatureRange' => [
                        'minThresholdCelsius' => 18,
                        'maxThresholdCelsius' => 30,
                    ],
                    'thermostatTemperatureUnit' => 'C'
                ],
                'deviceInfo' => [
                    'manufacturer' => 'smart-home-inc',
                ],
            ];
        }

        return response([
            'requestId' => "6894439706274654512",
            'payload' => [
                "agentUserId" => "user123",
                'devices' => $devices,
            ],
        ]);
    }

    private function syncExecute(array $commands)
    {
        foreach ($commands as $command) {
            // Get devices for execute command
            $thermostats = Thermostat::whereIn('id', Arr::pluck($command['devices'], 'id'))->get();

            foreach ($command['execution'] as $executionItem) {
                switch ($executionItem['command']) {
                    // Handle set point command and save it in our model
                    case 'action.devices.commands.ThermostatTemperatureSetpoint':
                        foreach ($thermostats as $thermostat) {
                            $thermostat->update([
                                'expected_temperature' => $executionItem['params']['thermostatTemperatureSetpoint'],
                            ]);
                        }
                        break;
                    // Handle set set mode command and save it in our model
                    case 'action.devices.commands.ThermostatSetMode':
                        foreach ($thermostats as $thermostat) {
                            $thermostat->update([
                                'mode' => $executionItem['params']['thermostatMode'],
                            ]);
                        }
                        break;
                }
            }
        }

        // It not necessary to return data for command request
        return response([]);
    }
}
```
Return to Console actions and set your Fulfillment URL in your project settings:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vzqe6ok9m9zh5fw5k996.png)
And create your device in your database: 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1iqfuxfn4adhf06avub0.png)
## Simplify the authentication process: 
Since we are not interested in authentication details we can skip the implement login page and force the authenticated user:
```php
# app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
    }

    public function boot()
    {
        Auth::setUser(User::first());
    }
}
```

## Link backend with Google Home
After previous actions, you can test your backend with Google Home app which is authenticated as the same account used for creating Actions Console project.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yh5m415dwi1jumobc9r6.png)
After linking your device will appear in Google Home:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6f2fh6mbwfgtx110pmqx.png)
Now you can control your "device" and its state will be saved on the database. You can trigger sync(reconnect account), query(swipe update gesture), and execute(try to change mode). 
 
You can find more about available device types and traits [Here](https://developers.google.com/assistant/smarthome/concepts/devices-traits)