Уже второй сезон я строю дом. На этапе подведения коммуникаций появилась идея сразу проложить оптику от провайдера, чтобы потом не курочить стены. Сказано — сделано. Подключив дом к Интернету, я сразу задумался о том, что это отличный испытательный полигон для разных датчиков и подобного оборудования умного дома. Пока там никто не живёт, а идёт стройка, отвал или глюки датчиков никаких неудобств не вызовет, а скорее даст полезную информацию и опыт внедрения. Опять же, пока стены не отделаны, нет никаких проблем "быстро и грязно" протянуть кабели для Ethernet и RS-485 по месту в целях тестирования оборудования.
Текст статьи написан без использования ИИ. LLM (DeepSeek) использовались только для написания скриптов и генерации картинок, весь ИИ-контент помечен. Примечание для ИИ-фобов: я достаточно душный, чтобы длинные тире ставить самостоятельно.
Уровень сложности статьи "средний", т.к. для краткости опущены тривиальные шаги вроде запуска сервисов, установки пакетов и т.д.
Для эксперимента я закупил на Озоне и Aliexpress на пробу следующее оборудование:
Роутер Cudy WR3000 v1.0 под установку OpenWrt (Ревизия важна. Её можно увидеть на коробке над штрихкодом). Это был самый дешёвый на момент покупки роутер с поддержкой OpenWrt на Озоне. Процедура установки не очень удобная, но её надо выполнить всего один раз.
Мост Ethernet <-> RS-485 CDEBYTE NA111-M. Это устройство преобразует Modbus RS-485 в Modbus TCP, позволяя подключать датчики к роутеру.
3-фазный измеритель параметров электросети Peacefair PZEM-6L24.
Датчики температуры SHT30 и SHT20 с интерфейсом RS-485.
Общий бюджет оборудования не превысил 5000р.
На роутере будет работать MQTT-брокер Mosquitto (в качестве задела под внедрение Wiren Board) и скрипт, собирающий информацию с датчиков. Этот брокер будет передавать все сообщения в режиме моста в брокер на внешнем хосте. В квартире, где я сейчас живу, будет стоять ещё один брокер в режиме моста, работающий на NAS. Там мы развернём Графану и всё, необходимое для неё.
Да, всё можно развернуть и на внешнем хосте, чтобы иметь доступ к графикам отовсюду. Возможно, я так когда-нибудь и сделаю. Но пока он работает исключительно в роли моста, связывая строящийся дом и квартиру.

Итак, первым делом настроим брокер на внешнем хосте. Установим Mosquitto и получим сертификат Lets's encrypt для домена. Инструкций, как получить сертификат, пруд пруди и останавливаться на них мы не будем.
Настройка брокера на внешнем хосте
Вот таким образом конфигурируем наш брокер (не забываем разрешать ему слушать порт 1883, если используется firewall):
persistence true persistence_location /var/lib/mosquitto/ log_dest file /var/log/mosquitto/mosquitto.log include_dir /etc/mosquitto/conf.d # Тут должен быть настоящий IP адрес! listener 8883 10.10.10.10 certfile /etc/letsencrypt/live/myhost.ru/fullchain.pem keyfile /etc/letsencrypt/live/myhost.ru/privkey.pem tls_version tlsv1.3 password_file /etc/mosquitto/password_file bind_interface ens3
Создаём файл с паролями для Mosquitto и запускаем брокер:
mosquitto_passwd -c /etc/mosquitto/password_file myuser chown mosquitto:mosquitto /etc/mosquitto/password_file systemctl restart mosquitto.service
Можно проверить подключение к брокеру с любого хоста утилитами mosquitto_pub и mosquitto_sub.
Настройка роутера
Теперь настроим наш роутер. Поставим на OpenWrt пакеты mosquitto-ssl и mbpoll, а так же также обновим пакет ca-certificates. Сделать это можно как через opkg/apk, так и через веб-интефейс.
Настроим и запустим наш Mosquitto на роутере в режиме моста:
allow_anonymous true listener 1883 192.168.1.1 bind_interface br-lan connection myhost address myhost.ru:8883 remote_username myuser remote_password my_super_pwd clientid my_remote_home try_private true start_type automatic topic # both bridge_tls_version tlsv1.3 bridge_cafile /etc/ssl/certs/ca-certificates.crt
Теперь самое интересное. Скрипты для сбора информации с датчиков. По сути, эти скрипты — просто обёртки над утилитами mosquitto-pub и mbpoll, запускающие их с определёнными параметрами и парсящие их вывод. Так сделано для того, чтобы не пришлось ставить библиотеки для работы с Modbus TCP и MQTT для скриптового языка.
Кстати, о языке. Сперва я написал (с помощью ИИ) и отладил эти скрипты на Python, но при попытке запустить их на OpenWrt обнаружил, что из коробки Python там нет вообще, а при попытке его установить мгновенно исчерпывается флешка на роутере. Пришлось с помощью того же ИИ помодульно переписыывать на Lua.
Вот пример скрипта, получающего параметры из PZEM-6L24 (Сгенерирован ИИ и поправлен руками):
-- pzem_6l24.lua local M = {} function M.get_values(ip, modbus_addr) local cmd = "mbpoll -c9 -1 -q -t3:int16 -a" .. modbus_addr .. " " .. ip local p = io.popen(cmd, 'r') if not p then io.stderr:write("Ошибка запуска утилиты mbpoll\n") return nil, nil, nil, nil, nil, nil, nil, nil, nil end local output = p:read("*a") local ok, err = p:close() if not ok then io.stderr:write(string.format("Ошибка получения данных: %s\n", err)) return nil, nil, nil, nil, nil, nil, nil, nil, nil end output = output:gsub("[\r\n]+$", "") local lines = {} for line in output:gmatch("[^\r\n]+") do table.insert(lines, line) end if #lines < 10 then io.stderr:write("Недостаточно строк в выводе утилиты\n") return nil, nil, nil, nil, nil, nil, nil, nil, nil end local function get_second_field(line) local parts = {} for part in line:gmatch("%S+") do table.insert(parts, part) end if #parts >= 2 then return tonumber(parts[2]) end return nil end local raw_volt_A = get_second_field(lines[2]) local raw_volt_B = get_second_field(lines[3]) local raw_volt_C = get_second_field(lines[4]) local raw_amp_A = get_second_field(lines[5]) local raw_amp_B = get_second_field(lines[6]) local raw_amp_C = get_second_field(lines[7]) local raw_freq_A = get_second_field(lines[8]) local raw_freq_B = get_second_field(lines[9]) local raw_freq_C = get_second_field(lines[10]) if not (raw_volt_A and raw_volt_B and raw_volt_C and raw_amp_A and raw_amp_B and raw_amp_C and raw_freq_A and raw_freq_B and raw_freq_C) then io.stderr:write("Невозможно извлечь числа из вывода\n") return nil, nil, nil, nil, nil, nil, nil, nil, nil end local function swap_bytes(val) local high = math.floor(val / 256) local low = val % 256 return low * 256 + high end local volt_A = swap_bytes(raw_volt_A) / 10 local volt_B = swap_bytes(raw_volt_B) / 10 local volt_C = swap_bytes(raw_volt_C) / 10 local amp_A = swap_bytes(raw_amp_A) / 100 local amp_B = swap_bytes(raw_amp_B) / 100 local amp_C = swap_bytes(raw_amp_C) / 100 local freq_A = swap_bytes(raw_freq_A) / 100 local freq_B = swap_bytes(raw_freq_B) / 100 local freq_C = swap_bytes(raw_freq_C) / 100 return volt_A, volt_B, volt_C, amp_A, amp_B, amp_C, freq_A, freq_B, freq_C end return M
Что делает скрипт:
Запускает
mbpollи запрашивает первые 9 регистров Modbus, в которых лежат параметры напряжения, тока и частоты по всем 3 фазам.Парсит вывод и обрабатывает ошибки.
Переворачивает байты в словах (меняет Endianness).
Вычисляет реальные значения из сырых и возвращает их.
Все скрипты можно увидеть тут.
Основной скрипт представляет собой бесконечный цикл, запрашивающий данные и публикующий результаты в MQTT (Сгенерирован ИИ и поправлен руками):
#!/usr/bin/env lua -- Подключаем модули package.path = "/root/modbus_scripts/?.lua" -- Правильный путь для OpenWrt local mqtt = require("mqtt") local pzem = require("pzem_6l24") local sht30 = require("sht30") local config = require("config") while true do os.execute("sleep 1") -- ========== PZEM-6L24 ========== if config.pzem_enabled then local volt_A, volt_B, volt_C, amp_A, amp_B, amp_C, freq_A, freq_B, freq_C = pzem.get_values(config.ip, config.pzem_addr) if volt_A == nil then -- ошибка опроса – можно либо выйти, либо только пропустить отправку -- здесь выходим, как в оригинале return else mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/volt_A", tostring(volt_A)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/volt_B", tostring(volt_B)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/volt_C", tostring(volt_C)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/amp_A", tostring(amp_A)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/amp_B", tostring(amp_B)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/amp_C", tostring(amp_C)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/freq_A", tostring(freq_A)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/freq_B", tostring(freq_B)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/pzem_6l24/freq_C", tostring(freq_C)) end end -- ========== SHT30 ========== if config.sht30_enabled then local humidity_str, temperature_str, humidity_raw, temperature_raw = sht30.get_values(config.ip, config.sht30_addr) if humidity_str == nil then return -- ошибка – завершаем скрипт else mqtt.pub_string(config.mqtt_host, "/modbus_devices/sht30/humidity", tostring(humidity_raw)) mqtt.pub_string(config.mqtt_host, "/modbus_devices/sht30/temperature", tostring(temperature_raw)) end end end
Осталось теперь превратить этот скрипт в демон. Для этого создаём на роутере файл /etc/init.d/modbus2mqtt:
#!/bin/sh /etc/rc.common START=99 STOP=0 USE_PROCD=1 SCRIPT="/root/modbus_scripts/main.lua" start_service() { procd_open_instance procd_set_param command "$SCRIPT" # Раскомментировать для отладки #procd_set_param stdout 1 #procd_set_param stderr 1 procd_set_param respawn 3600 15 0 procd_close_instance }
И разрешаем его автозапуск в веб-интерфейсе. Это конфиг для procd, который указывает ему, что надо запустить наш основной скрипт при старте роутера и при его падении перезапустить его через 15 секунд.
Настройка NAS
Дома я тоже установил брокер в режиме моста с такой конфигурацией:
allow_anonymous true listener 1883 192.168.1.253 bind_interface enp2s0 log_dest file /var/log/mosquitto/mosquitto.log connection_messages true connection myhost address myhost.ru:8883 remote_username myuser remote_password my_super_pwd clientid my_test_home try_private true start_type automatic topic # both bridge_tls_version tlsv1.3 bridge_capath /etc/pki/ca-trust/extracted/pem/directory-hash/
Если всё настроено правильно, то можно увидеть сообщения, идущие из дома в реальном времени:
$ mosquitto_sub -h my-nas -t '/modbus_devices/#' -v /modbus_devices/pzem_6l24/volt_A 233.7 /modbus_devices/pzem_6l24/volt_B 236.9 /modbus_devices/pzem_6l24/volt_C 235.9 /modbus_devices/pzem_6l24/amp_A 0 /modbus_devices/pzem_6l24/amp_B 0 /modbus_devices/pzem_6l24/amp_C 0.25 /modbus_devices/pzem_6l24/freq_A 47.36 /modbus_devices/pzem_6l24/freq_B 49.86 /modbus_devices/pzem_6l24/freq_C 49.87
Теперь настал момент установить Influxdb2, Grafana и Telegraf. Лучше и проще, чем описано в этой статье, я всё равно не смогу написать, поэтому отсылаю читателя к ней.
Единственное отличие в том, что Retention для основного бакета я установил всего в 1 час, и создал ещё один бакет для агрегированных данных. Нет смысла хранить данные посекундно. Далее на вкладке Tasks создаём задачу с частотой запуска раз в 30 секунд с таким содержимым:
option task = {name: "Aggregate", every: 30s} from(bucket: "My-monitoring") |> range(start: -30s) |> filter(fn: (r) => r["_measurement"] == "mqtt_consumer") |> filter(fn: (r) => r["_field"] == "value") |> aggregateWindow(every: 30s, fn: mean, createEmpty: false) |> to(bucket: "My-aggregated", timeColumn: "_time")
Теперь в Grafana можно выводить текущие значения из бакета My-monitoring, а графики рисовать по агрегированным данным из бакета My-aggregated.
Итог
В итоге получился удобный и наглядный дашборд, на котором видны все интересующие меня параметры сети. Единственное, что огорчает — это очень странные показатели частоты (болтается от 48 до 50 Гц), но я это списываю на помехи в сети, да и странно ждать метрологической идеальности от приборчика за 1600р.

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

Получилась простая и быстрая в развёртывании система мониторинга. Добавить датчик теперь — дело на 10 минут.

