- PVSM.RU - https://www.pvsm.ru -
На Гиктаймс уже была статья [1], посвященная разбору протокола чайника Redmond SkyKettle. Однако там речь шла о модели RK-M171S, здесь же речь пойдет о более функциональном G200S. В данной модели изменился протокол взаимодействия, из за чего подход автора предыдущей статьи уже не работает, а также появились дополнительные функции ночника и индикации текущей температуры цветом.
В данной статье я приведу результаты разбора протокола с примерами кода на python (если кто захочет разработать свой модуль/приложение для управления чайником). Также в конце статьи ссылка на готовый модуль для подключения чайника к HomeAssistant (это мой первый опыт написания на python после прохождения онлайн-курса, так что данный модуль можно и даже нужно улучшать).
Всем, кому интересно, добро пожаловать под кат.
У данного чайника есть один большущий минус (кроме тех, что указал автор первой статьи): как только чайник снимаешь с подставки, происходит сброс текущего времени и, как следствие, невозможность использования расписания для кипячения чайника. По задумкам авторов сего творения, каждый раз после возвращения чайника на подставку ты должен запустить их фирменное приложение и синхронизировать чайник со смартфоном. Вот так вместо облегчения рутинных задач «умная» техника дрессирует нас совершать дополнительные действия. Но все изменилось, когда в доме появился HomeAssistant. Тогда я и решил разобраться в протоколе.
Я честно пытался декомпилировать и разобрать оригинальное приложение, но потерпел фиаско. Те инструменты, которые я использовал, не позволили мне понять логику работы чайника. Все процедуры и функции получались «кривыми», безымянными (по типу a, b, c и тд). Возможно, у меня не хватает опыта и умения. В итоге я пошел тем же путем, что и автор предыдущей статьи. Единственное существенное отличие в том, что я использовал интерактивный режим работы утилиты gatttool. Преимущество в том, что данный режим исключает всевозможные «гонки», о которых писал автор первой статьи.
Так как HomeAssistant написан на python, то все дальнейшие команды будем писать на нем. Для использования интерактивного режима работы gatttool на python нам поможет библиотека pexpect, позволяющая порождать сущности сторонних приложений и следить за их выводом (лихо загнул).
Про общее описание протокола обмена я вновь отправлю к автору первой статьи, поэтому без долгих прелюдий приступим к командам управления.
Устанавливаем соединение:
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")
После установления соединения перво-наперво нам надо подписаться на получение уведомлений от чайника. Без этого чайник будет воспринимать команды, однако не сможет ничего нам ответить, кроме текстового «Successfully».
child.sendline("char-write-cmd 0x000c 0100")
child.expect(r'[LE]>')
child.sendline("char-write-req 0x000e 55" + iter + "ff" + key + "aa")
child.expect("value: ")
child.expect("rn")
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.
child.sendline("char-write-req 0x000e 55" + iter + "01aa")
child.expect("value: ")
child.expect("rn")
child.expect(r'[LE]>')
Пример ответа:
value: 55 01 01 02 1d aa
Во всех моих экспериментах ответ неизменно был таким.
child.sendline("char-write-req 0x000e 55" + iter + "6e" + timeNow + tmz + "0000aa")
child.expect("value: ")
child.expect("rn")
child.expect(r'[LE]>')
Здесь tmz — часовой пояс в обратном hex формате (например, часовой пояс +3 переводим в секунды, затем в hex формат и получаем hex(3*60*60)=2a30, разбиваем по парам и выводим в обратном порядке 302a). Как быть с отрицательными часовыми поясами я не знаю, не тестировал, но есть подозрение, что за это отвечает следующий за tmz байт. Здесь timeNow — текущее время unixtime [2] в обратном hex формате. Алгоритм такой же: получаем текущее время в секундах, переводим в HEX, разбиваем по парам и выводим строкой в обратном порядке.
Пример ответа:
value: 55 02 6e 00 aa
Во всех моих экспериментах ответ неизменно был таким.
child.sendline("char-write-req 0x000e 55" + iter + "4700aa")
child.expect("value: ")
child.expect("rn")
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("rn")
statusStr = child.before[0:].decode("utf-8")
times = hexToDec(str(statusStr.split()[7] + statusStr.split()[6]))
child.expect(r'[LE]>')
Watts — возвращает затраченную энергию в Ваттах, alltime — время работы чайника в часах, times — количество запусков чайника. hexToDec — функция перевода в десятичный формат.
child.sendline("char-write-req 0x000e 55" + iter + "06aa")
child.expect("value: ")
child.expect("rn")
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 (видимо, это какие то относительные единицы, точно не секунды).
child.sendline("char-write-req 0x000e 55" + iter + "05" + mode + "00" + temp + "00000000000000000000" + howMuchBoil + "0000aa")
child.expect("value: ")
child.expect("rn")
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 — не успешно.
child.sendline("char-write-req 0x000e 55" + iter + "03aa")
child.expect("value: ")
child.expect("rn")
statusStr = self.child.before[0:].decode("utf-8")
answer = statusStr.split()[3]
child.expect(r'[LE]>')
Пример ответа:
value: 55 06 03 01 aa
Четвертый байт ответа показывает успешность включения: 01 — успешно, 00 — не успешно.
child.sendline("char-write-req 0x000e 55" + iter + "04aa")
child.expect("value: ")
child.expect("rn")
statusStr = self.child.before[0:].decode("utf-8")
answer = statusStr.split()[3]
child.expect(r'[LE]>')
Пример ответа:
value: 55 07 04 01 aa
Четвертый байт ответа показывает успешность выключения: 01 — успешно, 00 — неуспешно.
child.sendline("char-write-req 0x000e 55" + iter + "37c8c8" + onoff + "aa") # 00 - off, 01 - on
child.expect("value: ")
child.expect("rn")
child.expect(r'[LE]>')
Параметр onoff равен либо 01 — включить функцию, либо 00 — выключить функцию.
Пример ответа:
value: 55 08 37 00 aa
Во всех моих экспериментах ответ неизменно был таким.
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("rn")
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
Во всех моих экспериментах ответ неизменно был таким.
child.sendline("char-write-req 0x000e 55" + iter + "33" + boilOrLight + "aa")
child.expect("value: ")
child.expect("rn")
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.
Как и обещал в конце статьи привожу ссылку [3] на модуль для подключения к 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.
Между тем есть и нерешенные вопросы:
P.S.: Если есть интерес, то в следующей статье могу изложить, как я подключал SinoTrack ST-901 к HomeAssistant.
Автор: mavrikk
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/umny-j-dom/281287
Ссылки в тексте:
[1] статья: https://habr.com/post/371965/
[2] unixtime: https://www.unixtimestamp.com/
[3] ссылку: https://yadi.sk/d/M3yEiguU3Wedfm
[4] Источник: https://habr.com/post/412583/?utm_campaign=412583
Нажмите здесь для печати.