Build your «Smart device» for Google Home
David
Posted on August 12, 2022
Creating Google Home project
We will use Cloud-to-cloud integration method. So we need to navigate to Actions on Google console and create a new project:
And we need to name our Smart Home Action:
Set up OAuth2 and backend server
Cloud-to-cloud integration required OAuth2 server for linking your smart device service to Google Home application. In this article, we will implement it with Laravel framework and Passport package. You should pass base steps, like Laravel and Passport installation, database setup, and make fake users or you can find example code here. 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.
$ 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:
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:
# Request example
{
"requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
"inputs": [{
"intent": "action.devices.SYNC"
}]
}
# 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:
# Request example
{
"requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
"inputs": [{
"intent": "action.devices.QUERY",
"payload": {
"devices": [{
"id": "1"
}]
}
}]
}
# 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):
# 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:
# 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:
# 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:
php artisan make:model Thermostat -m
# 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');
}
};
# 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',
];
}
php artisan migrate
Implement Fulfillment URL route
# routes/api.php
<?php
use App\Http\Controllers\FulfillmentController;
use Illuminate\Support\Facades\Route;
Route::post('/', FulfillmentController::class);
<?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:
And create your device in your database:
Simplify the authentication process:
Since we are not interested in authentication details we can skip the implement login page and force the authenticated user:
# 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.
After linking your device will appear in Google Home:
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
Posted on August 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.