Управление подсветкой Xiaomi Gateway через Node-Red

Возникла не совсем тривиальная задача – научиться управлять подсветкой Xiaomi Mi Gateway через Node-Red. И что же тут нетривиального, спросите вы? Для этого есть и отдельные плагины и даже в Home Assistant имеется специальная интеграция. Есть. И все это наверняка отлично работает. Но только пока наш сервер на котором это все крутится и ми-шлюз находятся в одной сети.

Но что будет, если вы либо озаботились безопасностью своей сети или же решили, что смарт-девайсы (большинство из которых до сих пор умеют работать только с WI-FI на 2.4 ГГц) должны подключаться к своему WI-FI или еще по какой причине, но в любом случае вы (ну в данном случае я, как минимум) взяли и выделили всем смарт-устройствам отдельный сетевой сегмент. Со своей адресацией, со своим WI-FI только на 2.4 ГГц и с правилами FW между этим сегментом и вашей основной сетью?

А будет вот что: та же интеграция Xiaomi-Aqara будет бодро стартовать и даже какое-то время (в пределах часа) успешно управлять подсветкой шлюза и получать информацию об его состоянии. А потом произойдет “ой” и лампочка перейдет в “unavailable”. А сам Home Assistant, когда вы решите его перезагрузить, благополучно зависнет на завершении своей работы.

Ноги у всего этого дела растут из того, что шлюз представляется в сети путем отсылки мультикастов, то есть безадресных сообщений, которые может получить любой желающий, кто будет их слушать на определенном порту. Этими же мультикастами он рассылает и информацию о своем текущем сессионном token-e (не путать с тем токеном, который грабим через приложение, это постоянный токен), насколько я помню – раз в 10 секунд. А для того, что бы он принял от нас какую-либо команду, нам требуется взять этот текущий сессионный токен, зашифровать его ключом разработчика (да, шлюз требуется перевести в режим разработчика, как написано на сайте HA) и отправить получившуюся строку вместе с нужной нам командой. При старте НА работает напрямую с указанным в конфиге IP адресом, не требуя получать информацию о статусе шлюза, который тот как раз и рассылает регулярно. Но через некоторое время, когда вы перестали тыкать в кнопки, включая и выключая подсветку, НА переходит в режим ожидания, ожидая соответственно получения информации о шлюзе через мультикасты. Но мультикаты не могут ходит через NAT, который обеспечивает обмен данными между разными сегментами сети, поэтому НА благополучно объявляет, что устройство недоступно и забивает на него. Почему он не шлет команду на конкретный IP указанный к конфиге – для меня загадка. Нужно будет как-нибудь написать им на гитхабе об этой проблеме, но а пока ее требуется решать.

Затянутое получилось вступление, но это было нужно, что бы в общих чертах объяснить зачем и почему это все потребовалось.

А теперь переходим к технической части.

Дано: ми-шлюз, переведенный в режим разработчика, живущий в сети 192.168.2.0/24, сервер (raspberry), с поднятым на нем Node-Red, живущий в сети 192.168.1.0/24, некое условие в Node-Red по которому нужно включить и выключить подсветку ми-шлюза. Ничего дополнительно ставить не требуется, достаточно того, что идет с Node-Red по умолчанию.

Что здесь что и для чего? Первая нода – это нода линка, она принимает с другого flow payload для всего этого процесса, payload простой – либо “ON”, либо “OFF”. Нода rbe просто проверяет, что не идет серия ON или OFF и пропускает сообщение дальше, только если значение изменилось.

Нода функции “Request current token”. Она нужна для того, что бы сформировать команду на запрос текущего токена у шлюза, ведь как мы помним, его постоянно рассылаемые сообщения захламляют соседнюю сеть и к нам не попадают.

var mode = flow.get('lightMode');
if(mode != msg.payload) {
    msg.payload = '{"cmd" : "get_id_list"}';
    return msg;
}

Тут видно, что у нас есть flow переменная lightMode и payload мы формируем только если ее значение не совпадает с пришедшим. То есть если lightMode установлен в ON (что означает, что свет на шлюзе включен), и пришла команда на включение – то делать ничего не надо и пришедшее сообщение тут можно тихо похоронить.

Нода “Send request to GW”. Этой самой нодой мы отсылаем сообщение шлюзу, то самое, которое сформировали ранее

Из важного здесь: я четко указываю с какого порта должно быть отправлено сообщение (выбрал 8989, как зеркальный к порту на шлюзе). Это нужно для того, что бы в следующей ноде поймать ответ, который пришлет шлюз на мой запрос. 192.168.2.9 – это IP адрес ми-шлюза, у вас он наверняка другой, не забудьте заменить.

Нода “Read port”. Эта нода соответственно читает то, что ответил шлюз на наш запрос. В ее настройках как раз указан тот же порт для прослушивания, с которого прошлая нода отравляет сообщения.

Следующая нода JSON просто для того, что бы отформатировать ответ шлюза в понятный нам формат.

Нода Get Token – как понятно из названия, ее основное предназначение получить текущий токен шлюза и создать зашифрованную его версию для отправки команд

var crypto = global.get('cryptoModule');
var iv = Buffer.from([0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e]);
var payload = msg.payload;
var newmsg = null;
var data = JSON.parse(payload.data);
if (payload.token) {
    var lastToken = payload.token;
    var password = 'ХХХХХХХХХХХХХХХХ'; // тут надо подставить свой пароль от точки доступа, полученный при переводе в режим разработчика
    var cipher = crypto.createCipheriv('aes-128-cbc', password, iv);
    var key = cipher.update(lastToken, "ascii", "hex"); // шифруем токен
    cipher.final('hex'); // конвертируем в hex
    flow.set("xiaomikey",key); // пишем результат в переменную xiaomikey, область видимости которой - наш flow.
    newmsg = {payload: key};
    return newmsg;
}

Огромное спасибо за этот код пользователю ЖЖ kuznik. Без его поста на эту тему, сам бы я скорее всего не разобрался.

Кстати, как он правильно заметил, что бы подключить криптомодуль, необходимо внести правки в settings.js в папке где находится сам Node-Red

    functionGlobalContext: {
        // os:require('os'),
        // jfive:require("johnny-five"),
        // j5board:require("johnny-five").Board({repl:false}),
        cryptoModule:require('crypto')
    },

Следующая нода функции Set Light, в которой, в зависимости от текущего состояния света (предположительного), я либо включаю, либо выключаю свет

var key = msg.payload;
var mode = flow.get('lightMode');
if(mode === "OFF") {
    msg.payload = '{"cmd":"write","model":"gateway","sid":"ХХХХХХХ","data":"{\\"key\\":\\"' + key + '\\",\\"rgb\\":838795264,\\"illumination\\":484}"}';
    flow.set('lightMode', "ON");
    node.warn("Turn on light");
} else {
    msg.payload = '{"cmd":"write","model":"gateway","sid":"ХХХХХХХХХХ","data":"{\\"key\\":\\"' + key + '\\",\\"rgb\\":0}"}';
    flow.set('lightMode', "OFF");
    node.warn("Turn off light");
}

return msg;

Да. Вам еще потребуется получить SID шлюза, что бы он понимал, что полученная им команда предназначена именно для него. Его можно получить например так:

Запустить sudo tcpdump -A udp port 9898 на малине с НА и попробовать тем же НА поуправлять шлюзом. Можно запустить это и на роутере, если он у вас такое позволяет и тогда уже шлюзом можно поуправлять через приложение. Или же запустить его на чем-то, расположенном в той же сети, что и шлюз и уже отловить нужные данные в мультикаст сообщениях.

(зачем я шлю параметр illumination – слабо представляю, сперва я думал, что это яркость, но нет, это уровень освещенности, которую намерял датчик в самом шлюзе)

Ну и последняя нода Send command to GW – это копия ноды UPD OUT, уже разобранной выше. Кстати, в данном случае уже жесткая привязка к порту не нужна и можно оставить отсылку сообщения с рандомного порта, все равно ответ шлюза нам не интересен.

На данный момент это все. Конечно это еще далеко до идеала, но для быстрого старта сгодится. В планах еще все таки брать реальное состояние шлюза и записывать его не во flow переменную, а в MQTT, потому что сейчас, можно попасть в рассинхрон, когда то что я записал в переменной состояния света будет прямо противоположно реальному состоянию света на шлюзе, в текущем варианте таких проверок не предусмотрено. Ну и вообще идеально будет потом привязать это в HK с отображением реального состояния. В общем еще есть где развернуться.

4 ответа к «Управление подсветкой Xiaomi Gateway через Node-Red»

  1. добрый день. делаю всё по инструкции, но каждый раз получаю ошибку “TypeError: Cannot read property ‘createCipheriv’ of undefined” в функции. поправки в settings.js внёс. Может не правильно как то это сделал?

  2. Вот на это я навскидку не отвечу. я уже благополучно разобрал всю эту конструкцию и выпилил node-red вообще, перенеся все автоматизации в homeassistant, после какого-то его очередного обновления

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *