https://habrahabr.ru/post/346120/- Разработка для интернета вещей
- Программирование
- Python
- Google API
Будущее всё ближе. Лет 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.pyfrom 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