Будущее всё ближе. Лет 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

Комментарии (21)


  1. achekalin
    11.01.2018 12:04

    «Ok, Google, угони мне машину!»


  1. telobezumnoe
    11.01.2018 12:12

    делал себе управление освещением дома через ок гугл, с помощью таскера и автовойса, зачастую это удобнее чем заходить в приложение. до авто ещё не дошло, лень штатный иммобилайзер обходить.


    1. radist2s
      11.01.2018 14:33

      Там у старлайна вроде как более хитрая система с обходом иммобилайзера сделана: сам чип в машине не хранится, а используется модуль, который эмулирует чип, и модуль этот работает только если его запустила сама сигналка(то есть не тупо подать питание, а передача какого-то ключа), а сигнализации подает сигнал на запуск твой пульт, в который привязан к сигнализации. Другое дело — уязвимости в протоколе между сигнализацией и пультом, но тут уже зависит от модели сигнализации. Опять же есть потенциальная уязвимость в модуле иммобилайзера, атаковать его я думаю вполне возможно.


      1. ssh1 Автор
        11.01.2018 18:39

        Не, в моём случае это стандартный обходчик (чип от ключа в машине)


      1. alexpic
        11.01.2018 21:21

        Для некоторых марок, действительно, можно не хоронить ключ в машине, а «интеллектуально» обходить штатный иммобилайзер. Полный список автомобилей можно посмотреть по ссылке.

        Для реализации используются различные уязвимости, но отдельных модуль не нужен. Команды на запуск двигателя или имитации наличия ключа посылаются по CAN или LIN интерфейсу из основного блока охранной системы. У старых Тойот есть еще IMMO IMMI интерфейс — по сути обычный UART.

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


        1. radist2s
          11.01.2018 22:34

          Для реализации используются различные уязвимости

          Хотите сказать, что вы эксплуатируете уязвимости в конкретных марках авто? И так может сделать каждый?


          1. alexpic
            11.01.2018 23:23

            И так может сделать каждый?

            Теоретически да. В том числе — с целью угона. Погуглите на ютюбе угона нет — ребята снимают ролики про то, как угнать автомобиль (и не дать этого сделать в их установочных центрах). Там мало хардкора, контент все таки для широкой публики, но общее представление получите.


            1. radist2s
              12.01.2018 13:36

              То есть получается, что иммобилайзер вообще не является сдерживающей защитой, раз любой угонщик может использовать его уязвимости и завести автомобиль даже близко не имея на руках чипа?


              1. BiTHacK
                12.01.2018 14:09

                Иммобилайзер — защита от дилетанта. Посмотрите ролики в интернете, обычно иммобилайзер без наличия чипа обходится за мгновения либо добавлением в доверенные своего чипа, либо заменой ЭБУ на заранее подготовленный.


              1. alexpic
                12.01.2018 14:33

                Зависит от марки автомобиля. Популярные корейцы дырявые, V*G этого года пока побеждаются только с помощью специального железа между антенной замка зажигания и блоком иммобилайзера. Но для многих автомобилей нужно производить расчеты на удаленном вычислительном кластере, то есть иметь достаточно серьезную инфраструктуру. Стоимость угона возрастает и может стать невыгодной для злоумышленников.


  1. berezuev
    11.01.2018 12:32
    +2

    Это все удобно, но… Чем более «умная» сигнализация машины, тем больше у нее потенциальных точек, в которых могут быть дыры для угонщиков…

    Собственно, в свое время на люксовых машинах начал появляться бесключевой доступ… И в Ингушетии стало появляться много люксовых машин.


  1. xakepmega
    11.01.2018 14:13

    Единственный косяк, в данной версии он отвечает «Engine started» до реального запуска двигателя так как не успевает дождаться ответа от Starline.

    Можно изменить фразу на engine starts или trying start engine — тогда не будет косяка :-)


  1. CoffeeDrummer
    11.01.2018 18:15

    Лень — ты двигатель прогресса.
    Осталось реализовать все остальные датчики, основать стартап, и продаться гугл за много много денег.


  1. alexpic
    11.01.2018 20:47

    Да, пользуясь случаем, хочу обратиться к команде StarLine — ребята, почему бы не сделать нормальный API с документацией? Глядишь и интеграций со сторонними продуктами стало бы в разы больше?

    API для партнеров уже есть, некоторые мобильные приложения и сервисы его используют. Но мы его не афишируем, так как пока не достаточно ресурсов на поддержку. Ведь мало опубликовать доку, нужно людям помочь, если мы говорим о публичном продукте.

    Вашу статью сегодня бурно обсуждали, решили, что весной-летом этого года выпустим открытый API для всех DIY желающих. Так что спасибо :-)


    1. radist2s
      11.01.2018 21:10

      Если не сложно, можете прокомментировать мой комментарий и правильно ли я понимаю работу сигнализации с модулем обхода иммобилайзера, без физического присутствия чипа.


      1. alexpic
        11.01.2018 21:22

        Не сложно :-)


  1. shamanchik
    12.01.2018 08:44

    В интернетах, таки есть умельцы, которые некоторую функциональность API смогли «расковырять» и опубликовать (ищется на раз-два).
    Я вчера смог открыть/закрыть автомобиль используя POST запрос.


    1. ssh1 Автор
      12.01.2018 09:01

      Да, для автозапуска я использую именно этот подход.


      1. shamanchik
        12.01.2018 09:12

        Я не программист и не особо специалист, но имхо описанный Вами метод несколько отличается от такого:

        POST https://dev.starline.ru/json/device/xxxxxx/set_param
        Content-Type: application/json
        Cookie: slnet=xxxxxxxxxxxxx
        {"ign":1}
        


        Или же я просто не туда смотрю ?\_(?)_/?


        1. ssh1 Автор
          12.01.2018 09:43

           r = session.post('https://starlineonline.ru/device/{0}/executeCommand'.format(DEVICE_ID), {
                      'value': '1',
                      'action': 'ign',
                      'password': ''}
          

          Да, немного другой запрос, но суть та же.


  1. delvin-fil
    13.01.2018 01:30

    От жеш красота, да. «Ок гугыл, старт» и ЧЕТЫРЕ машины ревут противоугонками.
    ID старлайна как то учесть надо(если он там есть).
    Пока писал, не было, теперь увидел "(DEVICE_ID)". А может распространятся на несколько машин? Ну, скажем, по криворукости.