Оглавление

TL;DR: у нас в доме пошли «моргашки» света из-за ремонта. Я подключил свой APC Smart-UPS SC620 к apcupsd, накатал первого телеграм-бота на Python и теперь получаю в чат компактные уведомления о смене статуса (ONBATT/ONLINE), просадки сети и низком заряде. Пороговые значения и «антиспам» настраиваются в .env. Называется это чудо — @Podezd24Bot.


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

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

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

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

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

  • apcupsd — демон, который общается с UPS (в моём случае по /dev/ttyS1, COM2).
  • apcaccess — утилита, через которую бот читает статус.
  • Python 3 + python-telegram-bot 21.x — мой код бота.
  • systemd — чтобы всё жило само и перезапускалось.
  • Немного udev и serial-getty магии, чтобы никто не трогал COM-порт.

Что бот умеет

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

Как я это готовил (пошагово)

1) Дровим apcupsd и убеждаемся, что порт живой

UPS у меня сидит на /dev/ttyS1 (визуально «COM2»). Проверка:

sudo apctest /dev/ttyS1
# 1) Query the UPS...  — должно показать SM/параметры, серийник и т.д.

Если apctest разговаривает — отлично, значит порт тот.

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 0.0.0.0
NISPORT 3551
EOF

Перезапуск:

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

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

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 -v -c add /dev/ttyS1

# На всякий случай отключил getty:
sudo systemctl disable --now serial-getty@ttyS1.service

4) Бот (структура)

/opt/apc-telegram-bot/
  ├── bot.py
  ├── .env
  └── .venv/           # виртуалка Python

В .env — все настройки:

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 — считаем скачком
ALERT_VOLT_JUMP=12

# Сколько должно держаться вне диапазона, чтобы слать алерт (анти-дерготня)
VOLT_PERSIST_SECONDS=3

# Кулдаун для напряжения — не торопимся писать каждые 2 сек
VOLT_COOLDOWN_SECONDS=60

5) Установка зависимостей

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

6) systemd-юнит

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

[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

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

sudo useradd -r -s /usr/sbin/nologin apcbot || true
sudo chown -R apcbot:apcbot /opt/apc-telegram-bot
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, все спецсимволы экранирую, чтобы не ломалось.
  • Кнопки:
    • «📊 Статус» — текущие значения
    • «🧾 События» — хвост лога
    • «🔔 Подписка / 🔕 Отписка» — мгновенно.
UPS в Телеграм: как я подружил старенький APC со смартфоном (мой первый бот на Python)

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

Я специально добавил анти-флаппер для напряжения:

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

Таким образом, «микроморгашки» из Netdata, которые не влияют на работу, не будят чат в 3 ночи. Но если реально «поплыло» — уведомление придёт.


Немного кода (фрагмент форматирования карточки)

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', pick('BATTCHG', '')):>6}",
        f"LEFT: {pick('TIMELEFT'):>7}",
    ])
    return "\n".join([
        f"*{esc('UPS')}*: *{esc(pick('MODEL'))}*",
        f"Статус: *{esc(pick('STATUS'))}*",
        "```\\n" + panel.replace("```", "`\u200b``") + "\\n```",
        f"_{esc(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))}_",
    ])

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

  • Bogus configuration value (*invalid-ups-mode*) — у тебя первая строка не ## apcupsd.conf v1.1 ##. Приведи конфиг к новому формату.
  • PANIC! Cannot communicate with UPS via serial port — проверь DEVICE, отключи serial-getty@ttyS*, запрети ModemManager через udev-правило.
  • Бесконечные REPLACEBATT — добавь SUPPRESS_STATUSES=REPLACEBATT в .env.
  • Бот не видит лог событий — дай чтение: setfacl -m u:apcbot:r /var/log/apcupsd.events.

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

  • Отправка графиков (мини-PNG) по запросу.
  • Агрегация: несколько UPS в одном боте.
  • Автоматический экспорт в Prometheus/Netdata.
  • /silent режим на ночь — «не буди меня по мелочи».

Итог

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