В этой статье мы рассмотрим создание Telegram-бота, который позволяет управлять устройствами на базе ESP32 с использованием протокола MQTT. Проект также включает функции мониторинга и уведомлений через Telegram, а в качестве платформы для MQTT-брокера используется Raspberry Pi.
Основные функции проекта
- Telegram-бот для управления ESP32: Бот принимает команды от пользователя и взаимодействует с ESP32 через MQTT.
- Keep-Alive мониторинг: Слежение за подключением ESP32 и отправка уведомлений в Telegram в случае отключения.
- Сохранение данных: Чтение данных с датчика BME280 (температура, влажность, давление) и сохранение их в базу данных каждые 15 минут.
- Watchdog для ESP32: Предотвращение зависания устройства.
- Ограничение доступа: Бот настроен таким образом, что только один пользователь может отправлять ему команды.
Теперь разберем пошаговую инструкцию по настройке проекта.
Настройка Raspberry Pi
- Установка Mosquitto (MQTT-брокера):
sudo apt install mosquitto mosquitto-clients - Включение автозапуска Mosquitto:
sudo systemctl enable mosquitto sudo systemctl start mosquitto - Определение IP-адреса Raspberry Pi: Выполните команду:
hostname -IСохраните IP-адрес для последующего использования в настройках ESP32. - Проверка статуса Mosquitto: Убедитесь, что брокер MQTT работает корректно:
sudo systemctl status mosquitto
Создание Telegram-бота
- Создание бота в Telegram:
- Найдите в Telegram пользователя
BotFather. - Отправьте команду
/start, затем/newbot. - Следуйте инструкциям, чтобы создать нового бота.
- Найдите в Telegram пользователя
- Получение токена API: После создания бота BotFather предоставит токен API. Скопируйте его и вставьте в файл
config.pyпроекта. - Получение вашего ID в Telegram: Используйте команду
/getidв чате с ботом, чтобы узнать ваш Telegram ID. Добавьте этот ID в настройки бота для ограничения доступа.
Настройка ESP32
- Редактирование настроек: Перед загрузкой кода на ESP32 укажите следующие параметры:
- SSID вашей Wi-Fi сети.
- Пароль от Wi-Fi.
- IP-адрес вашего Raspberry Pi (указан в настройках Mosquitto).
- Управление светодиодом Neopixel: В проекте используется светодиод Neopixel, которым можно управлять через команды в Telegram-боте. Убедитесь, что он подключен к ESP32 в соответствии с описанием в коде.
Как это работает
После завершения всех настроек Telegram-бот будет готов к работе. Вы сможете:
- Отправлять команды через Telegram для управления ESP32.
- Получать уведомления о состоянии устройства.
- Сохранять данные с датчиков в базу данных.
Вот несколько примеров команд, которые можно отправить боту:
- Включить/выключить светодиод:
/led onили/led off. - Запросить текущие данные с датчика:
/getdata.
Итог
Этот проект позволяет легко организовать управление умными устройствами через Telegram. Использование Raspberry Pi в качестве MQTT-брокера и сервера делает систему гибкой и надежной, а благодаря Keep-Alive мониторингу вы всегда будете в курсе состояния вашего оборудования.
Блок программ
Main.py
import sys
import os
import logging
from telegram_bot import start_bot
from mqtt_client import start_mqtt, send_mqtt_command
logging.basicConfig(filename='bot.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def main():
try:
# Cambiar al directorio solo si existe
if os.path.isdir('/home/alvaro/bot'):
os.chdir('/home/alvaro/bot')
else:
logging.error("Directorio no encontrado. Verifica la ruta.")
sys.exit(1)
# Inicia el bot de Telegram y MQTT
bot = start_bot()
start_mqtt(bot)
while True:
try:
print("\nOpciones:")
print("1 - Encender rojo")
print("2 - Encender blanco")
print("3 - Apagar")
print("4 - Temperatura")
print("5 - Led_arcoiris")
print("99 - Salir")
print("")
option = input("Selecciona una opcin: ")
commands = {
'1': 'led_rojo',
'2': 'led_blanco',
'3': 'led_apagar',
'4': 'temperatura_habitacion',
'5': 'led_arcoiris'
}
if option in commands:
send_mqtt_command(commands[option])
elif option == '99':
logging.info("Saliendo del programa.")
break
else:
print("Opcin no vlida")
except KeyboardInterrupt:
logging.info("Interrupcin por teclado.")
break
except Exception as e:
logging.error("Error en el men principal: %s", e)
except Exception as e:
logging.critical("Error en la inicializacin: %s", e)
if __name__ == '__main__':
main()
DataBase.py
import sqlite3
def create_db():
with sqlite3.connect('clima_db.db') as conn:
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS clima_db
(Time TEXT, Location TEXT, Temperature REAL, Humidity REAL, Pressure REAL)''')
conn.commit()
def update_db(timestamp, location, temperature, humidity, pressure):
try:
with sqlite3.connect('clima_db.db') as conn:
c = conn.cursor()
c.execute("INSERT INTO clima_db VALUES (?,?,?,?,?)", (timestamp, location, temperature, humidity, pressure))
conn.commit()
except sqlite3.Error as e:
print(f"Error en la base de datos: {e}")
def delete_db(time, location):
try:
with sqlite3.connect('clima_db.db') as conn:
c = conn.cursor()
c.execute("DELETE FROM clima_db WHERE Time = ? AND Location = ?", (time, location))
conn.commit()
except sqlite3.Error as e:
print(f"Error al eliminar de la base de datos: {e}")
def select_db():
with sqlite3.connect('clima_db.db') as conn:
c = conn.cursor()
c.execute("SELECT * FROM clima_db")
return c.fetchall()
MQTT_client.py
import paho.mqtt.client as mqtt
import config
import time
from threading import Timer, Lock
from db import create_db, update_db
import json
client = mqtt.Client()
last_status_time = time.time()
KEEP_ALIVE_INTERVAL = 60 * 60
lock = Lock() # Para asegurar concurrencia
def check_keep_alive(bot):
with lock:
current_time = time.time()
if current_time - last_status_time > KEEP_ALIVE_INTERVAL:
bot.sendMessage(config.PERSONAL_ID, "El servidor de tu habitacin no conecta")
# Reprograma el temporizador de forma segura
Timer(KEEP_ALIVE_INTERVAL, check_keep_alive, [bot]).start()
def on_connect(client, userdata, flags, rc):
try:
if rc == 0:
print("Conectado exitosamente al broker MQTT")
client.subscribe(config.DATA_TOPIC)
client.subscribe(config.STATUS_TOPIC)
client.subscribe(config.CLIMA_TOPIC)
else:
print(f"Conexin fallida, cdigo de error: {rc}")
except Exception as e:
print(f"Error en on_connect: {e}")
def on_message(client, userdata, msg):
global last_status_time
try:
payload = msg.payload.decode()
if msg.topic == config.DATA_TOPIC:
print(f"Datos recibidos de ESP32: {payload}")
if userdata['bot']:
userdata['bot'].sendMessage(config.PERSONAL_ID, payload)
elif msg.topic == config.STATUS_TOPIC:
print(f"Estado recibido del ESP32: {payload}")
last_status_time = time.time()
elif msg.topic == config.CLIMA_TOPIC:
print(f"Estado de clima recibido: {payload}")
try:
clima_data = json.loads(payload)
temperature = clima_data["temperature"]
humidity = clima_data["humidity"]
pressure = clima_data["pressure"]
create_db()
update_db(int(time.time()), 'Habitacin', temperature, humidity, pressure)
except json.JSONDecodeError as e:
print(f"Error al decodificar JSON: {e}")
except Exception as e:
print(f"Error en on_message: {e}")
def send_mqtt_command(command):
try:
client.publish(config.COMMAND_TOPIC, command)
print(f"Comando enviado: {command}")
except Exception as e:
print(f"Error al enviar comando MQTT: {e}")
def start_mqtt(bot):
try:
client.user_data_set({'bot': bot})
client.on_connect = on_connect
client.on_message = on_message
client.connect(config.MQTT_BROKER, config.MQTT_PORT, 60)
client.loop_start()
print('Cliente MQTT conectado y escuchando.')
Timer(KEEP_ALIVE_INTERVAL, check_keep_alive, [bot]).start()
except Exception as e:
print(f"Error en start_mqtt: {e}")
def stop_mqtt():
try:
client.loop_stop()
client.disconnect()
print("Cliente MQTT desconectado.")
except Exception as e:
print(f"Error al detener MQTT: {e}")
Telegram_bot.py
import telepot
import config
from mqtt_client import send_mqtt_command, stop_mqtt
import subprocess
def handle_telegram(msg):
chat_id = msg['chat']['id']
command = msg['text']
# Verificacin de usuario autorizado
if chat_id != config.PERSONAL_ID:
try:
bot.sendMessage(chat_id, 'Usuario no autorizado. Contacta el administrador.')
except Exception as e:
print(f"Error al enviar mensaje de usuario no autorizado: {e}")
return
print(f'Comando recibido: {command}')
#Comandos y sus funciones asociadas
commands = {
'/Led_rojo': lambda: send_mqtt_command('led_rojo'),
'/Led_blanco': lambda: send_mqtt_command('led_blanco'),
'/Led_apagar': lambda: send_mqtt_command('led_apagar'),
'/Led_noche': lambda: send_mqtt_command('led_noche'),
'/Temperatura': lambda: send_mqtt_command('temperatura_habitacion'),
'/Led_arcoiris': lambda: send_mqtt_command('led_arcoiris'),
'/Exit': lambda: shutdown_system(chat_id)
}
if command.startswith('/Brillo_'):
brightness_level = command[1:].lower()
send_mqtt_command(brightness_level)
print(f"Cambiando el brillo a {brightness_level}")
return
if command in commands:
try:
commands[command]()
except Exception as e:
print(f"Error al ejecutar el comando {command}: {e}")
bot.sendMessage(chat_id, 'Hubo un problema ejecutando el comando. Intntalo de nuevo.')
else:
bot.sendMessage(chat_id, 'Comando no reconocido. Intenta de nuevo.')
def shutdown_system(chat_id):
try:
bot.sendMessage(chat_id, 'Sistema detenido. Hasta luego!')
print("Apagando el sistema de forma ordenada...")
stop_mqtt()
subprocess.run(["sudo", "shutdown"], check=True)
except Exception as e:
print(f"Error al intentar apagar el sistema: {e}")
def start_bot():
try:
global bot
bot = telepot.Bot(config.TELEGRAM_TOKEN)
bot.message_loop(handle_telegram)
bot.sendMessage(config.PERSONAL_ID, 'Sistema funcionando ')
lista_de_comandos = '\n'.join(['/Led_rojo', '/Led_blanco', '/Led_apagar', '/Temperatura', 'Led_arcoiris'])
bot.sendMessage(config.PERSONAL_ID, f'Lista de comandos disponibles:\n{lista_de_comandos}')
print('Bot de Telegram iniciado y en espera de comandos.')
return bot
except Exception as e:
print(f"Error al iniciar el bot: {e}")
Confing.py
# config.py # ID de usuario autorizado para Telegram PERSONAL_ID = "Your_ID" # Token de acceso para el bot de Telegram TELEGRAM_TOKEN = 'API_TOKEN' # Configuracin de conexin para MQTT MQTT_BROKER = "Raspberry Pi IP" MQTT_PORT = 1883 # Temas MQTT DATA_TOPIC = "esp32/data" COMMAND_TOPIC = "esp32/commands" STATUS_TOPIC = "esp32/status" CLIMA_TOPIC = "esp32/clima"
ESP32MQTT.ino
#include <WiFi.h> //MQTT
#include <PubSubClient.h> //MQTT
#include <FastLED.h> //Neopixel
#include <Wire.h> //BME280
//#include <Adafruit_Sensor.h> //BME280
#include <Adafruit_BME280.h> //BME280
#include <esp_task_wdt.h> //Watchdog
// Configuracin FastLED
#define LED_PIN 27
#define NUM_LEDS 115
#define BRIGHTNESS 64
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
#define BME280_ADDRESS 0x76 //Direccin I2C del BME280
// Crear una instancia del sensor BME280
Adafruit_BME280 bme;
//Configuracin WiFi + MQTT
const char* ssid = "YourSSID";
const char* password = "PASSWORD!";
const char* mqtt_server = "Raspberry_PI_IP"; // IP del broker MQTT
//Crear una instancia para el wifi
WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMsg = 0; // Variable para controlar el tiempo del ltimo mensaje de delay no bloqueante
const long publish_interval = 900000; // 900000 Intervalo para enviar mensajes (15 minutos)
bool firstMessageSent = false; //Variable para controlar que el mensaje de funcionamiento solo se enve una vez
void efectoArcoiris()
{
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV((millis() / 10 + i * 10) % 255, 255, 255);
}
FastLED.show();
}
// Funcin de callback cuando se recibe un mensaje MQTT
void callback(char* topic, byte* payload, unsigned int length)
{
String message;
for (int i = 0; i < length; i++)
{
message += (char)payload[i];
}
Serial.println(message);
if (String(topic) == "esp32/commands")
{
if (message == "temperatura_habitacion")
{
float temperature = bme.readTemperature(); // Leer temperatura en C
float humidity = bme.readHumidity(); // Leer humedad en %
float pressure = bme.readPressure() / 100.0F; // Leer presin en hPa
String payload = String("{\"temperature\":") + temperature + ", \"humidity\":" + humidity + ", \"pressure\":" + pressure + "}";
client.publish("esp32/data", payload.c_str());
Serial.println("Datos enviados: " + payload);
}
else if (message == "LED_OFF")
{
digitalWrite(LED_PIN, LOW); // Apagar LED
Serial.println("LED APAGADO");
}
else if (message == "led_rojo")
{
FastLED.clear();
fill_solid(leds, NUM_LEDS, CRGB::Red);
// leds[1] = CRGB::Green; //El segundo led se enciende en verde
FastLED.show();
}
else if (message == "led_blanco")
{
FastLED.clear();
fill_solid(leds, NUM_LEDS, CRGB::White);
FastLED.show();
}
else if (message == "led_apagar")
{
FastLED.clear();
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
}
else if (message.startsWith("brillo_"))
{
int brillo = message.substring(7).toInt(); // "7" es la posicin del primer dgito
brillo = constrain(brillo, 0, 255); //Limitamos el valor entre 0 y 255
FastLED.setBrightness(brillo);
FastLED.show();
Serial.print("Ajustando brillo a: ");
Serial.println(brillo);
}
else if (message == "led_arcoiris")
{
efectoArcoiris();
}
}
}
// Conectar al WiFi
void setup_wifi()
{
delay(10);
Serial.println();
Serial.print("Conectando a ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi conectado");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
}
void setup_bme280()
{
Serial.println("BME280 sensor test");
Wire.begin(21, 22); // SDA (21), SCL (22) // Iniciar la comunicacin I2C
// Iniciar el sensor BME280
if (!bme.begin(BME280_ADDRESS))
{
Serial.println("No se encontr el sensor BME280, verifica la conexin!");
// while (1); //No queremos que sea bloqueante
}
else
{
Serial.println("Sensor BME280 inicializado correctamente.");
}
}
// Conectar al broker MQTT
void reconnect_MQTT_server()
{
while (!client.connected())
{
Serial.print("Intentando conexin MQTT...");
if (client.connect("ESP32Client"))
{
Serial.println("conectado");
client.subscribe("esp32/commands"); // Suscribirse a comandos
}
else
{
Serial.print("fallo, rc=");
Serial.print(client.state());
Serial.println(" intentar de nuevo en 5 segundos");
delay(5000);
}
}
}
void setup()
{
Serial.begin(115200); //Inicializamos el puerto serie
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
//---- Inicializar los LED ----
pinMode(LED_PIN, OUTPUT);
FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
FastLED.setBrightness(15); //256 max
FastLED.clear();
FastLED.show();
setup_bme280();
// Configurar el Watchdog
esp_task_wdt_config_t wdt_config = {
.timeout_ms = 10000, //Timeout de 10 segundos
.idle_core_mask = 0, //Para monitorear todos los ncleos, puedes usar `1` o `2` para monitorear solo un ncleo
.trigger_panic = true //Reiniciar si el watchdog llega a timeout
};
esp_task_wdt_init(&wdt_config);
esp_task_wdt_add(NULL); // Asociar el watchdog a la tarea actual
}
void loop()
{
if (!client.connected())
{
reconnect_MQTT_server();
}
client.loop();
esp_task_wdt_reset(); //Patear el watchdog
if(!firstMessageSent) //Utilizamos una flag ya que es ms eficiente
{
// Publicamos un mensaje oara saber que funciona correctamente
String payload = String("Cliente Habitacin publicando");
client.publish("esp32/data", payload.c_str());
Serial.println(payload);
firstMessageSent = true;
}
unsigned long now = millis();
if (now - lastMsg > publish_interval) // Verificar si ha pasado el intervalo de tiempo para enviar los datos; es como si fuese un delay pero no bloqueante
{
lastMsg = now;
// Leer los datos del sensor
float temperature = bme.readTemperature(); // Leer temperatura en C
float humidity = bme.readHumidity(); // Leer humedad en %
float pressure = bme.readPressure() / 100.0F; // Leer presin en hPa
// Publicar datos de temperatura, humedad y presin
String payload_clima = String("{\"temperature\":") + temperature + ", \"humidity\":" + humidity + ", \"pressure\":" + pressure + "}";
if (client.publish("esp32/clima", payload_clima.c_str())) {
Serial.println("Datos enviados: " + payload_clima);
}
else {
Serial.println("Error al enviar datos del clima");
}
// Publicar datos de keep alive
String payload_keepalive = String("Habitacion");
if (client.publish("esp32/status", payload_keepalive.c_str())){
Serial.println("Datos enviados: " + payload_keepalive);
}
else{
Serial.println("Error al enviar dato de keep alive");
}
esp_task_wdt_reset(); //Patear el watchdog despus de enviar los datos
}
}