Задача

Нужно управлять различными устройствами: свет, вентиляция, полив, а также получать нужные данные от микроконтроллера.
При этом для учебно-тренировочных или DIY-задач совершенно не хочется задействовать дополнительные устройства, на которых будет размещаться сервер и уж тем более не оплачивать внешний статический IP-адрес.

Идея

Обеспечить выход в интернет с микроконтроллера, запустить два скрипта: веб-сервер для приема информации от микроконтроллера и телеграм-бот для связи с пользователем.

Веб-сервер на Flask и бота будем размещать на ReplIt. Как это сделать бесплатно с работой 24/7 описано в статье Как хостить телеграм-бота.

Первая попытка была использовать Arduino + Ethernet-модуль W5500, эта связка заработала только внутри локальной сети и провалилась при переносе на ReplIt, т.к. ссылка веб-сервера оказалась доступна только по протоколу https, который Arduino не поддерживает.

Решение нашлось в виде платы NodeMCU v3 с модулем Wi-Fi:

NodeMCU v3 с WiFi-модулем
NodeMCU v3 с WiFi-модулем

Эта плата, основанная на микроконтроллере ESP8266, программируется через среду Arduino IDE с небольшой настройкой.

Реализация - шаг 0 - настраиваем IDE

Пример будет для версии Arduino IDE 2.0.3, которая легко доступна на официальном сайте.

После установки IDE необходимо добавить в нее библиотеки для работы с ESP8266.
Нужно зайти в настройки File→Preference:

и добавить ссылки для скачивания информации о дополнительных платах:
http://arduino.esp8266.com/stable/package_esp8266com_index.json
https://dl.espressif.com/dl/package_esp32_index.json

Далее открываем в левом меню BOARDS MANAGER и устанавливаем пакет для работы с ESP8266:

После этого IDE готова к работе. Но прежде, чем загрузить код в ESP, нужно создать два скрипта на Python и получить ссылку, по которой будет осуществляться обмен данными.

Реализация - шаг 1 - Пишем код телеграм-бота и веб-сервера на Flask

В данной статье будет описан принцип обмена данными, поэтому от ESP будет передаваться только "условная температура" (условная, потому что всегда статичная из переменной) и состояния встроенного в микроконтроллер светодиода.

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

Создаем проект на ReplIt, в нем делаем два файла main.py (в нем будет телеграм-бот) и background.py (в нем будет веб-сервер). Более подробно процесс описан в статье Как хостить телеграм-бота.

Telegram-бот

Размещаем в файле main.py.

Моменты, на которые стоит обратить внимание:

  • import pip pip.main(['install', 'pytelegrambotapi']) - установит необходимую нам библиотеку.

  • f.write(str(call.from_user.id)) - записываем id пользователя, с которым общаемся. В данной реализации предполагаем, что с ботом не будет одновременно работать несколько человек.

  • bot.send_message(call.from_user.id,"Свет скоро включится") - отправляем сообщение пользователю, что "команда принята". Чуть позже, когда от ESP придет информация, что свет включен, мы отправим еще одно подтверждение. Именно для этого и нужно запоминать user_id.

  • Токен от бота прячем в SECRETS, т.к. проект в бесплатном режиме открыт в режиме просмотра для всех. Подробнее об этом в документации.

import os
from background import keep_alive
import pip
pip.main(['install', 'pytelegrambotapi'])
import telebot
import time

#очищаем все файлы или создаем пустые
with open('messages.txt','w') as f:
      f.write("")
with open('user_id.txt','w') as f:
      f.write("")
with open('from_esp.txt','w') as f:
      f.write("")
with open('from_tg.txt','w') as f:
      f.write("")

def get_last_update(now,last):
  # функция для определения времени получения данных от микроконтроллера
  diff = now-last
  if diff<60:
    return f"{int(diff)} сек назад"
  elif diff<60*60:
    return f"{int(diff/60)} мин назад"
  elif diff<60*60*24:
    return f"{int(diff/60/24)} ч назад"  
  else:
    return "Более дня назад"


bot = telebot.TeleBot(os.environ['TOKEN'])# Создаем бот

# Создание клавиатур, для удобной коммуникации с пользователем
start_keyboard = telebot.types.InlineKeyboardMarkup()
start_keyboard.add(
    telebot.types.InlineKeyboardButton('Получить информацию', callback_data='info'),
    telebot.types.InlineKeyboardButton('Управлять устройством', callback_data='control')
)

control_keyboard = telebot.types.InlineKeyboardMarkup()
control_keyboard.add(
    telebot.types.InlineKeyboardButton('Включить', callback_data='on'),
    telebot.types.InlineKeyboardButton('Выключить', callback_data='off')
)
# Клавиатуры будут прикреплены к сообщениям бота

# На любое сообщение пользователя присылаем варианты действий
# Как вариант, обрабатывать команду /start от пользователя
@bot.message_handler(content_types=['text'])
def get_text_message(message):
  bot.send_message(message.from_user.id,"Что вы хотите сделать?",reply_markup=start_keyboard)

#Обработка нажатий на кнопки 
@bot.callback_query_handler(func=lambda call: True)
def func(call):
  bot.answer_callback_query(call.id) # подтверждаем боту, что действие по кнопке выполнено
  with open('user_id.txt','w') as f:
      f.write(str(call.from_user.id)) # записываем в файл user_id. Он понадобится для отправки сообщений
  if call.data=='info':#нажата кнопка с callback_data='info', получаем информацию из файла
    with open("from_esp.txt","r") as f:# читаем файл
      temp,light,time_last = f.readlines()[0].split(';')#получаем значения переменных
      last_update = get_last_update(time.time(),float(time_last))# и время получения данных
    #отправляем сообщение пользователю
    bot.send_message(call.from_user.id,f"Все хорошо, \nТемпература: {temp} \nОсвещение: {'включено' if light=='1' else 'выключено'}\nОбновлено: {last_update}", reply_markup=start_keyboard)
  if call.data=='control':#нажата кнопка с callback_data='control'
    bot.send_message(call.from_user.id,"Вот что можно сделать:",reply_markup=control_keyboard)
  if call.data=='on':#нажата кнопка с callback_data='on'
    bot.send_message(call.from_user.id,"Свет скоро включится")#отправляем сообщение пользователю
    with open('from_tg.txt','w') as f:#записываем в файл действие, которое хотим сделать
      f.write('1')#включить свет
  if call.data=='off':
    bot.send_message(call.from_user.id,"Свет скоро выключится")
    with open('from_tg.txt','w') as f:
      f.write('0')#выключить свет
  
keep_alive()# запуск веб-сервера из файла background.py
bot.polling(non_stop=True, interval=0)# запуск телеграм-бота

Веб-сервер на Flask

В этой статье не приводится полное описание, что такое Flask и как им пользоваться. Есть огромное количество статей на русском или английском об этом фреймворке. В качестве примера, можно почитать вот эту.

Размещаем код в файле background.py
Этот сервер выполняет сразу 2 задачи:

  • Обеспечивает обмен данными с микроконтроллером через GET-запросы.

  • Используется для поддержки работоспособности скрипта через UpTimeRobot. Подробности все в той же статье.

from flask import Flask
from flask import request
from threading import Thread
import time
import requests

app = Flask('')

@app.route('/')#Создаем "главную страницу" которую будет пинговать UpTimeRobot
def home():
  return "I'm alive"

@app.route('/iot', methods=['GET'])#создает ссылку /iot на которую будут приходить запросы 
def iot():
  temp = request.args.get('temp') #получаем параметры из GET-запроса
  light = request.args.get('light')
  with open('from_esp.txt', 'r') as f:#читаем данные, полученные из ESP в прошлый раз
    old_temp,old_light,time_last = f.readlines()[0].split(';')
    if old_light=="0" and light=="1":# и если старое состояние выкл, а новое вкл
      with open('messages.txt', 'w') as f_m:# записываем в файл messages текст сообщения
        f_m.write("Свет включился")
    if old_light=="1" and light=="0":
      with open('messages.txt', 'w') as f_m:
        f_m.write("Свет выключился")
  with open('from_esp.txt', 'w') as f:#записываем в файл новые значения
    f.write(f"{temp};{light};{time.time()}")
    
  with open('from_tg.txt', 'r') as f:# читаем из файла действие, сделанное командой в телеграм боте
    new_state = f.read(1) #т.к. у нас только 1 параметр Включить/выключить свет, читаем 1 символ
  return new_state #возвращаем значение
#Для Flask-сервера это означает, что прочитанный символ будет показан на веб-странице https://сайт/iot


def run(): #функция запуска flask-сервера
  app.run(host='0.0.0.0', port=80)

def reminder():
  while True:
    with open('user_id.txt','r') as f:#пытаемся прочитать user_id. Номер чата с пользователем
      lines = f.readlines()
      if len(lines)>0:
        chat_id = lines[0]
      else:
        chat_id = None
    with open('messages.txt','r') as f:# читаем файл с сообщением
      lines = f.readlines()
      if len(lines)>0 and chat_id is not None:#если есть user_id и сообщение
        text = lines[0]
        token = os.environ['TOKEN']   
        requests.get(r"https://api.telegram.org/bot"
                         +token
                         +r"/sendMessage?chat_id="+chat_id
                         +r"&text="+text)
        #отправляем сообщение по специальной ссылке с использованием токена
    with open('messages.txt','w') as f:
      f.write("")#очищаем файл с сообщениями
    time.sleep(0.3)

def keep_alive():# запускаем flask и reminder в отдельных потоках
  t = Thread(target=run)
  t.start()
  tr = Thread(target=reminder)
  tr.start()

Прекрасно! Теперь, когда все запустилось, нужно записать ссылку доступа к серверу и ключ шифрования для доступа по протоколу https.

Реализация - шаг 2 - код для ESP8266

После запуска сервера в правом верхнем углу экрана будет ссылка. Копируем ее и вставляем в браузер:

Нажимаем на иконку замка рядом с адресом сайта:

и просматриваем сертификат. Текст меню может немного отличаться в разных браузерах, но принцип остается тот же.

Копируем и сохраняем "отпечаток SHA-1":

Копируем код в Arduino IDE. Меняем параметры доступа к Wi-Fi, адрес сервера (указывается без https) и отпечаток SHA-1:

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h> 
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>

#define LED 2 

const char *ssid = "ИМЯ_WiFi_сети"; 
const char *password = "ППАРОЛЬ_WiFi_сети";

const char *host = "test.username.repl.co";//адрес сервера без https://
const int httpsPort = 443;

//отпечаток SHA-1, который скопировали раньше
const char fingerprint[] PROGMEM = "AA BB CC DD EE FF 00 11 22 33 44 55 66";

void setup() {
  pinMode(LED, OUTPUT);
  digitalWrite(LED, HIGH);
  delay(1000);
  Serial.begin(115200);
  WiFi.mode(WIFI_OFF);
  delay(1000);
  WiFi.mode(WIFI_STA);
  
  WiFi.begin(ssid, password);//подключаемся к сети
  Serial.println("");

  Serial.print("Connecting");

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }


  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP()); 
}


void loop() {
  WiFiClientSecure httpsClient; 

  Serial.println(host);

  Serial.printf("Using fingerprint '%s'\n", fingerprint);
  httpsClient.setFingerprint(fingerprint);
  httpsClient.setTimeout(500); 
  delay(1000);
  //подключение к серверу
  Serial.print("HTTPS Connecting");
  int r=0; 
  while((!httpsClient.connect(host, httpsPort)) && (r < 30)){
      delay(100);
      Serial.print(".");
      r++;
  }
  if(r==30) {
    Serial.println("Connection failed");
  }
  else {
    Serial.println("Connected to web");
  }
  
  String ADCData, getData, Link; 
//создаем переменные, значения которых будут передаваться на сервер
  int temp = 15;//условная температура, которую в дальнейшем можно получать с датчика
  int light = !digitalRead(LED);//состояние встроенного светодиода, которы и есть СВЕТ в данном проекте
  Link = "/iot?temp="+String(temp)+"&light="+String(light);//собираем ссылку из параметров

  Serial.print("requesting URL: ");
  Serial.println(host+Link);
// выполняем переход по этой ссылке
  httpsClient.print(String("GET ") + Link + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +               
               "Connection: close\r\n\r\n");

  Serial.println("request sent");
                  
  while (httpsClient.connected()) {
    String line = httpsClient.readStringUntil('\n');
    if (line == "\r") {
      Serial.println("headers received");
      break;
    }
  }

  Serial.print("reply:");
//в переменную line записываем ответ сервера. В данном случае 0 или 1, команда от телеграм-бота
  
  String line;
  while(httpsClient.available()){        
    line = httpsClient.readStringUntil('\n');
    Serial.println(line);
    if (line=="0") {//включаем или выключаем свет
      digitalWrite(LED, HIGH);
    }
    if (line=="1") {
      digitalWrite(LED, LOW);
    }
  }

  Serial.println("closing connection");
    
  delay(500);//ждем 0.5с и повторяем
}

Загружаем код в ESP и наслаждаемся первым шагом к умному дому, сделанному своими руками.

Дальше полет фантазии в реализации не ограничен!

Такие дела! Успехов!

Комментарии (11)


  1. ruslansh3i2w
    14.01.2023 20:23

    Вау, неплохо


  1. gregorybednov
    14.01.2023 20:39
    +1

    Безотносительно содержания, небольшое дополнение по предпосылкам этой статьи.

    Возможно, что это я не разобрался до конца, но вот, например, проблема аутентификации (или управлять устройствами и следить за показаниями, в том числе допустим камер, может кто угодно, кто нашёл бота?), является ли Replit бесплатным (или хотя бы более дешёвым, чем айпишник от домашнего провайдера) (и, кстати, если нет, то каким образом они вообще зарабатывают деньги, или они заведомо убыточны, если не считать вкладываемых инвестиций, и могут в любой момент "сдуться" вместе со всей инфраструктурой).

    При этом для учебно-тренировочных или DIY-задач совершенно не хочется
    задействовать дополнительные устройства, на которых будет размещаться
    сервер и уж тем более не оплачивать внешний статический ip-адрес.

    Касаемо внешнего статического ip-адреса: существуют такие сети, как I2P, Tor и Yggdrasil. К сожалению, не знаю, насколько пользование ими считается законным, но по сути это децентрализованные сети. Хотя, безусловно, для работы в таком случае потребуется серверное устройство (любое, хоть Raspberry Pi, хоть не очень старенький компьютер) - учитывая, что Вам всё равно потребовалось приобретать дополнительные устройства (пускай это и плата с wifi-модулем), тут получается оба решения требуют доп.вложений, совсем уж "без доп.устройств" не обойтись (хотя, если у вас уже есть свой собственный маршрутизатор, который прошивается OpenWRT, то в таком случае теоретически можно было бы попробовать обойтись совсем без доп.вложений).

    P.S. Из-за шифрования на уровне сети, Yggdrasil не требует https, ему достаточно http, а при автоматической настройке генерится IPv6 (причем из подмножества, которое точно никогда не будет занято в официальном Интернете).

    P.P.S. Если вдруг заинтересовал, то тут надо предупредить, что роутер Yggdrasil проходит "сквозь" NAT со всеми вытекающими последствиями, не только положительными (считай децентрализованный VPN и self-hosted домашний сервер - который, правда, не на весь интернет виден, а только тем кто знает про эту меш-сеть), но и потенциально отрицательными (становится теоретически возможным проникновение в локальную сеть за NAT).


    1. Stepan_Burmistrov Автор
      14.01.2023 21:17

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

      По поводу ReplIt - бесплатный он, если использовать некоторые нюансы, в виде UpTimeRobot. А так, там платные тарифы.

      По поводу сетей-да, решений много нужно придумать. При наличии свободной Raspberry можно сделать, но опять же, это повышает порог вхождения для начинающих.


    1. ShadF0x
      14.01.2023 22:15

      каким образом они вообще зарабатывают деньги

      Платные подписки с увеличенными объёмами предоставляемых ресурсов и фишками типа "always-on repls" (их не надо дёргать всякими UpTimeRobot'ами), и коммерческие лицензии.

      Одним днём не сдуются, но объём бесплатных ресурсов могут подрезать в любой момент.


  1. iShrimp
    14.01.2023 21:01

    Может я что-то не понимаю, но разве для таких простых задач нужен отдельный хостинг для бота? Есть куча библиотек, реализующих интерфейс Telegram Bot API для управления ботом с ESP-шки.


    1. Stepan_Burmistrov Автор
      14.01.2023 21:27

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


  1. sav13
    14.01.2023 21:38

    Мне тоже кажется избыточность данного решения.

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

    Ну и интересно было бы увидеть полностью работу ESP через Телеграм включая прошивку OTA (собственно загрузку файла) и прочее.


    1. Stepan_Burmistrov Автор
      14.01.2023 21:52

      Повторюсь: в данном контексте-возможно, но если устройств два? На каждое свой телеграм-бот нужен. А если 6 шт по дому развесить?

      По поводу работы и прошивки не понял, вы хотите увидеть как это работает?


  1. trapwalker
    15.01.2023 02:15
    +1

    А MQTT не рассматривали в качестве транспорта? Есть бесплатные сервисы.
    MQTT в таком случае возьмет на себя роль канала для получения свежего статуса и будет хранить состояние. Тогда телеграм будет не нужен вообще, ведь mqtt можно привязать напрямую к сценариям умного дома.


  1. vconst
    15.01.2023 04:50

    Любопытно
    В очередной раз ковыряюсь с ардуинками и датчиками, статья весьма кстати


  1. mxkmn
    17.01.2023 12:59

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

    2. Отпечаток SHA-1 быстро устаревает. Готовы перепрошиваться каждые 3 месяца? Думаю нет - в этом случае лучше взять полный ключ сертификата. По личному опыту скорость на ESP8266 не ухудшается, зато для кучи сервисов имеет актуальность до 2038 года.

    3. ESP8266 кривая и устаревшая, вроде как даже поддержка производителем прекращена. Про кривость и веселье я когда-нибудь обязательно напишу отдельную статью, но вот то, что есть в вашем проекте из подобного - .connect() на 8266 часто (очень часто!) приводит к крашу и перезагрузке при HTTPS подключении. Решения нет и проявляется на всех версиях фреймворка для Arduino как минимум три года (более старые версии и чистый ESP8266 SDK не проверял). Вернее решение имеется, простое, но из другой плоскости - забыть про ESP8266 и взять ESP32-C3/ESP32-S2 - современные микроконтроллеры того же инженерного бюро, но с кучей новых возможностей, мощностью выше ESP32 и гораздо надёжнее.

    В общем-то хорошо, что освящена возможность работать с микроконтроллерами через Telegram. Плохо, что максимально неоптимальным способом. Что-то на уровне умных теплиц из 2013-ого - сейчас можно проще и дешевле.