company_banner

Модифицирующий MQTT Proxy


    Janus MQTT Proxy — это сервис, который я написал на Go в качестве хобби-проекта. Он подключается к MQTT-брокеру и подписывается на все события, а клиенты, в свою очередь, подключаются к proxy и общаются с ним как с MQTT-брокером.


    Он позволяет:


    • ограничивать доступ клиентов к разным топикам. В том числе раздельно ограничивать доступ на чтение и запись;
    • подменять названия топиков и содержимое событий с помощью regexp-ов.

    Зачем это нужно


    В MQTT нет стандарта для структуры топиков и содержимого пакетов. Например, в каких-то сервисах включение лампочки выполняется отправкой 1 в топик /my/lamp/on, а где-то нужно отправить On в топик /my/lamp. Чтобы корректно связать между собой два таких сервиса, нужно явно указывать одному из них, что нужно отсылать и куда. Если топиков много, тогда конфиг будет огромным и совершенно нечитаемым.


    Janus MQTT позволяет оставить сервисы как есть: каждый работает с топиками так, как ему удобно, а весь конфиг конвертации MQTT-пакетов сосредоточен в одном единственном месте. Причём чем больше разных сервисов/устройств вы используете, тем такой подход удобнее.


    В идеале каждый сервис должен брать свою конфигурацию просто из структуры топиков. Допустим, среди них есть топик /light/main. «Ага, — должен подумать сервис, — значит, я могу включать и выключать свет, отправляя сообщения в этот топик». К сожалению, таких сообразительных сервисов я пока не встречал.


    Что касается безопасности, то любая конфигурация умного дома состоит из модулей, как физических, так и программных. С помощью Janus MQTT мы можем дать этим устройствам/модулям доступ только туда, куда нужно. Это, кстати, не только улучшит безопасность, но и снизит уровень хаоса — никаких публикаций в топики с неизвестных клиентов.


    Конфигурация Janus MQTT


    Чтобы было понятнее, как использовать сервис, я разберу небольшую часть моего конфига — ту, которая описывает управление освещением.


    У Janus MQTT есть основной конфиг, который содержит основные настройки и список пользователей. Для каждого пользователя задаётся пароль и отдельный конфиг преобразований — самое интересное происходит именно в нём:


    broker_to_client: # настройка преобразования пакетов от брокера к клиенту
    
      # описание устройств для MQTT discovery.
      - topic: ^/devices/wb-gpio/controls/LIGHT_([^/]*)/meta/type$
        template: /homeassistant/light/{{.f1}}/config
        val_map:
          switch: >-
            {
            "command_topic":"/light/{{.f1}}/state",
            "state_topic":"/light/{{.f1}}/state",
            "name":"{{.f1}}"
            }
    
      - topic: ^/devices/wb-gpio/controls/LIGHT_([^/]*)$
        template: /light/{{.f1}}/state
        val_map: {0: OFF, 1: ON}
    
    client_to_broker: # настройка преобразования пакетов от клиента к брокеру
    
      - topic: ^/light/([^/]*)/state$
        template: /devices/wb-gpio/controls/LIGHT_{{.f1}}/on
        val_map: {OFF: 0, ON: 1}

    Каждое правило содержит регулярное выражение, которое извлекает данные из топика. Затем эти данные могут быть подставлены в шаблон названия нового топика и в шаблон пакета.


    Любопытно, что в этой конфигурации для отправки команд и чтения состояния мой Home Assistant использует один и тот же топик, а уже внутри прокси эти топики разделяются на два.


    Хак для MQTT discovery работает так: среди топиков Wiren Board есть специальные сервисные топики, которые описывают тип устройства. Насколько я понимаю, они используются только в стандартном интерфейсе управления. Они есть у каждой лампы и все равны switch. Все эти события retained и приходят один раз — в момент подключения. Я просто беру и подменяю все такие пакеты JSON-структурой, которая нужна для того, чтобы MQTT discovery подхватил эти устройства.


    Устройство Janus MQTT


    Сервис написан на Go и построен на базе библиотеки paho.mqtt.golang. Эта библиотека реализует MQTT-клиент для работы с брокером. Использовать её в качестве сервера никто не предполагал, поэтому его пришлось писать самому, используя части MQTT-клиента.


    Принцип работы очевидный: выдаём себя за брокера, принимаем пакеты от клиентов, изменяем их и отправляем настоящему брокеру. То же самое в обратную сторону. Т.е. получаем такой MITM.


    Самые интересные штуки:


    • поддержка различных уровней QoS — за счёт того что доставка на уровнях 2 и 1 осуществляется с подтверждением, получаем диалог клиента и сервера по каждому отправляемому сообщению;
    • из-за тех же QoS нужно генерить различные message_id, которые закодированы в uint16 и должны переиспользоваться;
    • Janus MQTT держит одно единственное подключение к брокеру и подписан на все сообщения. Уже внутри себя он разбирает подписки клиентов и присылает им только то, что нужно каждому из них;
    • не хотелось делать хранилище сообщений внутри сервиса, однако пришлось сделать in-memory хранилище retained-сообщений.

    Обработка MQTT-пакетов от клиентов описана в функции client.serveIncoming, она запускается в горутине и читает из TCP-сокета. В этой функции описана высокоуровневая логика обработки MQTT-пакетов от клиентов:


    • ConnectPacket — аутентифицировать пользователя и вернуть Connack с подтверждением или ошибкой;
    • SubscribePacket — подтвердить подписку отправкой Suback и отправить retained-сообщения;
    • UnsubscribePacket — подтвердить отмену подписки отправкой Unsuback и отменить подписку;
    • PingreqPacket — отправить обратно Pingresp;
    • PublishPacket — запустить автомат отправки сообщения в брокер;
    • PubackPacket, PubrelPacket, PubcompPacket передаются в автоматы отправки сообщений;
    • DisconnectPacket — дисконнект.

    Автоматы отправки сообщений в клиент и от клиента нужны только для поддержки различных уровней QoS. Всего в MQTT три уровня QoS:


    • QoS 0: отправка без подтверждения доставки;
    • QoS 1: отправка с подтверждением доставки;
    • QoS 2: отправка с подтверждением доставки один и только один раз.

    Как мне кажется, уровень 1 и тем более уровень 2 не имеют большого смысла дома, но, тем не менее, реализовать их было интересно (уровень 2 я немного не доделал, поэтому пока что обрезаю весь QoS уровнем 1, протокол это позволяет).


    Обработка publish-пакета от клиента с QOS=2


    Обработка publish-пакета от брокера с QOS=2


    Мой сценарий использования


    Janus MQTT я использую для организации взаимодействия между тремя компонентами:


    • Wiren Board — основной контроллер;
    • Home Assistant — фронтенд (у него удобное приложение);
    • Yandex2mqtt — голосовое управление.

    Wiren Board


    Содержит кучу разных реле и датчиков. В нём же крутятся основные скрипты управления всем. На нём работает MQTT-брокер.


    Yandex2mqtt


    Реализует oAuth для аутентификации и шлюз для взаимодействия с API умного дома Яндекса. Я решил не тратить на это время и взял готовый компонент. Изначально его написал munrexio, потом bawdiest обернул в докер, а я немножко поправил.


    Пока что в конфиге одна единственная лампочка, но зато самая важная:


    $ mosquitto_sub -h localhost -u yandex2mqtt -P yandex2mqtt -t '#' -v
    /light/LIVING_TABLE/state 1


    Home Assistant


    Фронтенд. Собирает статку и рисует разные графики. Позволяет управлять как светом, так и отоплением.


    Заниматься настройкой Home Assistant мне не хочется, мне нужно, чтобы я запустил его и там сразу появились все мои устройства.


    К счастью, в Home Assistant есть MQTT Discovery — это специальный режим, когда Home Assistant получает конфиг устройств прямо из MQTT. Там нужно создать специальные топики с JSON-структурами, описывающими устройства.


    В итоге весь конфиг Home Assistant сводится к:


    mqtt:
      username: !env_var MQTT_USER
      password: !env_var MQTT_PASS
      broker: !env_var MQTT_HOST
      discovery: true
      discovery_prefix: /homeassistant

    Janus MQTT


    Конфиг Janus MQTT для Home Assistant самый сложный. Однако он уложился в 100 строчек, при том что описывает 17 групп освещения, 6 термостатов с обратной связью по состоянию (вкл/выкл) и по температуре в комнате и 5 тёплых полов. Часть этого конфига приведена выше. Полный конфиг можно посмотреть тут.


    Docker


    Все сервисы работают в докере на NanoPi и описаны в docker-compose.yml.



    В завершение


    У меня сервис безостановочно работает с января, была пара мелких багов, которые я поправил, но в целом всё хорошо.
    Весь докеробраз сервиса весит порядка 10 Mб. Слава Golang!


    Сервис можно скачать/посмотреть тут :
    https://github.com/phoenix-mstu/janus-mqtt-proxy.


    Файл docker-compose можно посмотреть тут:
    https://github.com/phoenix-mstu/smart_home/tree/master/raspberry.


    И самое интересное, сервис можно пощупать вот тут: 52.59.242.204:26927 (уже выключено), логин/пароль — habr/habr, протокол — MQTT, конечно же. Там настроен проброс порта в мою локалку. Специально для статьи запущен отдельный инстанс сервиса в докере со специальным конфигом, там можно:


    1. Управлять светом в моей кладовке (любопытно будет посмотреть на светомузыку).
    2. Оставить сообщение.

    В качестве брокера выступает мой основной брокер-сервер — посмотрим, сломается или нет. Я, конечно, предпринял ряд мер для безопасности. Понятно, что DoS-атаку оно не выдержит — вы просто забьёте узкий канал. Будьте разумны.

    FunCorp
    Разработка развлекательных сервисов

    Комментарии 3

      0

      Интересно узнать полосу пропускания. Хотя бы теоретическую на таком железе…

        0
        Какие термостаты использовали, как они управляются? Изучив бегло тему, я пришёл к выводу, что проще и дешевле купить OWEN ПЛК63, и настроить управление тёплыми полами через него, потому как у меня получается хитрая логика. В гостиной хочу подогревать на самом дешёвом тарифе, а потом поддерживать. На балконе рулить в зависимости от температуры воздуха и текущего тарифа. В ванной в зависимости от тарифа, времени, влажности…
          0
          У меня установлены газовый котёл, жидкостное отопление и жидкостные тёплые полы. Управляется всё термоэлектрическими головками на распределительной гребёнке. Датчики двух типов: датчики температуры и влажности в комнатах, и датчики температуры жидкости в подаче и обратке каждого отопительного прибора.
          В качестве контроллера используется WirenBoard.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое