(Не)очередной MQTT телеграм бот для IoT

от автора

Всем привет! Сегодня хочу поделиться опытом разработки универсального телеграм бота для получения информации и управления IoT устройствами посредством протокола MQTT.

Почему (не)очередной? Потому что это не просто бот с двумя захардкоженными кнопками для управление лампочкой, примеров которых в интернете много, а это бот, который поддерживает гибкую настройку подписок и компанд для управления прямо из своего меню, без изменения исходного кода. NoCode solution, так сказать.

Бот разрабатывался на языке Go, исходный код выложен в свободный доступ на гитхаб под лицензией MIT. В статье хочу рассказать о некоторых технических моментах реализации и получившемся функционале с примерами использования.

Зачем?

Тут сошлись несколько факторов. Во-первых, я давно хотел перевести свой умный домофон на что-то более легкое и универсальное. Одним телеграм ботом покрывается весь зоопарк устройств и операционных систем, оставляя проблемы совместимости товарищу Дурову. Наконец-то смогу открывать домофон с ноутбука.

Во-вторых, очень хотелось попрактиковаться в написании бота, меня очень привлекает сама идея управления чем-либо через месседжер. Как, например, Вастрик в своем клубе сделал админку через телеграм, это ли не круто?

Функционал

Писать бота с несколькими захардкоженными кнопками — скучно. Решил сделать более универсальное решение с кастомизированным пользовательским меню и возможностью выводить графики.

Итого, минимальной целью был следующий функционал:

  1. Многоуровневое пользовательское меню с возможностью создания следующих кнопок:
    • Каталог (как раз для древовидного меню)
    • Команда с отправкой одного значения
    • Переключатель (отправка чередующихся значений)
    • Команда с несколькими значениями (выпадает меню на выбор)
    • Показать последнее принятое сообщение для топика
    • Построить график для подписки
  2. Подписка на произвольные топики (в т.ч приём изображений)
  3. Хранение истории значений
  4. Ручная отправка сообщения в топик

Весь этот функционал доступен прямо из меню бота без необходимости внесения изменений в исходный код. На лету редактируем меню и пользуемся:

Можете попробовать мою копию бота по адресу @mqtg-bot, но он в любой момент может упасть под нагрузкой, т.к развернут на бесплатном Heroku. Если вдруг не отвечает на /start, то так оно и случилось. Вы можете запустить своего бота по инструкции в README.

Переключатель

Самое простое, что можно было придумать, это включать/выключать реле. Для этих целей я даже собрал небольшую декоративную настольную лампу с WiFi реле Sonoff Mini внутри. Добавляем в меню бота переключатель и вуаля:

Гифка бесконечно моргающей лампы

Построение графиков

Как мне кажется, каждый человек, заинтересовавшийся темой интернета-вещей, первым делом подключает к ардуино или esp датчик влажности и температуры. Мы вот с женой таким образом следили за влажностью воздуха в квартире после рождения дочки, ибо в зимний период при централизованном отоплении дома это было проблемой.

Отправим/примем эти данные по MQTT и построим график:

Прием фото с камеры

Один из примеров использования бота — получение изображений с IP камеры. По MQTT можно передавать любые бинарные данные в пределах 256Мб на одно сообщение. Вариантов использования тут может быть много. Например, можно делать захват нескольких изображений с IP камеры по сигналу с датчика движения и отправлять фотографии себе в телеграм. Я в качестве примера сделал отправку изображений с модуля ESP32-CAM по запросу. Ардуиновский скетч лежит в папке examples репозитория (он же отправляет данные с датчика влажности и температуры AM2301).

Вот как это выглядит:

Пара особенностей реализации

Больше всего времени заняло написание редактируемого пользовательского меню, его хранения и загрузки.

В качестве основы я создал интерфейс ButtonI, который описывает ряд методов, необходимых для работы кнопок.

Интерфейс кнопки с небольшими пояснениями:

type ButtonI interface {   GetType() button_types.ButtonType // каждя кнопка должна знать свой тип для маршаллинга    GetName() string    GetFullName() string // пришлось добавить отдельный метод только для кнопки TOGGLE, т.к иногда есть потребность выводить ее сдвоенное имя    // методы для работы с "командами" кнопок, это те действия, которые выполняются при нажатии   GetCurrentCommand() *CommandType   GetCommands() []*CommandType   AddNewCommand(*CommandType)   DeleteCommand(int)    // .... тут есть еще несколько методов ....    // методы для работы с деревом меню   SetParent(*FolderButton)   GetParent() *FolderButton    GetButtons() *[]ButtonI   AddButton(ButtonI)   DelButton(int32)    // переопределение методов маршаллинга/анмаршаллинга json   MarshalJSON() ([]byte, error)   UnmarshalJSON([]byte) error }

Сериализация/десериализация меню

Как вы могли заметить, интерфейс переопределяет методы Marshal/Unmarshal JSON, они используются для сохранения/загрузки пользовательского меню в/из базы.

При сохранении каждому объекту обязательно проставляется его тип, чтобы можно было потом десериализовать.

func (b *FolderButton) MarshalJSON() ([]byte, error) {   b.Type = b.GetType()   return json.Marshal(*b) }

С десериализацией немного сложнее, т.к заранее не известна структура пользовательского меню. Приходится рекурсивно десериализовать json в map[string]interface{} и кастовать каждый объект в кнопку определенного типа.

Небольшая портянка кода парсера

func parseDataMap(dataMap *map[string]interface{}) ButtonI {   var outButton ButtonI    fType, _ := (*dataMap)["Type"].(float64)    switch button_types.ButtonType(fType) {   case button_types.MULTI_VALUE:     var multiValueButton MultiValueButton     // заполнение нужных полей     outButton = &multiValueButton    case button_types.SINGLE_VALUE:     var singleValueButton SingleValueButton     // заполнение нужных полей     outButton = &singleValueButton    case button_types.TOGGLE:     var toggleButton ToggleButton     // заполнение нужных полей     outButton = &toggleButton    case button_types.FOLDER:     var folderButton FolderButton     folderButton.Name, _ = (*dataMap)["Name"].(string)     buttons, _ := (*dataMap)["Buttons"].([]interface{})      // рекурсивный вызов парсинга для дерева     folderButton.Buttons = make([]ButtonI, 0, len(buttons))     for _, button := range buttons {       buttonMap, ok := button.(map[string]interface{})       if ok {         ButtonI := parseDataMap(&buttonMap)         folderButton.Buttons = append(folderButton.Buttons, ButtonI)       }     }      outButton = &folderButton    case button_types.SYSTEM:     var systemButton SystemButton     // заполнение нужных полей     outButton = &systemButton    case button_types.PRINT_LAST_SUB_VALUE:     var printLastValueButton PrintLastValueButton     // заполнение нужных полей     outButton = &printLastValueButton    case button_types.DRAW_CHART:     var showCharButton DrawChartButton     // заполнение нужных полей     outButton = &showCharButton    return outButton }

Есть огромное предчувствие, что сохранять/восстанавливать меню таким способом немного костыльно, но лучше пока ничего не придумал. Буду искренне рад советам, как можно хранить пользовательское меню с разными типами кнопок и потенциально неограниченной вложенностью.

Кодируем информацию в коллбэках

Длина данных в коллбэке inline клавиатуры в телеграме (это меню, прикрепленное к сообщению) ограничина 64 символами.

Для добавления в коллбэк к каждой кнопке всей необходимой информации я завел структуру следующего вида:

type QueryDataType struct {   MessageId   int64   Keyboard    KeyboardType  // тип inline клавиатуры   Path        []int32       // где мы находимся (для навигации по многоуровневому меню)   Action      ActionType    // основное действие   IntValue    int32         //    BoolValue   bool          // вспомогательные данные к действию   Index       int32         //  }

Данная структура сериализуется в protobuf и кодируется в base64. Итого 64 символа мне вполне хватает. А как бы сделали вы?

Что дальше?

Есть следующие идеи по улучшению функционала:

  1. Более гибкая настройка графиков (имена для легенд, мин/макс значения для осей, пользовательский формат временной оси и т.д)
  2. Управление историей сохраненных сообщений
  3. Ответное действие на принятое сообщение (например, отправка команды при получении определенных данных в топик)
  4. Парсинг и хранение только нужных данных из сообщения
  5. Прием аудио данных

Буду рад любым вашим идеям в комментариях.

Заключение

Надеюсь, что статья была хоть немного полезна и не выглядит самопиаром. Славы и денег не ищу, это лишь мой небольшой вклад в open source и хабрасообщество. Два года назад я с головой ушел в веб-разработку, но все еще немного скучаю по железу, так и родился данный проект. Конечно, бот может быть еще сыроватым, но по реакции на статью я смогу понять, нужно это кому-то или нет, и стоит ли мне развивать его дальше. Всем добра!

Ссылки:

  1. mqtg-bot — github репозиторий
  2. @mqtg-bot — моя копия бота
  3. NoCode — интересная статья от Вастрика

ссылка на оригинал статьи https://habr.com/ru/post/526672/


Комментарии

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

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