История про то, как «просто пусть колонка читает вслух сообщения из канала» превратилась в квест по внутренностям умного дома Яндекса, с двумя тупиками, неофициальной интеграцией и в итоге рабочим пайплайном на webhook.
Задача
Дано: Telegram-канал, куда падают оповещения (у меня это был канал с оповещениями об угрозе атаки БПЛА по региону, но механизм универсальный — точно так же можно озвучивать алерты мониторинга, сообщения от умного дома или что угодно ещё). Есть Яндекс.Станция с Алисой. Хочется: когда в канал приходит сообщение с ключевым словом, колонка сама, без обращения к ней, произносит вслух заранее заданную фразу. Например «Внимание, объявлена опасность, пройдите в укрытие».
Звучит как задача на полчаса. По факту заняла несколько вечеров, потому что Яндекс не даёт из коробки простой возможности «скажи произвольный текст на колонке по внешнему событию». Расскажу весь путь — включая то, что не сработало, потому что тупики тут поучительнее итогового решения.
Вводная по экосистеме: почему это вообще нетривиально
У Яндекса есть три сущности, которые важно не путать:
-
Устройства умного дома — лампочки, розетки, датчики. Управляются через приложение «Дом с Алисой» и облачный Quasar API.
-
Сценарии — «если X, то Y». Например «если сработал датчик движения, включи свет и скажи фразу».
-
Колонка (Станция) — она же исполнитель голоса, она же одно из устройств. Ключевая проблема: произнести произвольный текст на колонке можно только двумя путями — либо голосом попросить саму Алису, либо через сценарий, у которого действие «Алиса говорит». А сценарий надо чем-то запустить. И вот тут начинаются приключения, потому что запустить сценарий извне (из своего кода) штатно нельзя — триггеры сценариев жёстко ограничены набором «датчиковых» событий.
То есть задача сводится к одному вопросу: как из своего Python-кода дёрнуть голос на колонке?
Тупик №1: виртуальные устройства и «Домовёнок Кузя»
Первая идея, к которой приходят все — сделать виртуальное устройство. Есть популярная прослойка «Домовёнок Кузя» (и аналоги вроде yandex2mqtt), которая умеет создавать виртуальные устройства в экосистеме Яндекса. Идея такая:
-
Создаём виртуальный «выключатель».
-
Бот при сообщении в Telegram переключает его в состояние «включён».
-
В приложении Яндекса делаем сценарий: «если выключатель включился → Алиса говорит фразу». Красиво. Кузя реально отдаёт статус устройства по URL, Яндекс его читает, отдаёт 200 OK, устройство появляется. А дальше — стена.
Когда открываешь создание сценария и пытаешься выбрать этот виртуальный выключатель как условие запуска, Яндекс сообщает: «Эти устройства пока не могут запускать сценарии».
Копаем документацию и выясняем фундаментальное ограничение. Триггером сценария в Яндексе может быть только:
-
датчик открытия/закрытия,
-
датчик движения,
-
датчик протечки,
-
датчик дыма,
-
беспроводная кнопка,
-
числовой датчик (температура, влажность, и т.п.) с порогом срабатывания,
-
голосовая команда или время. Обычные вкл/выкл-устройства (выключатель, розетка, лампа) в триггеры не допускаются вообще. Логика Яндекса понятна: сценарий должен запускаться от события в физическом мире, а не от того, что кто-то дёрнул состояние лампочки.
«Ну ок, — думаешь, — сделаю виртуальный датчик, а не выключатель». И тут второе ограничение: создание виртуальных датчиков (не устройств управления, а именно сенсоров) через сторонние прослойки Яндекс официально не поддерживает. Кузя умеет отдавать датчики в своём протоколе, но Яндекс их как триггеро-пригодные сенсоры не принимает.
Итог: автономная связка «внешнее событие → сценарий Алисы» через виртуальные устройства не строится в принципе. Это не вопрос «докрутить конфиг», это архитектурное ограничение платформы. Потратил на осознание этого целый вечер, поэтому выношу отдельно: не пытайтесь, тупик глухой.
Тупик №2: локальный TTS напрямую на колонку
Вторая идея — забыть про облако и сценарии, ломиться напрямую к колонке по локальной сети. У Станции есть локальный интерфейс, и в сообществе давно известны способы слать на неё команды через локальный токен (glagol/quasar-протокол).
Тут я упёрся в свою конкретную инфраструктуру: колонка стоит дома, а весь мой код крутится на VDS в дата-центре. Локальный доступ к колонке возможен только из той же сети, что и колонка. Гонять постоянный VPN/туннель между VDS и домашней сетью ради того, чтобы дёрнуть TTS — это оверинжиниринг и лишняя точка отказа. Плюс локальный протокол Станции периодически меняется от прошивки к прошивке, и решение вышло бы хрупким.
Для тех, у кого код и колонка в одной сети — этот путь рабочий, и он проще итогового. Но в моём случае (разные сети) он отпадал. Нужно было решение, которое работает через облако, но при этом позволяет слать произвольный текст.
Рабочее решение: Home Assistant + неофициальная интеграция YandexStation
Ключом оказался Home Assistant (HA) — open-source платформа умного дома, которую можно поднять в Docker на том же VDS. Для неё есть замечательная неофициальная интеграция YandexStation (авторства AlexxIT), которая инкапсулирует облачный TTS Яндекса и умеет говорить на колонке удалённо, через облако, не требуя одной сети.
То есть HA берёт на себя ровно ту часть, которую Яндекс не отдаёт наружу штатно: «произнеси вот этот текст на вот этой станции через облако».
Архитектура итогового решения:
Поток такой:
-
Python-бот на VDS слушает Telegram-канал через long polling (Bot API
getUpdates). -
Поймав сообщение с ключевым словом, бот делает локальный
POSTна webhook Home Assistant (оба на одном VDS, ходят по127.0.0.1). -
Home Assistant по webhook запускает автоматизацию с действием
media_player.play_media(TTS) через интеграцию YandexStation. -
Интеграция уходит в облако Яндекса и заставляет колонку произнести текст. Всё, что раньше не строилось напрямую, здесь разложено по слоям: бот отвечает за Telegram, HA — за разговор с колонкой.
Настройка Home Assistant и YandexStation
Поднимаем HA в Docker (фрагмент docker-compose.yml):
services: homeassistant: image: ghcr.io/home-assistant/home-assistant:stable container_name: homeassistant volumes: - ./ha-config:/config network_mode: host # чтобы webhook был доступен локально с VDS restart: unless-stopped
network_mode: host тут важен: он даёт боту ходить на HA по 127.0.0.1:8123 без пробрасывания портов.
Дальше ставим интеграцию YandexStation через HACS (Home Assistant Community Store) — это стандартный способ доустановки неофициальных интеграций. В конфиге интеграции авторизуемся токеном Яндекса. После этого колонка появляется в HA как media_player.yandex_station_<id>.
Проверяем, что HA вообще умеет говорить на колонке, прямо из Developer Tools → Services:
service: media_player.play_mediatarget: entity_id: media_player.yandex_station_xxxxxxxxdata: media_content_id: "Проверка связи" media_content_type: "text" # ключевой момент: тип text = TTS
Если колонка произнесла «Проверка связи» — самое сложное позади. media_content_type: text — это то, что интеграция трактует как «произнеси голосом», а не «проиграй аудиофайл».
Webhook в Home Assistant
Теперь делаем так, чтобы это действие запускалось по HTTP-запросу. В HA есть встроенный триггер автоматизации типа webhook. Добавляем в automations.yaml:
- alias: "TTS из Telegram-бота" trigger: - platform: webhook webhook_id: "tg_alert_speak" local_only: true # принимать только из локальной сети/хоста action: - service: media_player.play_media target: entity_id: media_player.yandex_station_xxxxxxxx data: media_content_id: "{{ trigger.json.text }}" media_content_type: "text"
Здесь несколько важных деталей:
-
webhook_id— секретная часть URL. Полный адрес будетhttp://127.0.0.1:8123/api/webhook/tg_alert_speak. Кто знает ID — может дёрнуть; поэтомуlocal_only: true, чтобы снаружи VDS webhook не был доступен. -
{{ trigger.json.text }}— шаблон Jinja2, который достаёт полеtextиз JSON-тела запроса. То есть бот может передавать произвольный текст для озвучки, а не только фиксированную фразу. -
local_only: true— критично для безопасности. Webhook в HA по умолчанию не требует авторизации (в этом вся его простота), поэтому единственная защита — ограничение источника. Раз бот на том же хосте, наружу порт можно вообще не открывать.
Python-бот
Финальный кусок — сам бот. Слушает канал, ловит ключевое слово, дёргает webhook. Минимальная версия на python-telegram-bot:
import httpxfrom telegram import Updatefrom telegram.ext import Application, MessageHandler, filters, ContextTypes HA_WEBHOOK = "http://127.0.0.1:8123/api/webhook/tg_alert_speak" # ключевые слова, на которые реагируемKEYWORDS = ("опасность", "угроза", "тревога", "внимание") # что произнести (можно и просто пробрасывать исходный текст)SPEAK_TEXT = "Внимание. Объявлена опасность. Пройдите в укрытие." async def on_message(update: Update, context: ContextTypes.DEFAULT_TYPE): msg = (update.channel_post or update.message) if not msg or not msg.text: return text_lower = msg.text.lower() if not any(kw in text_lower for kw in KEYWORDS): return # дёргаем Home Assistant try: async with httpx.AsyncClient(timeout=10) as client: await client.post(HA_WEBHOOK, json={"text": SPEAK_TEXT}) except Exception as e: # в реальном коде — логирование, здесь для краткости print print(f"Не смог достучаться до HA: {e}") def main(): app = Application.builder().token("ВАШ_ТОКЕН_БОТА").build() # слушаем и обычные сообщения, и посты в каналах app.add_handler(MessageHandler( filters.TEXT & (~filters.COMMAND), on_message )) app.run_polling(allowed_updates=["message", "channel_post"]) if __name__ == "__main__": main()
Несколько нюансов, на которые я наступил:
channel_post, а не message. Если бот читает именно канал (а не чат), апдейты приходят как channel_post. Легко забыть указать это в allowed_updates и потом полдня гадать, почему бот «не видит» сообщения. Бот при этом должен быть администратором канала.
Реакция на текст, а не фиксированную фразу. В примере я озвучиваю статический SPEAK_TEXT, но легко пробрасывать в HA сам текст сообщения: json={"text": msg.text}. Тогда колонка прочитает то, что реально пришло. Осторожно с длиной — TTS Яндекса режет очень длинные фразы.
Дебаунс. Если в канал за минуту падает десять сообщений с ключевым словом, колонка будет тараторить без остановки. В боевой версии я добавил простой троттлинг — не чаще одного объявления в N секунд, через time.monotonic() и запоминание времени последнего срабатывания.
Что в итоге получилось и чего это стоило
Схема работает стабильно уже некоторое время. Задержка от появления сообщения в канале до голоса на колонке — обычно 1-3 секунды (long polling + облачный TTS). Всё крутится в двух контейнерах на одном дешёвом VDS: бот и Home Assistant.
Честный разбор компонентов:
-
Home Assistant ради одного webhook — это overkill? С одной стороны да, поднимать целый HA ради «сказать фразу» — тяжеловато. С другой — именно интеграция YandexStation решает главную проблему (облачный TTS без общей сети с колонкой), а переписывать её логику руками смысла нет. Плюс HA открывает кучу других возможностей на будущее.
-
Неофициальная интеграция — это риск. YandexStation держится на реверс-инжиниринге облака Яндекса. Если Яндекс поменяет протокол, интеграция может временно отвалиться до обновления. Это плата за то, что официального API «скажи текст» просто нет.
-
Webhook без авторизации. Решается
local_onlyи тем, что бот на том же хосте. Если разносить бот и HA по разным машинам — придётся добавлять либо аутентификацию, либо туннель, либо HA Long-Lived Token вместо webhook.
Чему научил этот квест
Проверяйте ограничения платформы до того, как проектировать решение. Я потратил вечер на виртуальные устройства только потому, что не прочитал сразу, какие типы устройств Яндекс пускает в триггеры сценариев. Список ограничений триггеров — первое, что стоило изучить.
Тупики полезно документировать. Оба моих тупика (виртуальные устройства и локальный TTS) — это то, что гуглится в первую очередь и на что уходит больше всего времени. Если эта статья сэкономит кому-то два вечера — она написана не зря.
Готовая интеграция часто лучше своего велосипеда. Я мог бы попытаться реализовать облачный TTS-протокол Яндекса сам. Но YandexStation уже прошла этот путь, отладила edge-кейсы и поддерживается сообществом. Инкапсуляция сложного протокола в готовый компонент — то, ради чего существует open-source.
Webhook — недооценённый клей. Простой HTTP-триггер без авторизации, ограниченный local_only, оказался идеальным способом соединить два процесса на одном хосте. Не нужен брокер сообщений, не нужна очередь — POST с JSON закрывает задачу.
Что можно улучшить
-
Убрать HA, если найдётся способ дёргать облачный TTS Яндекса напрямую из Python. Пока готовой библиотеки, которая делает это так же надёжно, как YandexStation, я не нашёл — но это очевидное направление для упрощения.
-
Проброс исходного текста вместо фиксированной фразы (сделано опционально, см. выше).
-
Фильтры и приоритеты — разные ключевые слова → разные фразы или громкость.
-
Дублирование в несколько колонок, если их дома много: просто перечислить несколько
entity_idв действии автоматизации.
Если у вас есть Яндекс.Станция и желание научить её реагировать на внешние события — надеюсь, эта карта тупиков и рабочего маршрута сэкономит вам пару вечеров. Схема универсальная: на месте «сообщения из Telegram» может быть алерт из системы мониторинга, уведомление от CI, событие от другого умного дома — что угодно, что умеет сделать HTTP-запрос.
ссылка на оригинал статьи https://habr.com/ru/articles/1058250/