UPS в Телеграм: как я подружил старенький APC со смартфоном (мой первый бот на Python)

Коротко: в доме из-за ремонта начались «моргания» света. Я подключил свой APC Smart-UPS SC620 к apcupsd, написал первого Telegram-бота на Python и теперь получаю в чат компактные уведомления о смене статуса (ONBATT и ONLINE), просадках сети и низком заряде. Пороговые значения и антиспам настраиваются в .env. Бот называется @Podezd24Bot.

Почему вообще полез в эту историю

У соседей моргнуло, у меня мигнул монитор, и где-то тихо заплакала файловая система. Ремонт в доме дело полезное, но скачки в сети не радуют. Старенький APC Smart-UPS SC620 ещё бодр, грех его не использовать. Решение простое: поднять мониторинг и слать алерты в Telegram. А заодно отличный повод написать первого бота на Python.

Мини-ПК на стене с подключённым COM-кабелем от ИБП
Домашний мини-ПК: к COM-порту подключён кабель от ИБП, здесь работают apcupsd и бот
Источник бесперебойного питания APC Smart-UPS SC620 на полке
Тот самый APC Smart-UPS SC620: старенький, но ещё бодрый

Из чего это сделано

  • apcupsd, демон, который общается с ИБП. У меня по COM-порту /dev/ttyS1 (он же COM2).
  • Сетевой сервер apcupsd (NIS) на порту 3551, через который бот читает статус. То же самое показывает утилита apcaccess.
  • Python 3 и python-telegram-bot 21.x для самого бота.
  • systemd, чтобы всё работало само и перезапускалось.
  • Немного udev, чтобы никто не трогал COM-порт.

Что бот умеет

  • /ups показывает текущие параметры: напряжение на входе и выходе, частоту, нагрузку, батарею и оставшееся время.
  • /events выводит последние строки из /var/log/apcupsd.events.
  • /subscribe и /unsubscribe управляют подпиской на алерты.
  • Уведомляет о смене статуса (ONLINE и ONBATT), низком заряде и высокой нагрузке.
  • Фильтрует шумные статусы: батарея у меня неоригинальная, и я подавляю REPLACEBATT.
  • Не спамит при просадках: отклонение напряжения должно продержаться N секунд, прежде чем бот потревожит людей.
  • Сообщения размечены MarkdownV2 и оформлены компактными карточками с кнопками.

Как я это готовил

Шаг 1. Ставим apcupsd и проверяем порт.

Smart-UPS SC620 общается по протоколу APC Smart через фирменный кабель 940-0024C. ИБП у меня сидит на /dev/ttyS1. Перед проверкой остановите демон, иначе порт будет занят:

sudo apt install apcupsd
sudo systemctl stop apcupsd
sudo apctest

Если apctest видит ИБП и показывает модель и параметры, значит порт тот. Учтите, что apctest берёт порт из /etc/apcupsd/apcupsd.conf, поэтому сначала пропишите DEVICE.

Шаг 2. Минимальный apcupsd.conf.

sudo tee /etc/apcupsd/apcupsd.conf >/dev/null <<'EOF'
## apcupsd.conf v1.1 ##
UPSCABLE smart
UPSTYPE apcsmart
DEVICE /dev/ttyS1
LOCKFILE /var/lock
POLLTIME 5
NETSERVER on
NISIP 127.0.0.1
NISPORT 3551
EOF

В первой версии статьи здесь было NISIP 0.0.0.0. Это открывает сервер статуса для всей сети, а боту хватает локального адреса. Если у вас в /etc/default/apcupsd есть строка ISCONFIGURED=no, поменяйте её на yes, иначе демон в старых версиях пакета не запустится.

Перезапуск и проверка:

sudo systemctl restart apcupsd
apcaccess status | grep -E 'STATUS|LINEV|BATTV|LOADPCT|TIMELEFT|MODEL'

Шаг 3. Чтобы никто не мешал COM-порту.

ModemManager любит опрашивать последовательные порты, а getty может повесить на порт консоль. Запрещаем и то, и другое:

echo 'KERNEL=="ttyS1", ENV{ID_MM_DEVICE_IGNORE}="1"' | sudo tee /etc/udev/rules.d/99-apcupsd-ttyS1.rules
sudo udevadm control --reload
sudo udevadm trigger -c add /dev/ttyS1
sudo systemctl disable --now serial-getty@ttyS1.service

Шаг 4. Структура бота.

/opt/apc-telegram-bot/
  ├── bot.py
  ├── .env
  └── .venv/

Все настройки лежат в .env. Комментарии вынесены на отдельные строки: python-dotenv понимает комментарий после значения только если перед решёткой есть пробел, и так надёжнее.

TELEGRAM_BOT_TOKEN=тут_токен_от_BotFather
ADMINS=123456789
APC_HOST=127.0.0.1
APC_PORT=3551
POLL_SECONDS=10
ALERT_ON_BATTERY_SECONDS=5
ALERT_LOW_BATTERY_PCT=30
ALERT_LOAD_HIGH_PCT=80
# Подавить шумные статусы
SUPPRESS_STATUSES=REPLACEBATT
# Ниже этого напряжения считаем просадкой
ALERT_VOLT_LOW=200
# Выше этого напряжения считаем перенапряжением
ALERT_VOLT_HIGH=250
# Если разница между опросами не меньше этого значения, считаем скачком
ALERT_VOLT_JUMP=12
# Сколько секунд отклонение должно держаться, чтобы слать алерт
VOLT_PERSIST_SECONDS=3
# Пауза между алертами по напряжению
VOLT_COOLDOWN_SECONDS=60

Файл .env содержит токен бота, поэтому права на него стоит ограничить: chmod 600.

Шаг 5. Зависимости.

sudo apt install python3-venv
cd /opt/apc-telegram-bot
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install "python-telegram-bot==21.*" python-dotenv

Кавычки вокруг python-telegram-bot==21.* нужны, чтобы оболочка не попыталась раскрыть звёздочку.

Шаг 6. Юнит systemd.

# /etc/systemd/system/apc-telegram-bot.service
[Unit]
Description=APC UPS Telegram bot (apcupsd)
Wants=network-online.target
After=network-online.target apcupsd.service

[Service]
Type=simple
User=apcbot
WorkingDirectory=/opt/apc-telegram-bot
Environment="PYTHONUNBUFFERED=1"
ExecStart=/opt/apc-telegram-bot/.venv/bin/python /opt/apc-telegram-bot/bot.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Без Wants=network-online.target строка After=network-online.target почти ничего не гарантирует, поэтому я её добавил.

Пользователь, права и запуск:

sudo useradd -r -s /usr/sbin/nologin apcbot || true
sudo chown -R apcbot:apcbot /opt/apc-telegram-bot
sudo chmod 600 /opt/apc-telegram-bot/.env
sudo apt install acl
sudo setfacl -m u:apcbot:r /var/log/apcupsd.events
sudo systemctl daemon-reload
sudo systemctl enable --now apc-telegram-bot
sudo systemctl status apc-telegram-bot --no-pager

Интерфейс и уведомления

  • Бот присылает карточки: заголовок, блок параметров и кнопки.
  • Разметка MarkdownV2, все спецсимволы экранируются, чтобы сообщение не ломалось.
  • Кнопки: «Статус» с текущими значениями, «События» с хвостом лога, «Подписка» и «Отписка».
Карточка статуса ИБП в Telegram с параметрами сети и батареи и кнопками
Так выглядит карточка в Telegram: статус ONLINE, входное и выходное напряжение, нагрузка, заряд и кнопки управления

Почему бот мог молчать на скачках

Я специально добавил защиту от «дребезга» напряжения:

  • если просадка нырнула и тут же вернулась, алерта нет;
  • отклонение должно продержаться VOLT_PERSIST_SECONDS;
  • чтобы новые алерты не сыпались каждую минуту, есть VOLT_COOLDOWN_SECONDS.

Так микроскачки, которые видно в Netdata, но которые не влияют на работу, не будят чат в три ночи. А если реально поплыло, уведомление придёт.

Немного кода

Фрагмент форматирования карточки. В первой версии статьи в строке с ограждением блока кода был лишний обратный слеш, из-за которого в сообщение попадали символы \n вместо переноса строки. Исправленный вариант:

import re
from datetime import datetime

def fmt_status_md2(kv: dict) -> str:
    def esc(s):
        return re.sub(r'[_*\[\]()~`>#+\-=|{}.!\\]', r'\\\g<0>', str(s or ""))

    def pick(k, d="-"):
        return kv.get(k, d)

    panel = "\n".join([
        f"IN : {pick('LINEV'):>6}   FQ: {pick('LINEFREQ'):>5}",
        f"OUT: {pick('OUTPUTV'):>6}  LD: {pick('LOADPCT'):>6}",
        f"BAT: {pick('BATTV'):>6} CHG: {pick('BCHARGE'):>6}",
        f"LEFT: {pick('TIMELEFT'):>7}",
    ])
    return "\n".join([
        f"*{esc('UPS')}*: *{esc(pick('MODEL'))}*",
        f"Статус: *{esc(pick('STATUS'))}*",
        "```\n" + panel.replace("`", "'") + "\n```",
        f"_{esc(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))}_",
    ])

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

Типичные грабли и решения

  • Bogus configuration value. Проверьте, что первая строка конфига ## apcupsd.conf v1.1 ##, а значения UPSCABLE и UPSTYPE написаны без опечаток.
  • PANIC! Cannot communicate with UPS via serial port. Проверьте DEVICE и кабель, отключите serial-getty для этого порта и запретите ModemManager через правило udev.
  • Бесконечные REPLACEBATT. Добавьте SUPPRESS_STATUSES=REPLACEBATT в .env. Но помните, что ИБП может сигналить так не зря: батарею стоит хотя бы протестировать.
  • Бот не видит лог событий. Выдайте право на чтение: sudo setfacl -m u:apcbot:r /var/log/apcupsd.events. После ротации логов права может понадобиться выдать снова.

Дорожная карта

  • Отправка мини-графиков по запросу.
  • Несколько ИБП в одном боте.
  • Экспорт метрик в Prometheus или Netdata.
  • Тихий режим на ночь, чтобы не будить по мелочам.

Итог

Проект решил мою практическую боль: свет моргнул, и я в курсе. А ещё это отличный приземлённый старт в Python: реальное железо, реальная польза и код, который хочется улучшать. Если у вас тоже моргает, попробуйте.

Источники и ссылки


Комментарии загружаются…