Создание проекта Google Home
Мы будем использовать метод облачной интеграции. Поэтому нам нужно перейти в раздел Actions на консоли Google и создать новый проект:
И нам нужно назвать наш проект «Умный дом»:
Настроить OAuth2 и внутренний сервер
Облачная интеграция требует наличия сервера OAuth2 для связи службы умных устройств с приложением Google Home. В этой статье мы реализуем ее с помощью фреймворка Laravel и пакета Passport. Вы должны пройти базовые шаги, такие как установка Laravel и Passport, настройка базы данных и создание фальшивых пользователей, или вы можете найти пример кода здесь. Все дополнительные действия будут описаны далее.
Привязка учетной записи
Перейдите в свой проект Laravel и выполните команду для генерации информации клиента OAuth для 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
Передайте полученные данные и конечные точки oauth2 в настройки Account Linking в вашем проекте:
Реализация бэкенда
Чтобы уведомлять Google Home о состоянии нашего устройства, нам нужно возвращать данные о нем, когда Google Home запрашивает данные по Fulfillment URL. Google Home отправляет данные 3 типов: SYNC, QUERY и EXECUTE.
Ответ на запрос синхронизации вернет список всех устройств и их возможности:
# 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"
}
}
]
}
}
Ответ на запрос запроса должен включать полный набор состояний для каждого из признаков, поддерживаемых запрашиваемыми устройствами:
# 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
}
}
}
}
Запрос на выполнение содержит те же данные, что и запрос, но может содержать команды, отдаваемые группе устройств (ответ такой же, как и на запрос):
# 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"
}
Понимание объекта устройства
Объект устройства в запросе Sync должен содержать информацию об устройстве, такую как его имя, информация об устройстве, включая характеристики и атрибуты, основанные на включенных трейнах:
# 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"
}
}
И должен содержать состояние устройства при запросе Quest или Execute:
# Device object on Query or Execute request
{
"status": "SUCCESS",
"online": true,
"thermostatMode": "cool",
"thermostatTemperatureSetpoint": 24,
"thermostatTemperatureAmbient": 10,
"thermostatHumidityAmbient": 10
}
Вернитесь и сохраните состояние устройства
Перейдите в свой проект Laravel и создайте модель термостата:
php artisan make:model Thermostat -m
# database/migrations/2022_08_11_154357_create_thermostats_table.php
<?php
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
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 AppModels;
use IlluminateDatabaseEloquentModel;
class Thermostat extends Model
{
protected $fillable = [
'online',
'mode',
'current_temperature',
'expected_temperature',
'humidity',
];
protected $casts = [
'online' => 'boolean',
];
}
php artisan migrate
Внедрите URL-маршрут для доставки товара
# routes/api.php
<?php
use AppHttpControllersFulfillmentController;
use IlluminateSupportFacadesRoute;
Route::post('/', FulfillmentController::class);
<?php
namespace AppHttpControllers;
use AppModelsThermostat;
use IlluminateHttpRequest;
use IlluminateSupportArr;
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([]);
}
}
Вернитесь к действиям Console и установите ваш Fulfillment URL в настройках проекта:
И создайте устройство в вашей базе данных:
Упростите процесс аутентификации:
Поскольку нас не интересуют детали аутентификации, мы можем пропустить страницу входа в систему и принудительно ввести аутентифицированного пользователя:
# app/Providers/AppServiceProvider.php
<?php
namespace AppProviders;
use AppModelsUser;
use IlluminateSupportFacadesAuth;
use IlluminateSupportServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
//
}
public function boot()
{
Auth::setUser(User::first());
}
}
Свяжите бэкэнд с Google Home
После предыдущих действий вы можете протестировать ваш бэкенд с помощью приложения Google Home, которое аутентифицировано как та же учетная запись, которая использовалась для создания проекта Actions Console.
После связывания ваше устройство появится в Google Home:
Теперь вы можете управлять своим «устройством», а его состояние будет сохранено в базе данных. Вы можете запускать синхронизацию (переподключение аккаунта), запрос (жест обновления) и выполнение (попытка изменить режим).
Вы можете узнать больше о доступных типах устройств и их характеристиках здесь