Создайте свое «умное устройство» для Google Home


Создание проекта 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:

Теперь вы можете управлять своим «устройством», а его состояние будет сохранено в базе данных. Вы можете запускать синхронизацию (переподключение аккаунта), запрос (жест обновления) и выполнение (попытка изменить режим).

Вы можете узнать больше о доступных типах устройств и их характеристиках здесь

Оцените статью
devanswers.ru
Добавить комментарий