Как я сделал оповещения о БПЛА через Яндекс.Станцию

от автора

История про то, как «просто пусть колонка читает вслух сообщения из канала» превратилась в квест по внутренностям умного дома Яндекса, с двумя тупиками, неофициальной интеграцией и в итоге рабочим пайплайном на webhook.


Задача

Дано: Telegram-канал, куда падают оповещения (у меня это был канал с оповещениями об угрозе атаки БПЛА по региону, но механизм универсальный — точно так же можно озвучивать алерты мониторинга, сообщения от умного дома или что угодно ещё). Есть Яндекс.Станция с Алисой. Хочется: когда в канал приходит сообщение с ключевым словом, колонка сама, без обращения к ней, произносит вслух заранее заданную фразу. Например «Внимание, объявлена опасность, пройдите в укрытие».

Звучит как задача на полчаса. По факту заняла несколько вечеров, потому что Яндекс не даёт из коробки простой возможности «скажи произвольный текст на колонке по внешнему событию». Расскажу весь путь — включая то, что не сработало, потому что тупики тут поучительнее итогового решения.


Вводная по экосистеме: почему это вообще нетривиально

У Яндекса есть три сущности, которые важно не путать:

  • Устройства умного дома — лампочки, розетки, датчики. Управляются через приложение «Дом с Алисой» и облачный Quasar API.

  • Сценарии — «если X, то Y». Например «если сработал датчик движения, включи свет и скажи фразу».

  • Колонка (Станция) — она же исполнитель голоса, она же одно из устройств. Ключевая проблема: произнести произвольный текст на колонке можно только двумя путями — либо голосом попросить саму Алису, либо через сценарий, у которого действие «Алиса говорит». А сценарий надо чем-то запустить. И вот тут начинаются приключения, потому что запустить сценарий извне (из своего кода) штатно нельзя — триггеры сценариев жёстко ограничены набором «датчиковых» событий.

То есть задача сводится к одному вопросу: как из своего Python-кода дёрнуть голос на колонке?


Тупик №1: виртуальные устройства и «Домовёнок Кузя»

Первая идея, к которой приходят все — сделать виртуальное устройство. Есть популярная прослойка «Домовёнок Кузя» (и аналоги вроде yandex2mqtt), которая умеет создавать виртуальные устройства в экосистеме Яндекса. Идея такая:

  1. Создаём виртуальный «выключатель».

  2. Бот при сообщении в Telegram переключает его в состояние «включён».

  3. В приложении Яндекса делаем сценарий: «если выключатель включился → Алиса говорит фразу». Красиво. Кузя реально отдаёт статус устройства по 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 берёт на себя ровно ту часть, которую Яндекс не отдаёт наружу штатно: «произнеси вот этот текст на вот этой станции через облако».

Архитектура итогового решения:

Архитектура: Telegram → бот → Home Assistant → облако Яндекса → колонка

Архитектура: Telegram → бот → Home Assistant → облако Яндекса → колонка

Поток такой:

  1. Python-бот на VDS слушает Telegram-канал через long polling (Bot API getUpdates).

  2. Поймав сообщение с ключевым словом, бот делает локальный POST на webhook Home Assistant (оба на одном VDS, ходят по 127.0.0.1).

  3. Home Assistant по webhook запускает автоматизацию с действием media_player.play_media (TTS) через интеграцию YandexStation.

  4. Интеграция уходит в облако Яндекса и заставляет колонку произнести текст. Всё, что раньше не строилось напрямую, здесь разложено по слоям: бот отвечает за 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/