Telegram-бот на Raspberry Pi 5: уведомления от датчиков и управление командами

Raspberry Pi стоит где-то в шкафу и следит за температурой, дверью, датчиком воды. Как узнать, что что-то случилось, если вы не дома? Самый простой и бесплатный способ: чтобы плата сама написала вам в Telegram. А если научить её отвечать на команды, то из чата можно будет узнавать температуру, просить фотографию с камеры или включать устройство.

В статье сделаем Telegram-бота на Raspberry Pi 5 на Python. Сначала простое: функция, которая отправляет вам сообщение. Затем полноценный бот с командами /status и /photo, защитой от посторонних и запуском при включении платы. Эти приёмы подойдут для любого проекта: метеостанции, датчика движения или реле.

Доступность Telegram. Бот ходит на серверы api.telegram.org. Если из вашей сети этот адрес недоступен, бот работать не будет. Проверьте на плате: curl -s -o /dev/null -w "%{http_code}\n" https://api.telegram.org. Ответ 302 или другой HTTP-код означает, что связь есть, а таймаут — что нет. Если выход к Telegram у вас организован через прокси, библиотека python-telegram-bot умеет их использовать (параметр proxy у построителя приложения).


Шаг 1. Создаём бота

  1. В Telegram найдите бота @BotFather (официальный, с галочкой).
  2. Отправьте команду /newbot.
  3. Введите имя бота (любое, например «Мой Raspberry Pi») и уникальное имя пользователя, оканчивающееся на bot (например, my_rpi5_home_bot).
  4. BotFather выдаст токен (строка вида 123456789:ABC...). Это пароль к боту: любой, у кого он есть, сможет управлять ботом. Никому не показывайте его и не публикуйте в интернете.

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

Поиск @BotFather в Telegram: официальный бот с синей галочкой

Диалог создания бота: команда /newbot, имя, username и в последнем сообщении токен (на снимке он закрашен):

Диалог с BotFather: /newbot, имя бота, username и выданный токен

Если токен утёк, в BotFather команда /revoke выпускает новый.


Шаг 2. Узнаём свой chat_id

Боту нужно знать, кому писать. Для этого нужен идентификатор вашего чата.

  1. Найдите своего бота по имени пользователя и отправьте ему любое сообщение (например, «привет»).
  2. Откройте в браузере (подставьте свой токен):
https://api.telegram.org/botВАШ_ТОКЕН/getUpdates
  1. В ответе найдите блок "chat":{"id":123456789,...}. Число после id — ваш chat_id.

(Если ответ пустой [], отправьте боту сообщение ещё раз и обновите страницу.)

В Telegram найдите бота по username и откройте чат. Кнопка Start отправляет команду /start, после неё напишите любое сообщение (здесь «Hi!»):

Чат с ботом в Telegram: команда /start
Чат с ботом в Telegram: сообщение Hi!

Пока боту никто не писал, getUpdates возвращает пустой список ("result": []):

getUpdates в браузере: пустой ответ

После сообщения в result появляется блок message. Число id в блоке chat и есть ваш chat_id (для личной переписки оно совпадает с id в блоке from). На снимке вместо настоящих имени, username и номера стоят условные значения:

getUpdates в браузере: ответ с chat_id

Если токен вставлен неверно, вместо этого придёт ошибка 401 Unauthorized (см. таблицу в конце статьи).


Шаг 3. Установка библиотек

Библиотеки Python ставятся в виртуальное окружение (об окружениях):

mkdir -p ~/tgbot
cd ~/tgbot
python3 -m venv env
source env/bin/activate
pip install requests python-telegram-bot

requests — для простой отправки сообщений, python-telegram-bot — для полноценного бота с командами (нужен Python 3.10 или новее: в Raspberry Pi OS на базе Debian 12 и 13 он есть).

Токен и chat_id храните в отдельном файле, а не в тексте программы:

nano ~/tgbot/tg.env
TELEGRAM_TOKEN=123456789:ABC...
TELEGRAM_CHAT_ID=123456789
chmod 600 ~/tgbot/tg.env

Вариант 1. Просто отправить сообщение

Самый простой способ: обычный HTTPS-запрос без «ботов» и библиотек для работы с Telegram. Создайте notify.py:

import os

import requests

TOKEN = os.environ["TELEGRAM_TOKEN"]
CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]


def send(text):
    """Отправляет текстовое сообщение владельцу."""
    r = requests.post(
        f"https://api.telegram.org/bot{TOKEN}/sendMessage",
        data={"chat_id": CHAT_ID, "text": text},
        timeout=10,
    )
    r.raise_for_status()


if __name__ == "__main__":
    send("Raspberry Pi 5 на связи!")

Запуск (значения берутся из файла окружения):

set -a; source tg.env; set +a
python3 notify.py

В Telegram придёт сообщение. Команда set -a экспортирует переменные из файла в окружение.

Функцию send() можно подключить к любому скрипту: импортируйте её (from notify import send) и вызывайте, когда нужно.

Пример: уведомление о перегреве

Скрипт alert.py сообщает о высокой температуре процессора, и о том, что она вернулась в норму. Чтобы не получать десятки сообщений, используется гистерезис: сообщение приходит один раз при превышении порога и один раз при возвращении:

import time

from notify import send

LIMIT = 70.0      # порог тревоги, °C
RESET = 60.0      # температура, когда считаем, что всё в порядке


def cpu_temp():
    with open("/sys/class/thermal/thermal_zone0/temp") as f:
        return int(f.read()) / 1000


alarm = False
while True:
    t = cpu_temp()
    try:
        if not alarm and t >= LIMIT:
            alarm = True
            send(f"⚠️ Перегрев: процессор {t:.1f} °C")
        elif alarm and t <= RESET:
            alarm = False
            send(f"✅ Температура в норме: {t:.1f} °C")
    except Exception as error:      # нет сети или Telegram недоступен: попробуем позже
        print("Не удалось отправить:", error, flush=True)
    time.sleep(30)

Чтобы проверить оповещение, не дожидаясь настоящего перегрева, временно поставьте LIMIT чуть выше текущей температуры (она видна в vcgencmd measure_temp) и нагрузите процессор: stress-ng --cpu 4 --timeout 120s (пакет stress-ng). Так выглядит чат: сначала сообщение от notify.py, затем уведомление от alert.py:

Telegram: сообщения «на связи» и «Перегрев: процессор»

Тот же шаблон работает для любого датчика: замените cpu_temp() на чтение DHT22 или BME280 (метеостанция), а порог на нужный. Для событий (сработал датчик движения) отправляйте сообщение при срабатывании, но не чаще раза в минуту.


Вариант 2. Бот с командами

Теперь научим плату отвечать на команды. Создайте bot.py:

import asyncio
import os

from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes

TOKEN = os.environ["TELEGRAM_TOKEN"]
OWNER = int(os.environ["TELEGRAM_CHAT_ID"])


def allowed(update: Update) -> bool:
    """Отвечаем только владельцу."""
    return update.effective_chat is not None and update.effective_chat.id == OWNER


def cpu_temp() -> float:
    with open("/sys/class/thermal/thermal_zone0/temp") as f:
        return int(f.read()) / 1000


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not allowed(update):
        return
    await update.message.reply_text("Привет! Команды: /status, /photo")


async def status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not allowed(update):
        return
    load1, _, _ = os.getloadavg()
    await update.message.reply_text(
        f"Температура процессора: {cpu_temp():.1f} °C\nНагрузка (1 мин): {load1:.2f}"
    )


async def photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not allowed(update):
        return
    await update.message.reply_text("Снимаю…")
    proc = await asyncio.create_subprocess_exec(
        "rpicam-still", "-n", "-t", "1000", "--width", "1280", "--height", "720", "-o", "/tmp/photo.jpg"
    )
    await proc.wait()
    with open("/tmp/photo.jpg", "rb") as f:
        await update.message.reply_photo(f)


def main():
    app = Application.builder().token(TOKEN).build()
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("status", status))
    app.add_handler(CommandHandler("photo", photo))
    app.run_polling()


if __name__ == "__main__":
    main()

Запустите: python3 bot.py (с переменными из tg.env, как выше). Напишите боту /status: он ответит температурой и нагрузкой. Если подключена камера (подключение камеры), команда /photo пришлёт снимок.

Ответ бота на /status: температура процессора и нагрузка

Как это работает:

  • Application — основа бота. run_polling() сам опрашивает серверы Telegram: входящие соединения и открытые порты на роутере не нужны, работает и за NAT.
  • Функции start, status, photo — обработчики команд. Они асинхронные (async def, await): пока одна ждёт ответ сети, остальные не блокируются.
  • Функция allowed() проверяет номер чата: бот отвечает только вам. Это важно: имя бота публичное, и без проверки им сможет пользоваться любой.
  • rpicam-still запускается отдельным процессом, чтобы не «замораживать» бота.

На снимке: ответ на /start, на /status и начало ответа на /photo. Сначала бот пишет «Снимаю…», а снимок приходит после него. Если камера не подключена или не включена, после «Снимаю…» ничего не придёт, а в терминале появится ошибка (см. таблицу в конце статьи).

Чат с ботом: команды /start, /status и /photo

Меню команд

Чтобы список команд появлялся в подсказке чата, откройте BotFather и выполните /setcommands, затем выберите бота и отправьте:

status - Состояние платы
photo - Фото с камеры

Управление устройством

Добавьте команду, которая включает и выключает, например, светодиод или реле (реле на Raspberry Pi): с gpiozero это пара строк.

Сначала установите библиотеки в то же виртуальное окружение. На Raspberry Pi 5 gpiozero работает через драйвер lgpio, а системный пакет python3-lgpio обычное окружение не видит, поэтому ставим его отдельно. Для pip нужны инструменты сборки: у lgpio нет готового пакета под эту систему, и он собирается на плате.

sudo apt install -y swig python3-dev liblgpio-dev
source ~/tgbot/env/bin/activate
pip install gpiozero lgpio

Если пропустить apt, сборка остановится с ошибкой error: command 'swig' failed: No such file or directory и сообщением Failed to build lgpio. Если не хватает заголовка (lgpio.h), помогает пакет liblgpio-dev.

Без lgpio программа упадёт с ошибкой BadPinFactory: Unable to load any default pin factory! и предупреждениями PinFactoryFallback: Falling back from lgpio: No module named 'lgpio'. Другой способ: создать окружение с доступом к системным пакетам (python3 -m venv --system-site-packages env), тогда lgpio берётся из системы и собирать ничего не нужно.

Дальше правим bot.py в три приёма. Первый: в начало файла, к остальным import, добавьте импорт и создайте светодиод (GPIO17 — вывод 11 на колодке):

from gpiozero import LED

lamp = LED(17)

Второй: добавьте две функции-обработчика, например после функции photo, перед main():

async def lamp_on(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not allowed(update):
        return
    lamp.on()
    await update.message.reply_text("Включено")


async def lamp_off(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not allowed(update):
        return
    lamp.off()
    await update.message.reply_text("Выключено")


async def lamp_state(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not allowed(update):
        return
    await update.message.reply_text("Светодиод: " + ("включён" if lamp.value else "выключен"))

Функция lamp_state отвечает по состоянию, которое «помнит» программа: lamp.value равно 1, когда устройство включено. Для реле с active_high=False это тоже «включено», хотя на выводе при этом низкий уровень.

Третий: подключите их в функции main(), добавив строки add_handler к уже имеющимся. Функция целиком выглядит так:

def main():
    app = Application.builder().token(TOKEN).build()
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("status", status))
    app.add_handler(CommandHandler("photo", photo))
    app.add_handler(CommandHandler("on", lamp_on))
    app.add_handler(CommandHandler("off", lamp_off))
    app.add_handler(CommandHandler("state", lamp_state))
    app.run_polling()

Перезапустите bot.py и напишите боту /on и /off: светодиод включится и выключится. Команда /state скажет, включён ли он.

Команды /on, /off и /state и ответы бота

Чтобы команды появились в меню чата, добавьте в /setcommands строки on - Включить, off - Выключить и state - Состояние светодиода. Реле с низким уровнем включения (как на модуле из статьи про реле) управляется так же, но вместо LED(17) нужно написать OutputDevice(17, active_high=False) (импорт: from gpiozero import OutputDevice): тогда on() подаёт на вход реле низкий уровень.

Подробности про GPIO: GPIO в Python. Для управления опасными нагрузками соблюдайте правила безопасности.


Запуск при включении платы

Оформите бота как службу systemd (статья про автозапуск). Файл /etc/systemd/system/tgbot.service:

[Unit]
Description=Telegram bot
After=network-online.target
Wants=network-online.target

[Service]
User=pi
WorkingDirectory=/home/pi/tgbot
EnvironmentFile=/home/pi/tgbot/tg.env
ExecStart=/home/pi/tgbot/env/bin/python3 /home/pi/tgbot/bot.py
Restart=on-failure
RestartSec=10
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target

Замените pi на своё имя пользователя. Включите:

sudo systemctl daemon-reload
sudo systemctl enable --now tgbot
journalctl -u tgbot -f

Если бот использует камеру или GPIO, пользователь службы должен входить в группы video и gpio (у обычного пользователя Raspberry Pi OS они уже есть).


Советы

  • Не спамьте. Telegram ограничивает частоту сообщений: в один чат не чаще примерно раза в секунду, а при массовых отправках возможны ошибки 429. Для частых событий накапливайте и отправляйте сводку.
  • Сообщение об ошибке отправки. Если сети нет, send() выдаст ошибку: ловите исключение (как в примере alert.py), иначе программа упадёт.
  • Группы и каналы. Бота можно добавить в семейную группу: тогда chat_id будет отрицательным числом. Не забудьте разрешить боту читать сообщения группы.
  • Другая библиотека. Помимо python-telegram-bot, популярна aiogram: подход похож.
  • Безопасность. Токен храните в файле с правами 600, в программе проверяйте chat_id, не давайте боту команд, которые выполняют произвольный код на плате.

Если что-то не работает

ПроблемаЧто проверить
KeyError: 'TELEGRAM_TOKEN'Не загружены переменные окружения: set -a; source tg.env; set +a или EnvironmentFile в службе
401 UnauthorizedНеверный токен. Проверьте, нет ли лишних символов и пробелов
400 Bad Request: chat not foundНеверный chat_id или вы не писали боту первым
getUpdates возвращает пустой список []Отправьте боту сообщение ещё раз и обновите страницу
Таймаут при подключенииНет доступа к api.telegram.org. Проверьте сеть и настройки прокси
Бот не отвечает на командыПроверьте, что bot.py запущен, нет ли ошибок (journalctl -u tgbot) и что вы пишете с того аккаунта, чей chat_id указан
BadPinFactory: Unable to load any default pin factory!, No module named 'lgpio'В виртуальном окружении нет драйвера GPIO: sudo apt install -y swig python3-dev liblgpio-dev, затем pip install lgpio (см. раздел «Управление устройством»)
Failed to build lgpio, command 'swig' failedНе установлены инструменты сборки: sudo apt install -y swig python3-dev liblgpio-dev
/photo не присылает снимокКамера не обнаружена (rpicam-hello --list-cameras). Проверьте путь к файлу и права
Ошибка при запуске нескольких копийОдин токен можно использовать только в одной копии бота (Conflict: terminated by other getUpdates request)

Что дальше

  • Датчик движения PIR на Raspberry Pi 5: сообщение о движении.
  • Метеостанция на Raspberry Pi: тревога по температуре и давлению.
  • Автозапуск программ.
  • Камера Raspberry Pi.

Материалы

Оставьте комментарий

Отправляя комментарий, вы соглашаетесь с обработкой ваших данных (имя, адрес электронной почты, текст комментария) в соответствии с политикой конфиденциальности.