Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async def async_setup_service(hass: HomeAssistant) -> None:
async def send_text_command(call: ServiceCall) -> None:
"""Send a text command to Google Assistant SDK."""
command: str = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
await async_send_text_commands([command], hass)
await async_send_text_commands(hass, [command])

hass.services.async_register(
DOMAIN,
Expand Down
26 changes: 23 additions & 3 deletions homeassistant/components/google_assistant_sdk/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
}


async def async_send_text_commands(commands: list[str], hass: HomeAssistant) -> None:
async def _async_send_text_commands(
hass: HomeAssistant, commands: list[str], audio_out: bool
) -> bytes | None:
"""Send text commands to Google Assistant Service."""
# There can only be 1 entry (config_flow has single_instance_allowed)
entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
Expand All @@ -43,10 +45,28 @@ async def async_send_text_commands(commands: list[str], hass: HomeAssistant) ->

credentials = Credentials(session.token[CONF_ACCESS_TOKEN])
language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
with TextAssistant(credentials, language_code) as assistant:
with TextAssistant(credentials, language_code, audio_out=audio_out) as assistant:
for command in commands:
text_response = assistant.assist(command)[0]
resp = assistant.assist(command)
text_response = resp[0]
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
audio_response = resp[2]
if audio_out:
assert len(commands) == 1
return audio_response
return None


async def async_send_text_commands(hass: HomeAssistant, commands: list[str]) -> None:
"""Send text commands to Google Assistant Service."""
await _async_send_text_commands(hass, commands, False)


async def async_send_text_command_with_audio(
hass: HomeAssistant, command: str
) -> bytes | None:
"""Send a text command to Google Assistant Service and return the audio response."""
return await _async_send_text_commands(hass, [command], True)


def default_language_code(hass: HomeAssistant):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/",
"requirements": ["gassist-text==0.0.7"],
"requirements": ["gassist-text==0.0.8"],
"codeowners": ["@tronikos"],
"iot_class": "cloud_polling",
"integration_type": "service"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/google_assistant_sdk/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
commands.append(
broadcast_commands(language_code)[1].format(message, target)
)
await async_send_text_commands(commands, self.hass)
await async_send_text_commands(self.hass, commands)
63 changes: 63 additions & 0 deletions homeassistant/components/google_assistant_sdk/tts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Support for playback of Google Assistant's audio response."""
from __future__ import annotations

from typing import Any

import voluptuous as vol

from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA,
Provider,
TtsAudioType,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from .const import SUPPORTED_LANGUAGE_CODES
from .helpers import async_send_text_command_with_audio, default_language_code

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_LANG): vol.In(SUPPORTED_LANGUAGE_CODES),
}
)


async def async_get_engine(
hass: HomeAssistant,
config: ConfigType | None,
discovery_info: DiscoveryInfoType | None = None,
):
"""Set up Google Assistant SDK TTS provider."""
return GoogleAssistantSDKProvider(hass)


class GoogleAssistantSDKProvider(Provider):
"""The Google Assistant SDK TTS provider."""

hass: HomeAssistant

def __init__(self, hass):
"""Init Google Assistant SDK TTS provider."""
self.hass = hass
self.name = "Google Assistant SDK"

@property
def default_language(self) -> str | None:
"""Return the default language."""
return default_language_code(self.hass)

@property
def supported_languages(self) -> list[str] | None:
"""Return a list of supported languages."""
return SUPPORTED_LANGUAGE_CODES

async def async_get_tts_audio(
self, message: str, language: str, options: dict[str, Any] | None = None
) -> TtsAudioType:
"""Load tts audio file from provider.

Return a tuple of file extension and data as bytes.
"""
return "mp3", await async_send_text_command_with_audio(self.hass, message)
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ fritzconnection==1.10.3
gTTS==2.2.4

# homeassistant.components.google_assistant_sdk
gassist-text==0.0.7
gassist-text==0.0.8

# homeassistant.components.google
gcal-sync==4.1.2
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ fritzconnection==1.10.3
gTTS==2.2.4

# homeassistant.components.google_assistant_sdk
gassist-text==0.0.7
gassist-text==0.0.8

# homeassistant.components.google
gcal-sync==4.1.2
Expand Down
2 changes: 1 addition & 1 deletion tests/components/google_assistant_sdk/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def test_send_text_command(
blocking=True,
)
mock_text_assistant.assert_called_once_with(
ExpectedCredentials(), expected_language_code
ExpectedCredentials(), expected_language_code, audio_out=False
)
mock_text_assistant.assert_has_calls([call().__enter__().assist(command)])

Expand Down
18 changes: 10 additions & 8 deletions tests/components/google_assistant_sdk/test_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ async def test_broadcast_no_targets(
notify.DOMAIN,
DOMAIN,
{notify.ATTR_MESSAGE: message},
blocking=True,
)
await hass.async_block_till_done()
mock_text_assistant.assert_called_once_with(ExpectedCredentials(), language_code)
mock_text_assistant.assert_called_once_with(
ExpectedCredentials(), language_code, audio_out=False
)
mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)])


Expand Down Expand Up @@ -84,14 +86,14 @@ async def test_broadcast_one_target(

with patch(
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
return_value=["text_response", None],
return_value=["text_response", None, b""],
) as mock_assist_call:
await hass.services.async_call(
notify.DOMAIN,
DOMAIN,
{notify.ATTR_MESSAGE: message, notify.ATTR_TARGET: [target]},
blocking=True,
)
await hass.async_block_till_done()
mock_assist_call.assert_called_once_with(expected_command)


Expand All @@ -108,14 +110,14 @@ async def test_broadcast_two_targets(
expected_command2 = "broadcast to master bedroom time for dinner"
with patch(
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
return_value=["text_response", None],
return_value=["text_response", None, b""],
) as mock_assist_call:
await hass.services.async_call(
notify.DOMAIN,
DOMAIN,
{notify.ATTR_MESSAGE: message, notify.ATTR_TARGET: [target1, target2]},
blocking=True,
)
await hass.async_block_till_done()
mock_assist_call.assert_has_calls(
[call(expected_command1), call(expected_command2)]
)
Expand All @@ -129,14 +131,14 @@ async def test_broadcast_empty_message(

with patch(
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
return_value=["text_response", None],
return_value=["text_response", None, b""],
) as mock_assist_call:
await hass.services.async_call(
notify.DOMAIN,
DOMAIN,
{notify.ATTR_MESSAGE: ""},
blocking=True,
)
await hass.async_block_till_done()
mock_assist_call.assert_not_called()


Expand Down
90 changes: 90 additions & 0 deletions tests/components/google_assistant_sdk/test_tts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""The tests for the Google Assistant SDK TTS platform."""
import os
import shutil
from unittest.mock import patch

import pytest

from homeassistant.components import media_source, tts
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.setup import async_setup_component

from .conftest import ComponentSetup

from tests.common import async_mock_service


async def get_media_source_url(hass, media_content_id):
"""Get the media source url."""
if media_source.DOMAIN not in hass.config.components:
assert await async_setup_component(hass, media_source.DOMAIN, {})

resolved = await media_source.async_resolve_media(hass, media_content_id, None)
return resolved.url


@pytest.fixture(autouse=True)
def cleanup_cache(hass):
"""Clean up TTS cache."""
yield
default_tts = hass.config.path(tts.DEFAULT_CACHE_DIR)
if os.path.isdir(default_tts):
shutil.rmtree(default_tts)


@pytest.fixture
async def media_player_calls(hass):
"""Mock media player calls."""
return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)


@pytest.fixture(autouse=True)
async def setup_internal_url(hass):
"""Set up internal url."""
await async_process_ha_core_config(
hass, {"internal_url": "http://example.local:8123"}
)


@pytest.fixture
def mock_text_assistant():
"""Mock gtts."""
with patch(
"homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist",
return_value=["text_response", None, b"audio_response"],
) as mock_text_assistant:
yield mock_text_assistant


async def test_service_say(
hass, mock_text_assistant, media_player_calls, setup_integration: ComponentSetup
):
"""Test service call say."""
await setup_integration()

await async_setup_component(
hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_assistant_sdk"}}
)

message = "tell me a joke"
await hass.services.async_call(
tts.DOMAIN,
"google_assistant_sdk_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: message,
},
blocking=True,
)

assert len(media_player_calls) == 1
url = await get_media_source_url(
hass, media_player_calls[0].data[ATTR_MEDIA_CONTENT_ID]
)
assert url.endswith(".mp3")
mock_text_assistant.assert_called_once_with(message)