Коротко: в доме из-за ремонта начались «моргания» света. Я подключил свой APC Smart-UPS SC620 к apcupsd, написал первого Telegram-бота на Python и теперь получаю в чат компактные уведомления о смене статуса (ONBATT и ONLINE), просадках сети и низком заряде. Пороговые значения и антиспам настраиваются в .env. Бот называется @Podezd24Bot.
Почему вообще полез в эту историю
У соседей моргнуло, у меня мигнул монитор, и где-то тихо заплакала файловая система. Ремонт в доме дело полезное, но скачки в сети не радуют. Старенький APC Smart-UPS SC620 ещё бодр, грех его не использовать. Решение простое: поднять мониторинг и слать алерты в Telegram. А заодно отличный повод написать первого бота на Python.
Из чего это сделано
- 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, все спецсимволы экранируются, чтобы сообщение не ломалось.
- Кнопки: «Статус» с текущими значениями, «События» с хвостом лога, «Подписка» и «Отписка».
Почему бот мог молчать на скачках
Я специально добавил защиту от «дребезга» напряжения:
- если просадка нырнула и тут же вернулась, алерта нет;
- отклонение должно продержаться 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: реальное железо, реальная польза и код, который хочется улучшать. Если у вас тоже моргает, попробуйте.