diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 59c065ecbb6b1..94e155308612c 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -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, diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index e2d704a917a7f..c25fee913fa02 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -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] @@ -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): diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index e1b390f9496d5..0358d7609efec 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -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" diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index f9a212b54c34f..80d0e70f44c8d 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -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) diff --git a/homeassistant/components/google_assistant_sdk/tts.py b/homeassistant/components/google_assistant_sdk/tts.py new file mode 100644 index 0000000000000..49ed45a6741cf --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/tts.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index f5d8c9a6f3b18..5d45279d31079 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3a8e3fa8b042..0a8e4ab2a6b8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index b93f83feda726..6b80d17c58cd1 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -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)]) diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 85d421b1675a5..60235957f89f4 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -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)]) @@ -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) @@ -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)] ) @@ -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() diff --git a/tests/components/google_assistant_sdk/test_tts.py b/tests/components/google_assistant_sdk/test_tts.py new file mode 100644 index 0000000000000..9e6d95d3467cd --- /dev/null +++ b/tests/components/google_assistant_sdk/test_tts.py @@ -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)