Задача
Нужно управлять различными устройствами: свет, вентиляция, полив, а также получать нужные данные от микроконтроллера.
При этом для учебно-тренировочных или DIY-задач совершенно не хочется задействовать дополнительные устройства, на которых будет размещаться сервер и уж тем более не оплачивать внешний статический IP-адрес.
Идея
Обеспечить выход в интернет с микроконтроллера, запустить два скрипта: веб-сервер для приема информации от микроконтроллера и телеграм-бот для связи с пользователем.
Веб-сервер на Flask и бота будем размещать на ReplIt. Как это сделать бесплатно с работой 24/7 описано в статье Как хостить телеграм-бота.
Первая попытка была использовать Arduino + Ethernet-модуль W5500, эта связка заработала только внутри локальной сети и провалилась при переносе на ReplIt, т.к. ссылка веб-сервера оказалась доступна только по протоколу https, который Arduino не поддерживает.
Решение нашлось в виде платы NodeMCU v3 с модулем Wi-Fi:

Эта плата, основанная на микроконтроллере 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 и наслаждаемся первым шагом к умному дому, сделанному своими руками.
Дальше полет фантазии в реализации не ограничен!
Такие дела! Успехов!
