Skip to content

Commit

Permalink
Добавлена поддержка шаблонов для say_phrase (close #22)
Browse files Browse the repository at this point in the history
  • Loading branch information
dext0r committed Mar 24, 2023
1 parent d70a27a commit 8448ea4
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 43 deletions.
46 changes: 11 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,28 @@ yandex_station_intents:
Не выключай свет в прихожей: # (4)
extra_phrases:
- Не выключай свет в коридоре
say_phrase: Договорились
say_phrase: "{{ ['Договорились', 'Хорошо', 'Я тебя услышала', 'Оки-доки']|random }}"
Давай попьем чаю: # (5)
say_phrase: Отличная идея, сейчас включу свет на кухне
execute_command: Включи свет на кухне
Очень холодно: # (6)
execute_command: Прибавь температуру кондиционера на 1 градус в {{ event.room }}
Точная температура в комнате: # (7)
say_phrase: "Точная температура {{ sensor('sensor.room_temperature') }} в {{ event.room }}"
```
В данном случае интеграция автоматически создаст в УДЯ шесть сценариев, каждый из которых начинается с символов `---`. **Не удаляйте эти символы и не модифицируйте никак название!** По ним компонент понимает, что это его сценарий и в случае необходимости синхронизирует/удалит его.
В данном случае интеграция автоматически создаст в УДЯ семь сценариев, каждый из которых начинается с символов `---`. **Не удаляйте эти символы и не модифицируйте никак название!** По ним компонент понимает, что это его сценарий и в случае необходимости синхронизирует/удалит его.

Дополнительные параметры `say_phrase`, `extra_phrases`, `execute_command` являются необязательными и могут использоваться в любых вариациях.

Как работает:
1. Срабатывает от `Алиса, как дела`, генерирует событие с `text: Как дела`, колонка ничего не скажет в ответ
2. Срабатывает от `Алиса, кто нибудь дома`, генерирует событие с `text: Кто нибудь дома`, колонка, которая нас услышала ответит `Сейчас проверю`
3. Срабатывает от `Алиса, время кушать` (или `Алиса, время ужина`, или `Алиса, давай ужинать` и т.п.), генерирует событие с `text: Время ужинать`, колонка ничего не скажет в ответ
4. Срабатывает от `Алиса, не выключай свет в прихожей` (или `Алиса, не выключай свет в коридоре`), генерирует событие с `text: Не выключай свет в прихожей`, колонка, которая нас услышала ответит `Договорились`
4. Срабатывает от `Алиса, не выключай свет в прихожей` (или `Алиса, не выключай свет в коридоре`), генерирует событие с `text: Не выключай свет в прихожей`, колонка, которая нас услышала ответит случайной фразой из списка
5. Срабатывает от `Алиса, давай попьем чаю`, генерирует событие с `text: Давай попьем чаю`, колонка, которая нас услышала ответит `Отличная идея, сейчас включу свет на кухне`, после этого колонка выполнит команду `Включи свет на кухне`
6. Срабатывает от `Алиса, очень холодно`, генерирует событие с `text: Очень холодно`, колонка выполнит команду `Прибавь температуру кондиционера на 1 градус в КОМНАТА` (вместо `КОМНАТА` будет подставлена комната, в которой находится колонка, только для `mode: websocket`)
6. Срабатывает от `Алиса, очень холодно`, генерирует событие с `text: Очень холодно`, колонка выполнит команду `Прибавь температуру кондиционера на 1 градус в КОМНАТА` (вместо `КОМНАТА` будет подставлена комната, в которой находится колонка)
7. Срабатывает от `Алиса, точная температура в комнате`, генерирует событие с `text: Точная температура в комнате`, колонка, которая нас услышала ответит вычисленным шаблоном из `say_phrase`

### Обработка событий
После того как колонка услышит ключевую фразу в Home Assistant сгенерируется событие `yandex_intent` с параметрами:
Expand Down Expand Up @@ -132,35 +135,6 @@ automation:
media_content_id: Здесь в комнате {{ trigger.event.data.room }} всё отлично!
```

#### Ответ случайной фразой
При использования параметра `say_phrase` (примеры (2) и (4)) колонка будет отвечать заданной фразы при активации сценария. Эта фраза помещается внутрь сценария в УДЯ и поэтому может быть только одна.

Для использования случайных фраз, озвучку необходимо выполнять на стороне Home Assistant через компонент [Yandex.Station](https://github.com/AlexxIT/YandexStation)

Пример:
```yaml
yandex_station_intents:
intents:
Как дела:
automation:
- alias: Как дела
trigger:
- platform: event
event_type: yandex_intent
event_data:
text: Как дела
action:
- service: media_player.play_media
target:
entity_id: '{{ trigger.event.data.entity_id }}' # ответит колонка, которая услышала фразу активации
data:
media_content_type: text
media_content_id: >
{{ ['Неплохо', 'У вас как?', 'Сегодня отличный день']|random }}
```


### Режим синхронизации
По умолчанию сценарии синхронизируются с УДЯ автоматически при запуске Home Assistant. Это поведение можно отключить добавлением в конфигурацию опции `autosync: false`. После этого сценарии будут синхронизированы **только** при перезагрузке YAML конфигурации компонента со страницы `Панель разработчика` > `YAML` или через сервис `yandex_station_intents.reload`.

Expand All @@ -176,18 +150,20 @@ yandex_station_intents:
Интеграция поддерживает два режима работы: `websocket` (по-умолчанию) и `device`. Режим задаётся через параметр `mode` в конфигурации (менять можно в любой момент).

Режим `websocket` (по-умолчанию):
* Для работы требуется настроенная интеграция [Yandex.Station](https://github.com/AlexxIT/YandexStation)
* Постоянное подключение к серверам Яндекса, не требует интеграцию `Yandex Smart Home`
* Невозможно активировать событие из интерфейса УДЯ или голосом на телефоне, активация возможна только через колонку
* Позволяет получать `entity_id` и `room` колонки, которая услышала активационную фразу
* Использует недокументированную функцию УДЯ и в теории может быть отключен Яндексом
* Для работы требуется настроенная интеграция [Yandex.Station](https://github.com/AlexxIT/YandexStation)

Режим `device` (условно-устаревший):
* Требует установленный компонент `Yandex Smart Home`, в фильтрах необходимо разрешить `media_player.yandex_station_intents`
* Можно активировать событие кнопкой в интерфейсе или голосом на телефоне
* **Невозможно** определить колонку, которая услышала фразу, отсутствуют параметры `entity_id` и `room` в событиях
* Будет работать всегда, так как активация происходит через управление виртуальным устройством в УДЯ

* В этом режиме неподдерживаются:
* Параметр `execute_command`
* Шаблоны в `say_phrase`

## Вопросы и ответы
### Зачем это всё, если можно отдать скрипты через Yandex Smart Home?
Expand Down
29 changes: 22 additions & 7 deletions custom_components/yandex_station_intents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, template as template_helper
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.typing import ConfigType
import voluptuous as vol
Expand Down Expand Up @@ -41,12 +41,12 @@
PLATFORMS: Final[list[str]] = [media_player.DOMAIN]


def intent_config_validate(intent_config: dict) -> dict:
names = set(map(str.lower, intent_config.keys()))
def intents_config_validate(intents_config: dict) -> dict:
names = set(map(str.lower, intents_config.keys()))
execute_commands = set(
[
c[CONF_INTENT_EXECUTE_COMMAND].template.lower()
for c in intent_config.values()
for c in intents_config.values()
if CONF_INTENT_EXECUTE_COMMAND in c
]
)
Expand All @@ -55,7 +55,14 @@ def intent_config_validate(intent_config: dict) -> dict:
if forbidden_phrases:
raise vol.Invalid(f'Недопустимо использовать команды в активационных фразах: {forbidden_phrases}')

return intent_config
for name, intent_config in intents_config.items():
if (
isinstance(intent_config.get(CONF_INTENT_SAY_PHRASE), template_helper.Template)
and CONF_INTENT_EXECUTE_COMMAND in intent_config
):
raise vol.Invalid(f'Недопустимо совместное использование execute_command и шаблонной say_phrase в {name!r}')

return intents_config


def intent_item_validate(intent_item):
Expand All @@ -75,6 +82,14 @@ def intent_name_validate(name: str) -> str:
return name


def string_or_template(value: str) -> str | template_helper.Template:
value = cv.string(value)
if template_helper.is_template_string(value):
return cv.template(value)

return value


CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
Expand All @@ -89,13 +104,13 @@ def intent_name_validate(name: str) -> str:
vol.Optional(CONF_INTENT_EXTRA_PHRASES): [
vol.All(cv.string, intent_name_validate)
],
vol.Optional(CONF_INTENT_SAY_PHRASE): cv.string,
vol.Optional(CONF_INTENT_SAY_PHRASE): string_or_template,
vol.Optional(CONF_INTENT_EXECUTE_COMMAND): cv.template,
}
),
),
},
intent_config_validate,
intents_config_validate,
)
),
vol.Optional(CONF_MODE, default=MODE_WEBSOCKET): vol.In([MODE_WEBSOCKET, MODE_DEVICE]),
Expand Down
23 changes: 22 additions & 1 deletion custom_components/yandex_station_intents/yandex_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Intent:
name: str
trigger_phrases: list[str]
say_phrase: str | None = None
say_phrase_template: Template | None = None
execute_command: Template | None = None

@property
Expand Down Expand Up @@ -94,10 +95,12 @@ def __init__(self, hass: HomeAssistant, intents_config: dict | None):
return

for idx, (name, config) in enumerate(intents_config.items(), 0):
say_phrase = config.get(CONF_INTENT_SAY_PHRASE)
intent = Intent(
id=idx,
name=name,
say_phrase=config.get(CONF_INTENT_SAY_PHRASE),
say_phrase=say_phrase if not isinstance(say_phrase, Template) else None,
say_phrase_template=say_phrase if isinstance(say_phrase, Template) else None,
trigger_phrases=[name] + config.get(CONF_INTENT_EXTRA_PHRASES, []),
execute_command=config.get(CONF_INTENT_EXECUTE_COMMAND),
)
Expand All @@ -119,6 +122,9 @@ async def async_handle_phrase(self, phrase: str, event_data: dict, yandex_statio
if intent.execute_command:
await self._execute_command(intent, event_data, yandex_station_entity_id)

if intent.say_phrase_template:
await self._tts(intent, event_data, yandex_station_entity_id)

async def _execute_command(self, intent: Intent, event_data: dict, yandex_station_entity_id: str):
if self._detect_command_loop():
return
Expand All @@ -137,6 +143,21 @@ async def _execute_command(self, intent: Intent, event_data: dict, yandex_statio
},
)

async def _tts(self, intent: Intent, event_data: dict, yandex_station_entity_id: str):
intent.say_phrase_template.hass = self._hass

await self._hass.services.async_call(
media_player.DOMAIN,
media_player.SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: yandex_station_entity_id,
media_player.ATTR_MEDIA_CONTENT_TYPE: 'text',
media_player.ATTR_MEDIA_CONTENT_ID: intent.say_phrase_template.async_render(
variables={'event': event_data}
),
},
)

def _detect_command_loop(self) -> bool:
if self._last_command_at and self._last_command_at + COMMAND_EXECUTION_LOOP_WINDOW > dt.now():
self._command_execution_loop_count += 1
Expand Down

0 comments on commit 8448ea4

Please sign in to comment.