Domoticz c Node-Red на OpenWrt. Проброс mqtt из SLS и обратно.
Domoticz есть в стандартных пакетах OpenWrt, поскольку я буду в Domoticz заводить устройства через mqtt то на роутер надо поставить mosquitto, Node-Red в стандартной сборке я не нашел, есть 2 варианта установки:
1. Установка Node-Red по этой инструкции.
2. Сборка OpenWrt с пакетами Node.js версии 10, 12 или 14 и Node-red. а так же нам может понадобиться node-npm, собираем OpenWrt из исходников добавив пакеты node.js отсюда.
После установки Domoticz будет на порту 8080 по адресу роутера, Node-red по адресу роутера на порту 1880.
В Domoticz добавляем mqtt
и устройство Dummy
далее создаем виртуальные датчики, фактически на каждый топик в mqtt свой датчик, это может быть выключатель, температура, напряжение и т.п.
Чтобы смотреть куда Ваши устройства публикуют свои статусы рекомендую установить mqtt клиента, я использую кросс платформенный MQTT Explorer
Проблема в том, что Domoticz случает только топик domoticz/in и публикует в топик domoticz/out
Есть несколько вариантов решения данной проблемы, одна из которых это использование плагина для Domoticz, я лишь нашел Python плагины, но проблема в том, что видимо Domoticz для OpenWrt собран без поддержки Python и Python плагины не работают.
Есть вариант использовать оборудование с поддержкой Domoticz или возможностью настройки публикации, но это сильно сокращает список оборудования которое возможно использовать, да и например в прошивке Tasmota можно указать топики, но тогда кроме данных mqtt сервера надо настраивать топики на каждом конечном устройстве и обработчик всё равно надо писать, по этому на мой взгляд лучшее решение это Node-Red.
Настройка Node-Red
Вообще всю автоматизацию возможно использовать только с Node-Red, визуализация так же возможна с помощью Node-Red Dashboard, но мы будем использовать Node-Red только для конвертации сообщений в и из Domoticz.
Для этого надо создать 2 потока, фактически это потоки конвертирования из mqtt и mqtt. Поток mqtt to domoticz это конвертер из топиков # в топик domoticz/in, поток domoticz to mqtt это конвертер из потока domoticz/out в топики устройств.
Вот так выглядит часть кода конвертера из mqtt в domoticz/in (взято отсюда)
красным подчеркнуто последний уровень топика, синим idx устройства в Domoticz, при этом вызываемая функция зависит от типа датчика в Domoticz.
Для того чтобы каждый раз не ползать в Node-Red при добавлении нового устройства и не сопоставлять idx устройств с топиками и типом и не находить место куда надо добавить нужный код мы пойдем другим путем.
Кроме того данный метод не приемлем если использовать обратный поток из domoticz/out когда значение выключателя для переключения находиться в другом топике нежели значение для статуса, например у меня в шлюзе SLS реле xiaomi имеет статус в топике sls/rele1/state_l1 а чтобы его переключить надо опубликовать в топик sls/rele1/set/state_l1
При публикации в топике sls/rele1/set/state_l1 из потока domoticz to mqtt поток mqtt to domoticz запускается поскольку изменилось значение sls/rele1/state_l1 которое в потоке mqtt to domoticz изменяет статус выключателя в Dоmoticz, а поскольку статус изменился, то Domoticz публикует в топик sls/rele1/set/state_l1 и так по кругу обеспечивая такую ddos атаку собственного mqtt сервера. после 3 минут работы потоков и последующего отключения потоков в Node-Red в mqtt сыпались сообщения ещё 1 минуту. По этому нам надо как то решить эту проблему.
Метод такой: при получении сообщения из топика мы будем искать в свойствах устройств такой топик, при совпадении полученного топика и топика в устройстве и если значение топика отличается от значения в устройстве (чтобы не дергать лишний раз одинаковыми значениями) мы будем формировать json запрос в топик domoticz/in
Для получения параметров устройств мы воспользуемся Domoticz API/JSON URL’s
поток будет выглядеть следующим образом:
функция 1 получает от mqtt данные о топике и его значении, эти данные надо сохранить для того чтобы их использовать совместно с полученными данными от Domoticz о списке устройств и их параметрах, что при такой конструкции в одной функции невозможно. Тут нам на помощь приходят переменные, а именно переменные в пределах потока (flow.set), то есть при установке в функции 1 её можно получить в функции 2. Но этого не достаточно, поскольку существует проблема синхронизации запуска потока. Дело в том, что если устройства будет публиковать в несколько топиков данные с интервалом несколько милисекунд, а именно так происходит например когда нажимаешь клавишу выключателя xiaomi, публикуются данные в 6 топиков, то есть поток запускается 6 раз (6 экземпляров потока). Когда в первом экземпляре потока мы присвоим переменной какое то значение, начнется выполняться http запрос в этом экземпляре потока, в это время запуститься второй экземпляр потока который присвоит этой же переменной свое значение и когда первый экземпляре потока дойдет до функции 2 и там считается значение этой переменной, то значение будет уже из второго экземпляре потока. По наблюдениям когда второй экземпляре потока в функции 2 будет считывать переменную заданную в функции 1, то он получит значение которое присвоено в 18-м экземпляре потока, то есть на момент выполнения функции 2 в 17-и экземплярах потока в переменной будет значение из 17-го экземпляра потока и мы получим в функции 2 только значение 17 экземпляре потока во всех 17 потоках.
Чтобы этого избежать надо нумеровать переменные в зависимости от номера потока. Мы знаем, что каждый топик запускает поток, в потоке один раз выполняется функция 1, потом один раз выполняется http запрос, потом один раз выполняется функция 2. То есть в 10-м потоке функция 1 будет выполнена 10-й раз, функция 2 будет выполнена тоже 10-й раз, в 18-м потоке обе функции будет выполнены 18 раз. Теперь в каждой функции нам надо сделать счетчик запуска функции, чтобы этот счетчик могла менять только функция, для этого мы будем использовать переменную в контексте (context.set).
В Node-Red имеются следующие периоды жизни переменных:
var myParam = «моя переменная»; — будет жить только внутри функции при выполнении, закончилась выполняться функция, переменная исчезла, при новом запуске функции значения этой переменной не будет пока его заново не присвоить.
context.set(‘myParam’,»моя переменная»); — будет жить в пределах функции в которой присвоена, при новом запуске функции значение этой переменной сохраниться.
flow.set(‘myParam’, «моя переменная»); — будет жить в пределах всего потока, при завершении потока сохраняется и может быть получена при повторном запуске потока. Не видна в других потоках
и есть глобальный контекст, то есть переменная будет жить во всех потоках, мы такие переменные использовать не будем. Подробнее о переменных, контекстах и функциях тут и тут
Поток mqtt to domoticz будет выглядеть так
данные в узлах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// делаем счетчик запусков функции var max_count = 50; // ограничим счетчик 50 var count = context.get('count')||0; // если счетчика нет в переменной контекста то присваиваем значение 0 count += 1; if (count == max_count) {count = 0;} // ограничиваем количество переменных context.set('count',count); // присваиваем счетчик в переменную в контексте var mytopic = "mytopic" + context.get('count'); // формируем имя переменной потока для передачи в функцию 2 var mypayload = "mypayload" + context.get('count'); // формируем имя переменной потока для передачи в функцию 2 flow.set(mytopic, msg.topic); // присваиваем переменной топик flow.set(mypayload, msg.payload); // присваеваем переменной значение топика // параметры http запроса для узла "http запрос" var urlmsg = {} urlmsg.payload = "data to post"; urlmsg.headers = {"Content-Type": "application/json"}; urlmsg.url = "http://192.168.1.1:8080/json.htm?type=devices&filter=all"; // получить список всех устройств return urlmsg; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
// делаем счетчик запусков функции var max_count = 50; // ограничим счетчик 50 var count = context.get('count')||0; // если счетчика нет в переменной контекста то присваиваем значение 0 count += 1; if (count == max_count) {count = 0;} // ограничиваем количество переменных context.set('count',count); // присваиваем счетчик в переменную в контексте var mytopic2 = "mytopic" + context.get('count'); // формируем имя переменной потока для передачи в функцию 2 var mypayload2 = "mypayload" + context.get('count'); // формируем имя переменной потока для передачи в функцию 2 var my = {} var newmsg2 = {} arr = msg.payload.result // получаем массив из устройств arrlen = arr.length; // вычисляем длинну массива устройств var myTopic = flow.get(mytopic2); // получаем топик из переменной var myPayload = flow.get(mypayload2); // получаем значение топика из переменной // разбираем http запрос for(var j = 0; j < msg.payload.result.length; j++){ my.idx = msg.payload.result[j].idx; // idx my.HardwareName = msg.payload.result[j].HardwareName; // оборудование my.description2 = msg.payload.result[j].Description; // комментарий my.Type = msg.payload.result[j].Type; // подтип my.SubType = msg.payload.result[j].SubType; // подтип // присваиваем текущее значение в зависимости от типа if (my.SubType == "Switch") {my.Status = msg.payload.result[j].Status;} if (my.Type == "Temp") {my.Status = msg.payload.result[j].Temp;} if (my.SubType == "Voltage") {my.Status = msg.payload.result[j].Voltage;} // получаем из комментария топик для подписки и публикации if(msg.payload.result[j].Description.indexOf('topic: ') + 1) { my.topic2 = msg.payload.result[j].Description.match(/topic: ([\s\S]+?);/i)[1]; } if(msg.payload.result[j].Description.indexOf('topic_set: ') + 1) { my.topic_set = msg.payload.result[j].Description.match(/topic_set: ([\s\S]+?);/i)[1]; } // сравниваем топик для обновления значения if (myTopic == my.topic2) { // сравниваем значение, если значения не совпадают то не обновляем if (myPayload == 'ON') { myPayload = 'On';} if (myPayload == 'on') { myPayload = 'On';} if (my.Status == 'ON') { my.Status = 'On';} if (my.Status == 'on') { my.Status = 'On';} if (myPayload == 'OFF') { myPayload = 'Off';} if (myPayload == 'off') { myPayload = 'Off';} if (my.Status == 'OFF') { my.Status = 'Off';} if (my.Status == 'off') { my.Status = 'Off';} if (myPayload == my.Status){ return null; } else { if (my.SubType == "Switch") {newmsg2.payload = setSwitch(my.idx, myPayload);} if (my.Type == "Temp") {newmsg2.payload = setTemp(my.idx, myPayload);} if (my.SubType == "Voltage") {newmsg2.payload = setTemp(my.idx, myPayload);} return newmsg2; } } my = {} // очищаем переменную } return null; // --------------------------- функции ---------------------------- function setSwitch(idx, nm){ // выключатель var switchpayload = '{"command":"switchlight","idx":' + idx + ',"switchcmd":"'+ nm +'","parse":false}'; return switchpayload; } function setTemp(idx, nm){ // температура nm = +nm; var temppayload = '{"idx":' + idx +',"svalue":"' + nm.toFixed(2) + '","parse":false}'; return temppayload; } function setText(idx, nm){ // текстовое значение var textpayload = '{"idx":' + idx +',"svalue":"' + nm + '","parse":false}'; return textpayload; } function setHumi(idx, nm){ // влажность nm = Math.round(nm); var humipayload = '{"idx":' + idx +',"nvalue":' + nm + ',"svalue":"0","parse":false}'; return humipayload; } function setWind(idx, nm){ nm = Math.round(nm * 10); var sv = '0;N;' + nm +';' + nm + ''; var windpayload = '{"idx":' + idx +',"nvalue":0,"svalue":"' + sv +';0;0","parse":false}'; return windpayload; } |
Полностью json потока для экспорта в Node Red
теперь создадим обратно из Domoticz в Node-red, поток будет выглядеть так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
newmsg={}; if(msg.payload.dtype == "Light/Switch"){ myDescr = msg.payload.description; //топик по писанию устройства // получаем из комментария топик для подписки и публикации if(myDescr.indexOf('topic_set: ') + 1) { newmsg.topic = myDescr.match(/topic_set: ([\s\S]+?);/i)[1]; } else {return null;} if(msg.payload.nvalue == 1){ newmsg.payload = 'ON'; } else if (msg.payload.nvalue === 0){ newmsg.payload = 'OFF'; } else { newmsg.payload = msg.payload.svalue1; } return newmsg; } else { return null; } |
Полностью json потока для экспорта в Node Red
Теперь нам осталось в устройствах Domoticz в описаниях добавить пути к топикам, для статусов только путь к топику со статусом в формате: topic: путь/к/топику;, для тех кто публикует (выключатели) путь set в формате: topic_set: путь/к/топику;
для датчиков:
для выключателей:
на этом все, при добавлении новых устройств достаточно в свойствах устройства указывать пути к топикам.