В прошлой статье я воскресил лазерник Samsung ML-1615 2005 года и научил его печатать по AirPrint через LXC-контейнер на Proxmox. Штука отличная, но работает ровно до порога квартиры: чтобы что-то напечатать, надо быть дома, в своей Wi-Fi сети, и лезть в диалог печати. А хочется проще — переслал документ в семейный чат с подписью «напечатай», и он ждёт тебя на лотке. Сделал. По пути выяснилось, что Telegram не даёт двум клиентам слушать одного бота, а YAML умеет молча съедать заголовки у curl.
Почему не отдельный бот
Первая мысль была очевидной: написать маленький скрипт-поллер, который сидит в том же контейнере с CUPS, дёргает getUpdates и печатает всё, что ему прислали. Написал, запустил — и немедленно получил 409 Conflict.
Причина простая, но её легко не заметить при проектировании: Telegram Bot API не позволяет двум клиентам одновременно держать getUpdates или webhook на одном токене. А тот бот, чей токен я собирался использовать, уже был занят — его слушает Home Assistant, через него же работает другая моя автоматизация (пересылаешь magnet-ссылку в чат — она уезжает в qBittorrent) и через него же HA шлёт уведомления. То есть мой новый поллер не просто не заработал — он на несколько секунд перетянул одеяло на себя и мог отобрать апдейты у HA.
Вывод, который стоит запомнить: у одного бота должен быть ровно один потребитель апдейтов. Если в доме уже есть система, которая слушает бота — новую функцию надо вешать на неё, а не пытаться подключиться вторым. Поэтому схема перевернулась: Home Assistant остаётся единственным владельцем токена, а печать становится тупым HTTP-эндпоинтом, которому HA сам приносит файл.

Главная засада: событие называется не так, как ожидаешь
Автоматизация в Home Assistant должна ловить сообщение с файлом. Интуитивно кажется, что событие одно — «пришло сообщение». На деле интеграция telegram_bot бросает разные события в зависимости от содержимого:
telegram_text— для обычного текстового сообщения;telegram_attachment— для документа или фотографии с подписью.
И вот приятная деталь: подпись к файлу (в терминах Telegram API это caption) Home Assistant кладёт в то же самое поле text события — рядом с file_id, file_name, file_mime_type и file_size. То есть проверять слово «напечатай» можно ровно так же, как проверял бы обычный текст, просто событие другое.
Выяснить это гаданием невозможно, зато элементарно подсматривается: в настройках интеграции Telegram Bot включается debug-логирование, и в логе появляется строка Firing event telegram_attachment: {...} со всем содержимым события. Рекомендую начинать именно с этого, а не с чтения документации — в логе сразу видно точные имена полей.
Сервер печати на 60 строк
В контейнере с CUPS крутится крошечный HTTP-сервер на голом http.server из стандартной библиотеки — никаких фреймворков и зависимостей. Принимает ровно один запрос:
POST /print
X-Print-Secret: <секрет>
X-File-Name: <имя файла>
<тело — сырые байты документа>
→ lp -d ML1615 <файл>
→ {"ok": true}
Про безопасность, раз уж мы вешаем в сеть штуку, которая исполняет команды: порт слушается только в локалке и наружу через роутер не проброшен, запрос обязан принести общий секрет в заголовке (иначе 403), сам секрет сгенерирован secrets.token_urlsafe и лежит в файле с правами 600. Отдельно приятно, что сервер логирует несовпадение секрета вместе с тем, что реально пришло в заголовке — именно это потом и спасло при отладке. Сверху systemd-юнит с Restart=on-failure, чтобы переживал перезагрузки.
Связка в Home Assistant
Автоматизация ловит событие, проверяет, что это нужный чат и что в подписи есть слово «напечатай», и дёргает shell-команду:
automation:
- alias: "Печать документа из Telegram"
trigger:
- platform: event
event_type: telegram_attachment
condition:
- condition: template
value_template: "{{ trigger.event.data.chat_id == <ID вашего чата> }}"
- condition: template
value_template: "{{ 'напечатай' in (trigger.event.data.text | default('')) | lower }}"
action:
- service: shell_command.print_telegram_file
data:
file_id: "{{ trigger.event.data.file_id }}"
file_name: "{{ trigger.event.data.file_name }}"
А сама команда получает у Telegram путь к файлу, скачивает его и отправляет на печать:
shell_command:
print_telegram_file: >
bash -c '
FILE_PATH=$(curl -s "https://api.telegram.org/bot<ТОКЕН>/getFile?file_id={{ file_id }}" | python3 -c "import sys,json;print(json.load(sys.stdin)[\"result\"][\"file_path\"])") &&
curl -s -o /tmp/tg_print_file "https://api.telegram.org/file/bot<ТОКЕН>/$FILE_PATH" &&
curl -s -X POST "http://<IP контейнера>:9199/print" -H "X-Print-Secret: <секрет>" -H "X-File-Name: {{ file_name }}" --data-binary @/tmp/tg_print_file
'
Грабли, на которые я честно наступил: YAML съедает заголовки
Первая версия этой команды не работала, причём изощрённо: файл скачивался, POST уходил, сервер отвечал 403 и писал в лог, что секрет пришёл пустой. При том что в конфиге он был написан прямо в заголовке, вот же он, глазами видно.
Дело оказалось в YAML. Блок > (folded scalar) обещает схлопнуть переносы строк в пробелы — на этом и строится расчёт, когда пишешь многострочную команду. Но есть нюанс: строки с бо́льшим отступом, чем первая строка блока, не схлопываются — перенос перед ними сохраняется как настоящий перенос строки. А я, как всякий нормальный человек, аккуратно выровнял продолжения с флагами -H чуть правее, чтобы читалось красивее.
В результате bash получал не одну команду с тремя флагами, а три независимые команды: сначала curl -s -X POST "url" без единого заголовка (вот откуда пустой секрет), а следом — попытки выполнить -H "X-Print-Secret: ..." как отдельную программу. Самое противное, что внешне всё выглядело работающим: файл скачался, запрос ушёл, ошибка прилетела осмысленная, но совершенно не про то.
Правило на будущее: в shell_command со стилем > держите все строки скрипта на одном уровне отступа. Либо пишите весь вызов curl в одну длинную строку, либо берите литеральный стиль | (он сохраняет переносы всегда, честно и предсказуемо) и расставляйте в bash-скрипте обратные слэши в конце строк.
Что получилось
Пересылаешь в семейный чат PDF с подписью «напечатай» — и лист выезжает из принтера, пока ты ещё смотришь в чат. Работает откуда угодно, где есть Telegram: из машины, с работы, от родителей. Никакого VPN, проброшенных наружу портов и танцев с AirPrint через интернет — наружу торчит только сам Telegram, который и так у всех есть.
Если однажды перестанет работать, порядок проверки такой:
- дошёл ли апдейт до Home Assistant и какое событие он из него слепил — смотреть в debug-логе интеграции
telegram_bot; - дошёл ли POST до сервера печати и что приехало в заголовке — в журнале его systemd-юнита;
- сервер ответил 200, а бумаги нет — это уже епархия CUPS из прошлой статьи: скорее всего принтеру не хватило памяти на 600 dpi и он мигает красным.
