
Будущее всё ближе. Лет 10 назад я и не мог подумать, что буду заводить машину с помощью голосовой команды!
Последние годы я с интересом наблюдал за бурным развитием голосовых ассистентов. После выхода Google Home Mini, решил что и мне уже пора попробовать, так как цена стала более-менее адекватной для «игрушки». Первый проект — интеграция голосового помощника с GSM модулем StarLine для автозапуска, контроля координат, напряжения аккумулятора и других параметров, отдаваемых сигнализацией автомобиля. Итак, поехали?
Наличие Google Home не обязательно, всё описанное далее будет работать и с приложением Google Assistant на телефоне. У меня установлен GSM/GPS модуль StarLine M31, но должно работать со всеми GSM сигнализациями от StarLine.
Общая схема приложения для Google Assistant

- Google Home / Google Assistant отвечает за преобразование голоса в текст и обратно + взаимодействие со стандартными гугловскими сервисами. При вызове нашего приложения, Action в терминологии Google, запросы передаются на DialogFlow (API.AI на схеме).
- DialogFlow — отвечает за определение схемы диалога, обработку текста запросов на естественном языке, выделение сущностей, формирование ответов и взаимодействие с внешним миром с помощью вызова WebHook при необходимости.
- WebHook — WEB-сервис для взаимодействия с внешним миром. На вход подается ветка диалога (Intent) + параметры извлеченные из запроса (Entities). На выходе — ответ пользователю.
1. DialogFlow.com
Для начала нам надо создать приложение (agent) на dialogflow (бывший API.AI).
Регистрируемся с помощью Google аккаунта к которому у нас будет привязан Google Home.
К сожалению, русский язык пока не доступен для Google Assistant, выбираем английский.

Далее нам надо создать Intents. Intent в терминологии DialogFlow — одна из веток диалога отвечающая за определенное действие. В нашем случае это будут: GetBattery, GetTemperature, StartEngine, StopEngine. Так же существует Default Intent, срабатывающий в самом начале, обычно это приветствие и краткий рассказ о том, что можно делать с помощью данного приложения.
В каждом Intent нам необходимо указать примеры голосовых команд (User says), желательно по 5-10 разных вариантов.

Во всех Intents, кроме дефолтного, нам необходимо отправлять запросы к нашему скрипту (WebHook), поэтому ставим Fulfillment — Use webhook.

2. WebHook для взаимодействия с сервером Starline
Нам нужен скрипт который получает Intent из запроса от DialogFlow и дергает команды Starline. Быстрее всего у меня получилось реализовать это на Python+Flask.
Взаимодействие со StarLine взято отсюда + прочекано на актуальность снифером в браузере.
Для запуска на сервере я использовал gunicorn
gunicorn -b :3333 flask.starline:app
+ nginx в качестве реверс прокси.
Учтите, HTTPS обязателен!
starline.py
from flask import Flask, request from flask_restful import reqparse, Resource, Api, abort import requests import logging DEVICE_ID = 1234567 # Use HTTPS sniffer to find your DEVICE_ID in https://starline-online.ru/ traffic LOGIN = 'YOUR_STARLINE_EMAIL' PASS = 'YOUR_STARLINE_PASSWORD' logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') header = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest'} def start_engine(): with requests.Session() as session: t = session.get('https://starline-online.ru/', headers=header) login = session.post('https://starline-online.ru/user/login', { 'LoginForm[login]': LOGIN, 'LoginForm[pass]': PASS, 'LoginForm[rememberMe]': 'off'}, headers=header) logging.debug(login.content) r0 = session.get('https://starline-online.ru/device', headers=header) logging.debug(r0.content) r = session.post('https://starline-online.ru/device/{0}/executeCommand'.format(DEVICE_ID), { 'value': '1', 'action': 'ign', 'password': ''}, headers=header, timeout=1) logging.debug(r.status_code) logging.debug(r.content) logout = session.post('https://starline-online.ru/user/logout', { '': ''}, ) return ('Engine started!') def stop_engine(): with requests.Session() as session: t = session.get('https://starline-online.ru/', headers=header) login = session.post('https://starline-online.ru/user/login', { 'LoginForm[login]': LOGIN, 'LoginForm[pass]': PASS, 'LoginForm[rememberMe]': 'off'}, headers=header) logging.debug(login.content) r0 = session.get('https://starline-online.ru/device', headers=header) logging.debug(r0.content) r = session.post('https://starline-online.ru/device/{0}/executeCommand'.format(DEVICE_ID), { 'value': '0', 'action': 'ign', 'password': ''}, headers=header) logging.debug(r.status_code) logging.debug(r.content) logout = session.post('https://starline-online.ru/user/logout', { '': ''}, ) return ('Engine stopped!') def get_params(): with requests.Session() as session: t = session.get('https://starline-online.ru/', headers=header) login = session.post('https://starline-online.ru/user/login', { 'LoginForm[login]': LOGIN, 'LoginForm[pass]': PASS, 'LoginForm[rememberMe]': 'off'}, headers=header) logging.debug(login.content) r0 = session.get('https://starline-online.ru/device', headers=header) logging.debug(r0.content) res_dict = r0.json()['answer']['devices'][0] logout = session.post('https://starline-online.ru/user/logout', { '': ''}, ) return {'battery': res_dict['battery'], 'temperature': res_dict['ctemp']} def get_battery_text(): return ("Battery voltage {0} volts.".format(get_params()['battery'])) def get_temperature_text(): return ("Temperature: {0} degrees.".format(get_params()['temperature'])) app = Flask(__name__) app.config['BUNDLE_ERRORS'] = True api = Api(app) class ProccessGoogleRequest(Resource): def get(self): return {"status": "OK"} def post(self): req = request.get_json() logging.debug(request.get_json()) response = '' if req['result']['metadata']['intentName'] == 'GetBattery': response = get_battery_text() if req['result']['metadata']['intentName'] == 'GetTemperature': response = get_temperature_text() if req['result']['metadata']['intentName'] == 'StartEngine': response = start_engine() if req['result']['metadata']['intentName'] == 'StopEngine': response = stop_engine() if response == '': abort(400, message='Intent not detected') return {"speech": response, "displayText": response} api.add_resource(ProccessGoogleRequest, '/starline/') if __name__ == '__main__': app.run(debug=False)
Да, пользуясь случаем, хочу обратиться к команде StarLine — ребята, почему бы не сделать нормальный API с документацией? Глядишь и интеграций со сторонними продуктами стало бы в разы больше?
3. Тестируем в симуляторе и на реальном усройстве
Для тестирования в DialogFlow заходим в Integrations -> Google Assistant -> INTEGRATION SETTINGS -> Test и попадаем в симулятор Actions on Google

А вот и результат тестирования в реальном мире
Единственный косяк, в данной версии он отвечает «Engine started» до реального запуска двигателя так как не успевает дождаться ответа от Starline.
Идеи:
1. Запрос местоположения у Google Assistant, озвучивание расстояния до машины (Starline умеет отдавать координаты). Пока непонятно как для WebHook на Python запросить местоположение Google Home.
2. Упростить интеграцию Google <-> Starline, тогда отпадёт необходимость хардкодить пароль. Без участия со стороны Starline, как я понимаю, это не возможно.
Известные проблемы:
1. Google Assistant не успевает дождаться от сервера Starline ответа о статусе запуска двигателя
2. Пока при тестировании можно использовать только дефолтное имя приложения(Invocation) — Hey Google, talk to my test app.
Полезные ссылки:
1. Видео от Google
2. Пример с использованием Entities
