Привет, Хабр!

Время от времени я возвращаюсь к своему PET-проекту приватного и независимого от сторонних сервисов голосового ассистента под кодовым названием «Альфа», который разработан для голосового взаимодействия с умным домом на базе Home Assistant. Простыми словами — это приватная умная колонка для управления умным домом. В этот раз я решил заняться вопросом «нормального» взаимодействия «Альфы» и Home Assistant. А что из этого вышло — читайте далее.

♳ Начало

Однажды, глядя на моё предыдущее решение по взаимодействию «Альфы» с Home Assistant, мой коллега сказал мне, что я знаю толк в технических извращениях и было бы лучше выделить время и разобраться как реализуются интеграции в Home Assistant, что упростит жизнь потенциальным пользователям моих умных колонок. И я с ним полностью согласен — вопрос о полноценной интеграции уже назревал давно.

♴ Выбор коммуникации

Один из важных вопросов перед разработкой интеграции — какой тип коммуникации будет использовать для связи с Home Assistant. Рассматривал следующие варианты:

  • Request-Response/HTTP (классический, используется в REST API);

  • WebSocket;

  • gRPC.

В итоге решил реализовать связь «Альфы» с Home Assistant на gRPC. С данной системой я уже работал в одном коммерческом проекте и она мне очень понравилась.

С коммуникацией определились, теперь предстоит определиться с логикой подключения колонки и об этом пойдет речь дальше.

♴ Логика взаимодействия

Чтобы упростить пользователю процесс подключения «Альфы» к Home Assistant, нужно опираться на его прошлый опыт. Ближайший распространённый опыт — это интеграция умной колонки «Алисы» от Яндекса.

Для примера вы можете посмотреть, как реализовано получение команд от Яндекс Станции. Другими словами, Home Assistant отслеживает событие yandex_speaker и при наличии автоматизации и совпадения полученного значения обрабатывает команду. Для наглядности ниже представлен пример автоматизации обработки событий:

automation:
- alias: Расскажи шутку
  trigger:
  - platform: event
    event_type: yandex_speaker
    event_data:
      value: ничего не делай  # фраза из Сценария Яндекса (раздел То) 
  action:
  - service: media_player.play_media
    target:
      entity_id: '{{ trigger.event.data.entity_id }}'  # ответить той же колонкой
    data:
      media_content_type: text
      media_content_id: '{{ ["На связи", "Смольный слушает", "Тут я"]|random }}'
  mode: single

И пример автоматизации для озвучивания событий (TTS) на Яндекс Станции:

script:
  alice_custom_volume1:
    sequence:
    - service: tts.yandex_station_say
      entity_id: media_player.yandex_station  # замените на вашу колонку
      data:
        message: Внимание! Важное сообщение...
        options:
          volume_level: 0.8

Описанной выше логики вполне будет достаточно для взаимодействия с Home Assistant на первоначальном этапе — значит, будем действовать по аналогии.

Но в процессе разработки интеграции мне пришло в голову несколько идей, как улучшить взаимодействие «Альфы» с устройствами умного дома без создания сценариев в Home Assistant. Описанный выше метод получения команд от колонки мы оставим как резервный для очень сложных случаев.

♵ Разработка интеграции

На то, чтобы понять, как работает интеграция в Home Assistant, и на её разработку с отладкой у меня ушло чуть более пяти дней. Результат разработки вы можете видеть на моём GitHub.

Само собой, в данной статье я не планировал описывать процесс разработки интеграции Home Assistant — на эту тему полно информации в интернете, в том числе и на Хабре. Эта статья описывает взаимодействие с интеграцией на стороне «Альфы», и об этом я расскажу подробнее.

Итак, моя интеграция имеет следующие скромные возможности:

  • Голосовое управление умным домом;

  • Локальный TTS (озвучивание текста);

  • Мониторинг состояний устройств в реальном времени (на стороне «Альфы»);

  • Двусторонняя связь через gRPC;

  • Полная приватность - все данные передаются локально;

  • Поддержка подключения группы колонок.

♶ Установка интеграции в Home Assistant

Установка пользовательской интеграции — а именно так классифицируется наша интеграция — у большинства пользователей не должна вызвать проблем. Для установки интеграции в Home Assistant должен быть установлен менеджер пользовательских интеграций, он же HACS. Если HACS установлен, то для установки необходимо добавить репозиторий нашей интеграции. Ниже на скриншотах показан процесс добавления пользовательского репозитория:

Процесс добавления пользовательского репозитория
Процесс добавления пользовательского репозитория

На главной странице HACS необходимо нажать в правом верхнем углу три точки и выбрать из выпадающего меню пункт «Пользовательские репозитории»

Окно «Пользовательские репозитории»
Окно «Пользовательские репозитории»

И в поле «Репозиторий» вставить ссылку на GitHub нашей интеграции для подключения «Альфы».

После данного действия на главной странице HACS в разделе «Загруженные» должна появиться наша интеграция. Теперь мы можем перейти к процессу установки.

Чтобы наконец установить нашу интеграцию, нам нужно перейти в раздел Home Assistant: Настройки → Устройства и службы → Интеграции и нажать кнопку «+ Добавить интеграцию».

Список интеграции
Список интеграции

Далее необходимо найти нашу интеграцию «Alpha Connector» и выбрать её путем нажатия. Нажатие запустит процесс установки:

Настройка интеграции в процессе установки
Настройка интеграции в процессе установки

Как видно из скриншота, для работы интеграции требуется токен доступа, который необходимо сгенерировать в разделе профиля пользователя: Профиль → Безопасность → Долгосрочные токены доступа → Создать токен:

Получение токена доступа
Получение токена доступа

Затем дайте название токену и скопируйте его для последующей вставки в поле настроек интеграции «Alpha Connector». После этой процедуры интеграция будет установлена и готова к использованию.

Интеграция Alpha Connector
Интеграция Alpha Connector

♷ Внутри Альфы

Вот мы и подошли, пожалуй, к самому интересному. В этой части статьи я расскажу и покажу, как реализовано взаимодействие с нашей интеграцией внутри «Альфы».

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

wget https://raw.githubusercontent.com/VGCH/alpha-private-speaker-connector/refs/heads/main/custom_components/alpha_speaker/proto/alpha_speaker.proto

Прежде чем генерировать файлы для gRPC, необходимо установить пакеты protobuf, grpcio, grpcio-tools. Это делается командой:

pip install protobuf grpcio grpcio-tools

Теперь с помощью команды мы можем сгенерировать необходимые для работы gRPC файлы:

python3 -m grpcio_tools.protoc -Iprotos --python_out=. --pyi_out=. --grpc_python_out=. alpha_speaker.proto

И чтобы отделить сгенерированные файлы от рабочих файлов «Альфы», генерация файлов выполняется в отдельной директории с именем protos.

После успешного выполнения команды в папке protos появятся три новых файла:

protos/
├── alpha_speaker.proto          # Исходная схема
│
├── alpha_speaker_pb2.py         # Классы данных
├── alpha_speaker_pb2_grpc.py    # Сетевой код
└── alpha_speaker_pb2.pyi        # Подсказаки типов

Для работы с gRPC-коммуникацией на стороне «Альфы» используется отдельный класс AlphaSpeakerClient, реализованный в файле smart_connect.py. Этот файл импортирован в основной скрипт. Ниже представлен его код.

smart_connect.py
import grpc
import time
import threading
from protos import alpha_speaker_pb2
from protos import alpha_speaker_pb2_grpc


class AlphaSpeakerClient:
    def __init__(self, host='192.168.0.102', port=50051, speaker_id="alpha_smart_assistant_02"):
        self.channel = grpc.insecure_channel(f'{host}:{port}')
        self.stub = alpha_speaker_pb2_grpc.AlphaSpeakerServiceStub(self.channel)
        self.speaker_id = speaker_id
        self.error_connect = False
        self.send_initial_state = True

    def register(self, speaker_name="Альфа приватная умная колонка", speaker_type="alpha_private", firmware_version="1.0.2026"):
        """Регистрация колонки на сервере"""
        request = alpha_speaker_pb2.SpeakerRegistration(
            speaker_id=self.speaker_id,
            speaker_name=speaker_name,
            speaker_type=speaker_type,
            firmware_version=firmware_version,
            capabilities=["tts", "voice_control", "state_monitoring", "audio_streaming"],
            settings={
                 "voice": "female",
                 "language": "ru",
                 "volume": "80",
                 "audio_device": "default"
             }
        )
        try:
            response = self.stub.RegisterAlphaSpeaker(request)
            print(f"Регистрация: {response.success}, Сообщение: {response.message}")
            return response.session_id
        except grpc.RpcError as e:
            print(f"Ошибка регистрации: {e}")

    def listen_for_tts(self, callback):
        """Прослушивание потока команд TTS от HA (Server Streaming)"""
        request = alpha_speaker_pb2.StateStreamRequest(speaker_id=self.speaker_id)
        print("Ожидание TTS команд от сервера...")

        try:
            # Этот цикл будет висеть и ждать новых сообщений от сервера
            for command in self.stub.StreamTTSCommands(request):
                if command.text:
                   print(f"--- Новая команда TTS ---")
                   print(f"Текст: {command.text}")
                   print(f"Голос: {command.voice}, Громкость: {command.volume}")
                   
                   # Имитация выполнения и отправка ответа
                   self.send_tts_report(command.message_id, True)
                   callback(command.text, command.voice)
        except grpc.RpcError as e:
            print(f"Поток TTS прерван: {e}")

    def send_tts_report(self, message_id, success):
        """Отправка подтверждения о воспроизведении текста"""
        report = alpha_speaker_pb2.SpeakTextResponse(
            speaker_id=self.speaker_id,
            success=success,
            message="Воспроизведено успешно" if success else "Ошибка",
            message_id=message_id,
            timestamp=int(time.time())
        )
        self.stub.SendTTSResponse(report)

    def send_command(self, text):
        """Отправка команды в HA"""
        cmd = alpha_speaker_pb2.AlphaCommand(
            speaker_id=self.speaker_id,
            command_type="voice_action",
            voice_command=text,
            timestamp=int(time.time())
        )
        response = self.stub.SendAlphaCommand(cmd)
        print(f"Результат команды: {response.success}, {response.message}")

    def start_keepalive(self, callback, reconnect_status):
        """Фоновая отправка Ping для поддержания соединения"""
        self.error_connect = reconnect_status

        def ping_loop():
            while self.error_connect != True:
                try:
                    self.stub.KeepAlive(alpha_speaker_pb2.PingRequest(speaker_id=self.speaker_id))
                    time.sleep(15)
                except:
                    self.error_connect = True
                    callback(self.error_connect)
                    print("Ошибка отправки пинга.")
                    time.sleep(5)
                


        threading.Thread(target=ping_loop, daemon=True).start()
    
    # ВЗАИМОДЕЙСТВИЕ С УСТРОЙСТВАМИ HA (Команды)
    def control_device(self, entity_id, action, params=None):
        """Отправка команды в HA (например, включить свет)"""
        command = alpha_speaker_pb2.AlphaCommand(
            speaker_id=self.speaker_id,
            command_type=action,  # "turn_on", "set_temperature"
            entity_id=entity_id,
            #parameters=params or {},
            timestamp=int(time.time())
        )
        try:
            response = self.stub.SendAlphaCommand(command)
            print(f"HA Response: {response.message} (Event ID: {response.event_id})")
            return response
        except grpc.RpcError as e:
            print(f"Ошибка управления HA: {e}")

    #  МОНИТОРИНГ СОСТОЯНИЙ (Streaming)
    def watch_states(self, callback, filters=None, initial_state = True):
        """Получение обновлений состояний устройств из HA в реальном времени"""
        request = alpha_speaker_pb2.StateStreamRequest(
            speaker_id=self.speaker_id,
            entity_filters=filters or ["switch.dvukhkanalnoe_rele_kanal_1"],
            send_initial_state=initial_state
        )
        print("Подписка на обновления состояний HA...")
        #while self.error_connect != True:
        try:
            req = self.stub.StreamDeviceStates(request)
            initial_state = False
            for state in req:
                #print(f"Изменение: {state.friendly_name} ({state.entity_id}) -> {state.state}")
                callback(state.friendly_name, state.entity_id, state.state)
            #time.sleep(12)
        except grpc.RpcError as e:
                print(f"Поток состояний прерван: {e}")
                time.sleep(2)


    # ПОЛУЧЕНИЕ СПИСКА УСТРОЙСТВ
    def get_ha_devices(self, domains=None):
        """Запрос списка всех доступных устройств в HA"""
        req = alpha_speaker_pb2.DeviceListRequest(
            speaker_id=self.speaker_id,
            domains=domains)
        res = self.stub.GetAvailableDevices(req)
        print(f"Найдено устройств: {res.total_count}")
        for dev in res.devices:
            print(f"- {dev.friendly_name} [{dev.entity_id}]")
    

    def close(self):
        self.channel.close()

Назначение методов в коде класса подписано, поэтому, с вашего позволения, я не буду их описывать в подробностях.

Ниже показана реализация использования этого класса в основном скрипте: инициализация, регистрация колонки, подписка на обновления и т. д.

Использование класса в основном скрипте
from smart_connect import AlphaSpeakerClient
from ha_device_manager import DeviceStateManager

ha_manager = DeviceStateManager()

status_smart_connect = True # Мониторим статус подключения к HA   
client = None
errors_alarm = ""

def status_connect_ac(Status: bool):
    print(Status)
    alpha_c_init()

def alpha_connect_init():
    global status_smart_connect
    global client
    global errors_alarm
    
    count_trys = 0
    
    # Безопасно закрываем старый клиент
    if client:
        try:
            client.close()
        except Exception:
            pass  # Игнорируем ошибки закрытия старого линка
        client = None
        
    try:
        # Инициализируем gRPC клиент
        client = AlphaSpeakerClient(
            host='192.168.0.102', # Адрес HomeAsisstant
            port=50051,
            speaker_id="alpha_smart_assistant_01" # Идентификатор колонки (должен быть уникальный)
        )
    
        time.sleep(2)  
        
        # Цикл регистрации
        while True:
            try:
                status_connet_grpc = client.register(
                    speaker_name="Альфа с нейрочипом",
                    speaker_type=config.prototype_name,
                    firmware_version=config.version
                )
            except Exception as register_error:
                # Если сам метод .register() падает из-за отсутствия сети
                print(f"Попытка {count_trys + 1}: Ошибка вызова register: {register_error}")
                status_connet_grpc = False

            time.sleep(3)
            count_trys += 1
            
            if status_connet_grpc or count_trys >= 50: 
                break
                
        # Запускаем пинг в фоне или обрабатываем ошибку
        if status_connet_grpc:
            status_smart_connect = True  # Флаг успеха
            client.start_keepalive(status_connect_ac, False)
        else:
            status_smart_connect = False
            print("Ошибка: не удалось зарегистрироваться на gRPC сервере за 50 попыток")
            errors_alarm += "Ошибка подключения к Альфа коннект; "
    
    except Exception as e: # Перехватываем только системные исключения, сохраняя Ctrl+C
        print(f"Критическая ошибка инициализации умного дома: {e}")
        errors_alarm += f"Ошибка подключения к умному дому ({e}); "
        status_smart_connect = False


# Функция-фидбека TTS
def say_tts(phrase: str, voice: str):
    if phrase:
        if voice == "default":
            va_speak(phrase)
        else:
            va_speak(phrase, voice)

def tts_worker():
    client.listen_for_tts(callback=say_tts)

def ha_dev_status(frandly_name, devices, status):
    if devices:
        print(f"{frandly_name} {devices}, {status}")
        ha_manager.check_and_update(devices, status)
        print(ha_manager.get_state(devices))


def alpha_c_init():
    alpha_connect_init()

    if status_smart_connect:
        # Получаем статусы устройств
        # Запускаем наш воркер в отдельном потоке
        tts_thread = threading.Thread(target=tts_worker, daemon=True)
        tts_thread.start()

        # Запускаем воркер для получение статуса в отдельном потоке
        ha_states_thread = threading.Thread(target=get_ha_states, daemon=True)
        ha_states_thread.start()
        # Отправляем тестовую команду в Home Assistant
        client.send_command("Alpha is live!")

def get_ha_states():
    print("Чтение статусов: ")
    # Подписка на обновление датчиков, переключатели и т.п. (устройсвт HA)
    client.watch_states(filters=["light.", "switch.", "sensor.meteostantsiia", "button."], callback=ha_dev_status, initial_state=True)
    time.sleep(2)
    client.watch_states(filters=["light.", "switch.", "sensor.meteostantsiia", "button."], callback=ha_dev_status, initial_state=False)

alpha_c_init()

В коде постарался максимально в комментариях описать функционал, то хочется акцентировать внимание на некоторые моменты. Например стока со следующим кодом:

client.watch_states(filters=["light.", "switch.", "sensor.meteostantsiia", "button."], callback=ha_dev_status, initial_state=False)

Здесь «Альфа» подписывается на обновления устройств умного дома. В метод watch_states передаются аргументы: filters — список фильтров устройств по доменам, и callback — функция обратного вызова, куда и будут приходить обновлённые данные. Если посмотреть функцию ha_dev_status, то мы увидим использование дополнительного объекта ha_manager (экземпляр класса DeviceStateManager):

def ha_dev_status(frandly_name, devices, status):
    if devices:
        print(f"{frandly_name} {devices}, {status}")
        ha_manager.check_and_update(devices, status)
        print(ha_manager.get_state(devices))

DeviceStateManager — это вспомогательный класс для хранения списка устройств и их статусов. Метод check_and_update проверяет и обновляет статус устройства по ключу, а get_state просто возвращает значение по ключу — в данной функции используется для отладки.

Ниже представлен полный код вспомогательного класса DeviceStateManager.

ha_device_manager.py
class DeviceStateManager:
    def __init__(self):
        # Инициализируем словарь для хранения состояний устройств
        self.state = {}
    
    def update_state(self, key, value):
        """Обновляет состояние устройства по ключу"""
        old_value = self.state.get(key)
        self.state[key] = value
        
        if old_value is not None:
            print(f"Обновлено: {key}: {old_value} -> {value}")
        else:
            print(f"Добавлено: {key}: {value}")
    
    def get_state(self, key):
        """Возвращает текущее состояние устройства по ключу"""
        return self.state.get(key, None)
    
    def add_data(self, data_list):
        """Добавляет или обновляет данные из списка кортежей (ключ, значение)"""
        for key, value in data_list:
            self.update_state(key, value)
    
    def get_all_states(self):
        """Возвращает все текущие состояния в виде списка кортежей"""
        return [(k, v) for k, v in self.state.items()]
    
    def check_and_update(self, key, new_value):
        """
        Проверяет наличие ключа и обновляет его значение.
        Если ключа нет - добавляет его.
        
        Args:
            key: ключ устройства
            new_value: новое значение
        
        Returns:
            tuple: (old_value, was_updated)
                   old_value: старое значение или None если ключа не было
                   was_updated: True если ключ существовал, False если добавлен новый
        """
        old_value = self.state.get(key)
        was_updated = old_value is not None
        
        self.state[key] = new_value
        
        if was_updated:
            print(f"Ключ '{key}' обновлен: {old_value} -> {new_value}")
        else:
            print(f"Ключ '{key}' добавлен: {new_value}")
        
        return old_value, was_updated
    
    def check_exists(self, key):
        """Проверяет наличие ключа в менеджере состояний"""
        exists = key in self.state
        if exists:
            print(f"Ключ '{key}' существует: {self.state[key]}")
        else:
            print(f"Ключ '{key}' не существует")
        return exists
    
    def remove_state(self, key):
        """Удаляет ключ из менеджера состояний"""
        if key in self.state:
            value = self.state.pop(key)
            print(f"Ключ '{key}' удален (было: {value})")
            return value
        else:
            print(f"Ключ '{key}' не найден для удаления")
            return None

Теперь «Альфа» может в реальном времени напрямую получать статусы устройств, без создания дополнительных сценариев в Home Assistant.Теперь «Альфа» может в реальном времени напрямую получать статусы устройств, без создания дополнительных сценариев в Home Assistant.

Теперь «Альфа» может в реальном времени напрямую получать статусы устройств, без создания дополнительных сценариев в Home Assistant. Ниже представлен пример реализации функции управления освещением с помощью «Альфы».

    def light_on():
    try:
        # Получаем состояния реле один раз в начале
        state_1 = ha_manager.get_state('switch.dvukhkanalnoe_rele_kanal_1')
        state_2 = ha_manager.get_state('switch.dvukhkanalnoe_rele_kanal_2')
        
        # Определяем текст ответа в зависимости от текущего состояния
        if state_1 == 'off' and state_2 == 'off':
            text = "Включила свет"
        elif state_1 == 'on' and state_2 == 'off':
            text = "Первый светильник уже включен, включила оставшийся!"
        elif state_1 == 'off' and state_2 == 'on':
            text = "Один светильник уже включен, включила оставшийся!"
        else:
            text = "Свет уже включен! Но я могу выключить, если попросите!"
        
        # Включаем те каналы, которые были выключены
        if state_1 == 'off':
            client.control_device("switch.dvukhkanalnoe_rele_kanal_1", "turn_on")
        if state_2 == 'off':
            client.control_device("switch.dvukhkanalnoe_rele_kanal_2", "turn_on")
            
        va_speak(text)
        
    except Exception as e:
        print(f"Ошибка в light_on: {e}")
        va_speak("Кажется я глюкнула! Возникла ошибка подключения к умному дому!")

Метод control_device отправляет команду конкретному устройству, которое отвечает за освещение. Идентификатор объекта можно посмотреть в его свойствах. По аналогии можно сделать и с озвучиванием данных метеостанции на «Альфе»:

Функция произношения метеоданных
import random


def meteo():
    led_on()
    try:
        # Список приветственных фраз
        start_phrases = [
            'На свя+зи ваш метеоролог!',
            'Данные вашей метеостанции.',
            'Прослушайте метеосводку.',
            'Вот данные с вашей метеостанции!'
        ]

        # Инициализируем текст случайным приветствием
        speech = random.choice(start_phrases) + " ...."

        # Получаем данные датчиков и приводим к числу с плавающей точкой
        temp = float(ha_manager.get_state('sensor.meteostantsiia_temperatura_2'))
        hum = float(ha_manager.get_state('sensor.meteostantsiia_vlazhnost_2'))
        pres = float(ha_manager.get_state('sensor.meteostantsiia_atmosfernoe_davlenie_2'))
        v_bat = float(ha_manager.get_state('sensor.meteostantsiia_zariad_batarei_2'))

        # Словари единиц измерения для функции decimal2text
        units_f = (('целая', 'целых', 'целых'), 'f')
        units_m = (('процент', 'процента', 'процентов'), 'm')
        
        deg_exp = (('сотая градуса', 'сотых градуса', 'сотых градусов'), 'f')
        mm_exp = (('сотая миллиметра', 'сотых миллиметров', 'сотых миллиметров'), 'f')
        volt_exp = (('сотая вольта', 'сотых вольта', 'сотых вольт'), 'f')

        # Формируем озвучку температуры
        speech += " Температура воздуха: "
        speech += decimal2text(temp, int_units=units_f, exp_units=deg_exp)
        speech += " цельсия. "

        # Формируем озвучку влажности
        speech += "  Относительная влажность: "
        speech += num2text(hum, units_m)

        # Формируем озвучку давления
        speech += "... Атмосферное давление: "
        speech += decimal2text(pres, int_units=units_f, exp_units=mm_exp)
        speech += " ртутного столба. "

        # Формируем озвучку заряда батареи
        speech += ".... Заряд батареи метеостанции: "
        speech += decimal2text(v_bat, int_units=units_f, exp_units=volt_exp)
        speech += " "

        # Отправляем итоговый текст в голосовой ассистент
        va_speak(speech)

        # Сохраняем
        global last_speech
        last_speech = speech

    except Exception as e:
        print(f"Ошибка в meteo: {e}")
        va_speak("Кажется я глюкнула! Возникла ошибка подключения к умному дому!")

В случае, если по каким-то причинам не удаётся реализовать логику управления устройствами умного дома на стороне «Альфы», всегда можно воспользоваться методом send_command и обработать запрос с помощью сценария автоматизации на стороне Home Assistant. Например:

# Команда со сотороны Альфы
client.send_command("Alpha is live!")

Обработка команды на стороне Home Assistant (автоматизация):

alias: "Alpha: Ответ на тестовое команду"
description: Отвечает на команду 'Alpha is live!'
triggers:
  - event_type: alpha_speaker_command
    trigger: event
conditions:
  - condition: template
    value_template: |
      {{ 'alpha is live' in trigger.event.data.voice_command|lower }}
actions:
  - data:
      speaker_id: "{{ trigger.event.data.speaker_id }}"
      text: Я онлайн и готова к работе! Что вам нужно?
    action: alpha_speaker.send_tts
mode: single

Здесь в ответ на отправленную команду, Home Assistant отправит запрос TTS и «Альфа» произнесет фразу «Я онлайн и готова к работе! Что вам нужно?». По аналогии можно обработать любые команды.

Также можно отправить TTS-запрос на «Альфу» с дополнительными параметрами, например изменить голос синтеза речи:

alias: Уведомление о включении ПК на Альфа колонку
description: ""
triggers:
  - trigger: state
    entity_id:
      - switch.pc
conditions:
  - condition: state
    entity_id: switch.pc
    state:
      - "on"
actions:
  - data:
      speaker_id: alpha_smart_assistant_01
      text: Компьютер в комнате включен
      language: ru
      volume: 70
      voice: eugene
      priority: true
    action: alpha_speaker.send_tts
mode: single

В параметре voice можно передавать следующие голоса: aidar, baya, kseniya, eugene, xenia.

♸ Итоги

Ниже представлено видео демонстрации работы голосового ассистента.

Для удобства пользователей, которые не имеют доступа к YouTube, я разместил видео на доступном видеосервисе. Но основную часть видео с экспериментами с «Альфой» я выкладываю в шортсах на своём YouTube. Подписываться не призываю, просто посмотрите, если интересна тема.

Кстати, на видео — моя третья, продвинутая версия «Альфы», реализованная на платформе с NPU на 6 TOPS, соответственно работает с локальной LLM. Несмотря на то что описываемая интеграция разрабатывалась для новой версии, она прекрасно работает и с предыдущими версиями моих колонок.

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

В любом случае, спасибо, что уделили своё время на прочтение статьи. Всем успехов и удачных проектов! А если у вас остались вопросы или есть что добавить, то, как всегда, добро пожаловать в комментарии!

Ссылки к статье:

Комментарии (0)