Вступление


На Гиктаймс уже была статья, посвященная разбору протокола чайника Redmond SkyKettle. Однако там речь шла о модели RK-M171S, здесь же речь пойдет о более функциональном G200S. В данной модели изменился протокол взаимодействия, из за чего подход автора предыдущей статьи уже не работает, а также появились дополнительные функции ночника и индикации текущей температуры цветом.

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

Всем, кому интересно, добро пожаловать под кат.

Проблемы и задачи


У данного чайника есть один большущий минус (кроме тех, что указал автор первой статьи): как только чайник снимаешь с подставки, происходит сброс текущего времени и, как следствие, невозможность использования расписания для кипячения чайника. По задумкам авторов сего творения, каждый раз после возвращения чайника на подставку ты должен запустить их фирменное приложение и синхронизировать чайник со смартфоном. Вот так вместо облегчения рутинных задач «умная» техника дрессирует нас совершать дополнительные действия. Но все изменилось, когда в доме появился HomeAssistant. Тогда я и решил разобраться в протоколе.

Инструменты


Я честно пытался декомпилировать и разобрать оригинальное приложение, но потерпел фиаско. Те инструменты, которые я использовал, не позволили мне понять логику работы чайника. Все процедуры и функции получались «кривыми», безымянными (по типу a, b, c и тд). Возможно, у меня не хватает опыта и умения. В итоге я пошел тем же путем, что и автор предыдущей статьи. Единственное существенное отличие в том, что я использовал интерактивный режим работы утилиты gatttool. Преимущество в том, что данный режим исключает всевозможные «гонки», о которых писал автор первой статьи.

Так как HomeAssistant написан на python, то все дальнейшие команды будем писать на нем. Для использования интерактивного режима работы gatttool на python нам поможет библиотека pexpect, позволяющая порождать сущности сторонних приложений и следить за их выводом (лихо загнул).

Практика


Про общее описание протокола обмена я вновь отправлю к автору первой статьи, поэтому без долгих прелюдий приступим к командам управления.

  1. Установка и разрыв соединения

    Устанавливаем соединение:

    child = pexpect.spawn("gatttool -I -t random -b " + mac, ignore_sighup=False)
    child.expect(r'\[LE\]>', timeout=3)
    child.sendline("connect")
    child.expect(r'Connection successful.*\[LE\]>', timeout=3)

    Здесь mac — мак адрес чайника.

    Разрываем соединение:

    child.sendline("exit")
  2. Подписка на уведомления

    После установления соединения перво-наперво нам надо подписаться на получение уведомлений от чайника. Без этого чайник будет воспринимать команды, однако не сможет ничего нам ответить, кроме текстового «Successfully».

    child.sendline("char-write-cmd 0x000c 0100")
    child.expect(r'\[LE\]>') 
  3. Авторизация

    child.sendline("char-write-req 0x000e 55" + iter + "ff" + key + "aa")
    child.expect("value: ")
    child.expect("\r\n")
    connectedStr = child.before[0:].decode("utf-8")
    answer = connectedStr.split()[3] # parse: 00 - no   01 - yes
    child.expect(r'\[LE\]>')

    Здесь и далее iter — целочисленная итерационная hex переменная от 0 до 64 (от 0 до 100 в десятичной системе). После каждой команды (как успешной, так и неуспешной) эту переменную следует увеличивать на 1, при достижении 64 она вновь сбрасывается на 0; key — hex 8 байт ключ авторизации (например: ffffffffffffffff).

    Пример ответа:
    value: 55 00 ff 01 aa
    Четвертый байт (01) означает, что чайник авторизовал вас, в противном случае ответ будет 00.
  4. Немного уличной магии
    После авторизации всегда отправляется «волшебный» запрос, суть которого мне не ясна. Есть теория, что он нужен для «удержания» состояния connected. Якобы если его не отправлять, то разрыв связи происходит в пределах секунды, и нужно начинать все заново. Если же его отправить, то таймаут существенно увеличивается, доходя до примерно десятка секунд. Достоверно подтвердить это я так и не смог.

    child.sendline("char-write-req 0x000e 55" + iter + "01aa")
    child.expect("value: ")
    child.expect("\r\n")
    child.expect(r'\[LE\]>')

    Пример ответа:
    value: 55 01 01 02 1d aa

    Во всех моих экспериментах ответ неизменно был таким.

    UPD: в комментариях подсказали, что никакая это не магия, а просто запрос версии ПО, в ответе соответственно именно эта версия и содержится. Таким образом, данный запрос можно вообще убрать за ненадобностью.
  5. Синхронизация
    Команда, синхронизирующая время в чайнике с часами сервера. У нее есть еще один эффект. В чайнике есть возможность показывать текущую температуру в режиме простоя миганием светодиода определенного цвета. Эта функция работает только после синхронизации. Описание самой функции см. в пункте 11.

    child.sendline("char-write-req 0x000e 55" + iter + "6e" + timeNow + tmz + "0000aa")
    child.expect("value: ")
    child.expect("\r\n")
    child.expect(r'\[LE\]>')

    Здесь tmz — часовой пояс в обратном hex формате (например, часовой пояс +3 переводим в секунды, затем в hex формат и получаем hex(3*60*60)=2a30, разбиваем по парам и выводим в обратном порядке 302a). Как быть с отрицательными часовыми поясами я не знаю, не тестировал, но есть подозрение, что за это отвечает следующий за tmz байт. Здесь timeNow — текущее время unixtime в обратном hex формате. Алгоритм такой же: получаем текущее время в секундах, переводим в HEX, разбиваем по парам и выводим строкой в обратном порядке.

    Пример ответа:
    value: 55 02 6e 00 aa
    Во всех моих экспериментах ответ неизменно был таким.
  6. Статистика
    В чайнике есть счетчик затраченной электроэнергии, общее время работы и количество пусков. Если кому то эти данные не нужны, можно смело пропускать данный пункт.

    child.sendline("char-write-req 0x000e 55" + iter + "4700aa")
    child.expect("value: ") 
    child.expect("\r\n") 
    statusStr = child.before[0:].decode("utf-8")
    Watts = hexToDec(str(statusStr.split()[11] + statusStr.split()[10] + statusStr.split()[9]))
    alltime = round(self._Watts/2200, 1)
    child.expect(r'\[LE\]>')
    
    child.sendline("char-write-req 0x000e 55" + iter + "5000aa")
    child.expect("value: ") 
    child.expect("\r\n") 
    statusStr = child.before[0:].decode("utf-8")
    times = hexToDec(str(statusStr.split()[7] + statusStr.split()[6]))
    child.expect(r'\[LE\]>')

    Watts — возвращает затраченную энергию в Вт*ч, alltime — время работы чайника в часах, times — количество запусков чайника. hexToDec — функция перевода в десятичный формат.
  7. Прочесть текущий режим работы

    child.sendline("char-write-req 0x000e 55" + iter + "06aa")
    child.expect("value: ") 
    child.expect("\r\n") 
    statusStr = child.before[0:].decode("utf-8")
    
    answer = statusStr.split()
    status = str(answer[11])
    temp = hexToDec(str(answer[8]))
    mode = str(answer[3])

    Пример ответа:
    value: 55 04 06 00 00 00 00 01 2a 1e 00 00 00 00 00 00 80 00 00 aa
    Четвертый байт — режим работы (mode): 00 — кипячение, 01 — нагрев до температуры, 03 — ночник. Шестой байт — hex температура, до которой нужно нагревать в режиме работы «нагрев», в режиме кипячения равен 00. Девятый байт — hex текущая температура воды (2a=42 по Цельсию). Двенадцатый байт — это состояние чайника: 00 — выключен, 02 — включен. Семнадцатый байт — это продолжительность работы чайника после достижения нужной температуры, по умолчанию равна 80 в hex (видимо, это какие то относительные единицы, точно не секунды).
  8. Записать текущий режим работы

    child.sendline("char-write-req 0x000e 55" + iter + "05" + mode + "00" + temp + "00000000000000000000" + howMuchBoil + "0000aa")
    child.expect("value: ")
    child.expect("\r\n")
    statusStr = child.before[0:].decode("utf-8")
    answer = statusStr.split()[3]
    child.expect(r'\[LE\]>')

    Параметр mode: 00 — кипячение, 01 — нагрев до температуры, 03 — ночник. Параметр temp — hex температура, до которой нужно нагревать в режиме работы «нагрев», в режиме кипячения он равен 00. Параметр howMuchBoil — это продолжительность работы чайника после достижения нужной температуры, по умолчанию равна 80 в hex (видимо, это какие то относительные единицы, точно не секунды).

    Пример ответа:
    value: 55 05 05 01 aa
    Четвертый байт ответа показывает успешность настроек: 01 — успешно, 00 — не успешно.
  9. Запустить текущий режим работы

    child.sendline("char-write-req 0x000e 55" + iter + "03aa")
    child.expect("value: ")
    child.expect("\r\n") 
    statusStr = self.child.before[0:].decode("utf-8")
    answer = statusStr.split()[3]
    child.expect(r'\[LE\]>')

    Пример ответа:
    value: 55 06 03 01 aa
    Четвертый байт ответа показывает успешность включения: 01 — успешно, 00 — не успешно.
  10. Остановить текущий режим работы

    child.sendline("char-write-req 0x000e 55" + iter + "04aa")
    child.expect("value: ") 
    child.expect("\r\n") 
    statusStr = self.child.before[0:].decode("utf-8")
    answer = statusStr.split()[3]
    child.expect(r'\[LE\]>')

    Пример ответа:
    value: 55 07 04 01 aa
    Четвертый байт ответа показывает успешность выключения: 01 — успешно, 00 — неуспешно.
  11. Отображение текущей температуры цветом в простое

    child.sendline("char-write-req 0x000e 55" + iter + "37c8c8" + onoff + "aa") # 00 - off, 01 - on
    child.expect("value: ")
    child.expect("\r\n") 
    child.expect(r'\[LE\]>')

    Параметр onoff равен либо 01 — включить функцию, либо 00 — выключить функцию.

    Пример ответа:
    value: 55 08 37 00 aa
    Во всех моих экспериментах ответ неизменно был таким.
  12. Записать палитру цветов различных режимов работы
    Устанавливается палитра соответствия между цветом светодиода и температурой в режиме отображения текущей температуры и режимах нагрева и кипячения, а также палитра цветов в режиме ночника.

    child.sendline("char-write-req 0x000e 55" + iter + "32" + boilOrLight + scale_from + rand + rgb1 + scale_mid + rand + rgb_mid + scale_to + rand + rgb2 + "aa") 
    child.expect("value: ")
    child.expect("\r\n")
    child.expect(r'\[LE\]>')

    Параметр boilOrLight равен 00, если мы настраиваем режим отображения текущей температуры или 01, если мы настраиваем режим ночника. Параметр scale_from указывает начало диапазона изменения цвета и равен 00 в режиме ночника и 28 в режиме отображения текущей температуры (28 — это 40 в десятичном формате и именно с этой температуры начнется плавное изменение цвета). Параметр scale_mid — это середина диапазона и равен 32 в режиме ночника и 46 в режиме отображения текущей температуры. Параметр scale_to указывает конец диапазона изменения цвета и равен 64 в обоих режимах. Параметр rgb1 — hex цвет начала палитры. Параметр rgb_mid — hex цвет середины палитры (я вычисляю его как середину между левым и правым концом, но теоретически можно задать любой цвет, это повлияет только на красивость и плавность смены цвета). Параметр rgb2 — hex цвет конца палитры. Параметр rand — некий параметр, значение которого я точно не понял, возможно, как то связан с яркостью цвета (примеры значений: e5, cc).

    Пример ответа:
    value: 55 09 32 00 aa
    Во всех моих экспериментах ответ неизменно был таким.
  13. Прочесть палитру цветов различных режимов работы

    child.sendline("char-write-req 0x000e 55" + iter + "33" + boilOrLight + "aa") 
    child.expect("value: ")
    child.expect("\r\n")
    statusStr = self.child.before[0:].decode("utf-8")
    child.expect(r'\[LE\]>')

    Параметр boilOrLight может быть 00 — если мы настраиваем режим отображения текущей температуры или 01 — если мы настраиваем режим ночника.

    Пример ответа:
    value: 55 10 33 01 00 7f 00 00 ff 32 7f 00 ff 00 64 7f ff 00 00 aa
    Здесь шестой, одиннадцатый и шестнадцатый байты (7f) это параметр rand из пункта 12. Пятый байт — scale_from, десятый байт — scale_mid, пятнадцатый байт — scale_to. Седьмой+восьмой+девятый байты — rgb_from. Двенадцатый+тринадцатый+четырнадцатый байты — rgb_mid. Семнадцатый+восемнадцатый+девятнадцатый байты — rgb_to.

Заключение


Как и обещал в конце статьи привожу ссылку на модуль для подключения к HomeAssistant.
Вот так выглядит чайник в интерфейсе HomeAssistant:





Для подключения данного модуля его необходимо скопировать в директорию «config_folder_homeassistant/custom_components/switch/», а также добавить в конфигурацию следующие строки:

switch:
  - platform: R4S_G200S
    mac: 'FF:FF:FF:FF:FF:FF' # insert your mac
    key: 'ffffffffffffffff' # generate your own 8 byte key
    scan_interval: 90

После этого необходимо перевести чайник в режим сопряжения и перезагрузить сервис homeassistant.

Между тем есть и нерешенные вопросы:

  1. Это далеко не все возможные команды для управления чайником, в дампе я видел еще кучу неопознанных команд чтения/записи, однако мне они не нужны. Для всех основных/видимых функций этих команд более чем достаточно. Возможно, кто-то сможет красиво разобрать фирменное приложение и подскажет все возможные команды или хотя бы выложит где-нибудь читаемый исходный код — буду рад.
  2. Есть некоторые проблемы и неясности в текущих командах (волшебный пакет, отрицательные часовые пояса, параметр rand в управлении цветами и тд). С ними по-хорошему нужно разобраться.
  3. Модуль чайника для HomeAssistant урезан в функциях (умеет отображать статистику, а также включаться-выключаться в режиме кипячения и использовать режим отображения температуры цветом в режиме простоя и нагрева), так как шаблоном для написания был модуль switch. По-хорошему, правильнее написать свой компонент ReadyForSky, а затем для этого компонента написать свой UI, учитывающий все режимы работы и дающий возможность управления всеми функциями чайника. Если есть опыт в написании своих компонент для HomeAssistant и желание помочь, то прошу в личку — буду рад помощи/содействию.

P.S.: Если есть интерес, то в следующей статье могу изложить, как я подключал SinoTrack ST-901 к HomeAssistant.

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


  1. saw_tooth
    29.05.2018 17:23

    Сожжено энергии

    Потрачено (с)
    А почему не угля?))


  1. mavrikk Автор
    29.05.2018 17:45

    Как всегда: готовишь ответы на все каверзные вопросы, прокручиваешь все возможные сценарии в голове, а тут — бац! И весь сценарий накрылся. К такому меня жизнь не готовила.
    А по факту: ваш вариант звучит конечно же более правильно )


    1. saw_tooth
      29.05.2018 17:50

      Я если честно, даже не сразу обратил внимание, а когда обратил — не сразу понял, что именно меня привлекло, то что сожжено энергии, или таки что размерность в Ваттах, а не в Джоулях)


      1. mavrikk Автор
        29.05.2018 18:01

        А вот это уже промах с моей стороны. Не отмажешься сказками про деревенский акцент)) Спасибо, что заметили… стыдно. кВт*ч конечно же.


        1. mavrikk Автор
          29.05.2018 18:16

          поправил


  1. NewStahl
    29.05.2018 18:02

    >SkyKettle
    Рассел задумчиво смотрит в пустую чашку…


    1. tvr
      29.05.2018 18:06

      Рассел задумчиво смотрит в пустую чашку…


      Количество запусков — 545 раз.



  1. halted
    29.05.2018 18:52

    И ни слова про блокчейн с майниногм.


  1. vtulin
    29.05.2018 18:57

    Ура! Наконец что-то полезное, а не очередной градусник.
    Спасибо за статью!


    1. mavrikk Автор
      30.05.2018 07:17

      Рад, что вам понравилось.


  1. wholeman
    29.05.2018 20:31

    Мне кажется, многим было бы понятнее, если бы протокол был описан в терминах GATT, а не только в виде примеров кода на Python, то есть, какие там службы и характеристики, что туда можно читать/писать и в каком формате.


    1. mavrikk Автор
      30.05.2018 07:29

      Совершенно согласен. Однако сделал это осознанно. Статья и без того получилась объемной, экономил как мог. К тому же она более практическая с уклоном применения в HomeAssistant. В самом начале я оставил ссылку на статью другого автора, там есть описание протокола в нужных вам терминах, там не так много поменялось. К тому же в моем коде за оберткой из pexpect довольно легко увидеть "голый" gatttool, например, легко видеть "хендлы", к которым обращаюсь, а также сами команды. Команды и ответы содержат последовательность байтов информации. Те байты, в которых я разобрался приведены с описаниями, чтобы их можно было правильно задать или прочесть.


  1. aamonster
    29.05.2018 22:59

    Спасибо, как раз купил на пробу два стартовых комплекта розетка+цоколь от Редмонд и думал, как их при случае в рамки стандарта загнать, чтобы докупать другие устройства от других производителей (вроде ни аналога реле sonoff, ни диммеров у Редмонд нету), а тут как раз впша статья — можно будет малой кровью справиться по аналогии.


    1. mavrikk Автор
      30.05.2018 07:38

      Да, я уже указывал в статье, что в идеале бы написать общий компонент Ready4Sky с возможностью подключения любого устройства из серии.
      Насчет малой крови — это как посмотреть. Разобравшись в официальном приложении получаешь все устройства серии, а описанный тут путь — это грубое решение
      конкретной задачи в лоб )) Если бы исходники официального приложения в удобоваримом виде получить...


  1. Sdima1357
    30.05.2018 01:10

    Разбираем протокол чайника


    И чайник шепнул утюгу:
    «Я дальше идти не могу»…
    Корней Чуковский


  1. Mox
    30.05.2018 01:39

    Господи, разбор протокола обмена данными с чайником.
    Жить в 2018 — это офигенно.


    1. darkdaskin
      30.05.2018 01:52

      Более того, на него ещё и обновления ставить нужно. У меня он из коробки не кипятил (отключался слишком рано), пришлось идти с ним в сервис, чтобы там подключились к нему телефоном (мой на тот момент не умел в BLE) и обновили прошивку.


      1. mavrikk Автор
        30.05.2018 07:45

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


  1. darkdaskin
    30.05.2018 01:41

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


    А ещё чайнику очень не хватает возможности узнать, сколько в нём осталось воды. Есть идея попробовать встроить в подставку весы с ESP8266 или чем-то подобным.


    1. mavrikk Автор
      30.05.2018 07:56

      Возможно, нестабильность — это не вина приложения, а вина аппаратной\программной части самого чайника. У меня в HomeAssistant тоже иногда проскакивают ошибки коннекта. Я специально для этого и ввел параметр "время обновления состояния", чтобы наблюдать за этим. Абсолютно не критично оказалось: все автоматизации, связанные с чайником, работают как часы! В моменты ручного запуска\остановки из интерфейса HomeAssistant тоже ни разу не подводил.
      По поводу датчика уровня — согласен, не хватает. В описании к чайнику было сказано, что он не будет кипятиться, если нет воды… я потратил вечер, чтобы найти этот параметр в запросах\ответе, пока не провел эксперимент. Реальность больно ударила меня: там тупо термореле стоит, а не датчик уровня… а я так надеялся. Если его включить пустым, то он включится и через несколько секунд его вырубит защита.


    1. mavrikk Автор
      30.05.2018 07:57

      Если идея осуществится — обязательно поделитесь ;)


  1. impalex
    31.05.2018 10:39

    Давно руки чесались «поковырять» его… а тут всё готовое, на блюдечке.

    Но довольно коварный чайник. :( У меня он часто отказывается самостоятельно отключаться, хотя вода вовсю кипит. Приходится следить за ним, без присмотра оставлять просто опасно. При желании в сети можно найти отзывы с такой же проблемой, так что случай, увы, не единичный.

    Плюс конфликтует с моноколесом. Софтина для управления колесом то и дело пытается подключиться к чайнику. :) Больше ни с одним девайсом такого не возникает. Возможно, что-то общее между ними есть.


    1. mavrikk Автор
      31.05.2018 10:45

      К счастью, никогда не сталкивался с подобным поведением. Особенно в плане не отключения после кипячения. Может проблема как раз с параметром, который у меня называется howMuchBoil? Не пробовали в фирменном приложении уменьшать его?


      1. impalex
        31.05.2018 10:52

        Этот баг весьма рандомный. Может отключиться, а может и не отключиться, какой-то зависимости от чего-либо замечено не было.
        В принципе, могу поэкспериментировать, конечно… Но из-за этой рандомности сложно судить о результатах.

        edit: сейчас что-то подумалось, может датчик температуры глючит как-то… что-то я не догадался по приложению глянуть.


  1. Drakosvlad
    31.05.2018 10:39

    Вот так HTTP 418 из невинной шутки превращается в самый обычный статус-код…


  1. JillDow
    31.05.2018 10:39

    Очевидно же, что
    > 55 01 01 02 1d aa
    — это версия ПО (2.29) — и никаких там таймаутов на дисконнект.

    > Здесь tmz — часовой пояс в обратном hex формате (например, часовой пояс +3 переводим в секунды, затем в hex формат и получаем hex(3*60*60)=2a30, разбиваем по парам и выводим в обратном порядке 302a)

    мозг взорвался. называется просто «little-endian».


    1. mavrikk Автор
      31.05.2018 10:42

      Вот и нет больше уличной магии.
      Спасибо за находку ) Этот запрос можно вообще убрать из модуля.
      Насчет «little-endian» утверждать не буду, но данным термином не пользовался, так как не весь ответ от чайника соответствует данной концепции, а лишь отдельные байты. Возможно, не прав.