Migrate google translate to config entries (#93803)

Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Martin Hjelmare 2023-06-21 14:58:58 +02:00 committed by GitHub
parent 22e1feb223
commit f9366e5cc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 557 additions and 103 deletions

View file

@ -1 +1,20 @@
"""The google_translate component."""
"""The Google Translate text-to-speech integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
PLATFORMS: list[Platform] = [Platform.TTS]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Google Translate text-to-speech from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View file

@ -0,0 +1,49 @@
"""Config flow for Google Translate text-to-speech integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.tts import CONF_LANG
from homeassistant.data_entry_flow import FlowResult
from .const import (
CONF_TLD,
DEFAULT_LANG,
DEFAULT_TLD,
DOMAIN,
SUPPORT_LANGUAGES,
SUPPORT_TLD,
)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES),
vol.Optional(CONF_TLD, default=DEFAULT_TLD): vol.In(SUPPORT_TLD),
}
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Google Translate text-to-speech."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is not None:
self._async_abort_entries_match(
{
CONF_LANG: user_input[CONF_LANG],
CONF_TLD: user_input[CONF_TLD],
}
)
return self.async_create_entry(
title="Google Translate text-to-speech", data=user_input
)
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA)

View file

@ -1,6 +1,11 @@
"""Constant for google_translate integration."""
"""Constants for the Google Translate text-to-speech integration."""
from dataclasses import dataclass
CONF_TLD = "tld"
DEFAULT_LANG = "en"
DEFAULT_TLD = "com"
DOMAIN = "google_translate"
SUPPORT_LANGUAGES = [
"af",
"ar",

View file

@ -2,6 +2,8 @@
"domain": "google_translate",
"name": "Google Translate text-to-speech",
"codeowners": [],
"config_flow": true,
"dependencies": ["repairs"],
"documentation": "https://www.home-assistant.io/integrations/google_translate",
"iot_class": "cloud_push",
"loggers": ["gtts"],

View file

@ -0,0 +1,15 @@
{
"config": {
"step": {
"user": {
"data": {
"language": "Language",
"tld": "TLD"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}

View file

@ -1,33 +1,120 @@
"""Support for the Google speech service."""
from __future__ import annotations
from io import BytesIO
import logging
from typing import Any
from gtts import gTTS, gTTSError
import voluptuous as vol
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
from homeassistant.components.tts import (
CONF_LANG,
PLATFORM_SCHEMA,
Provider,
TextToSpeechEntity,
TtsAudioType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import MAP_LANG_TLD, SUPPORT_LANGUAGES, SUPPORT_TLD
from .const import (
CONF_TLD,
DEFAULT_LANG,
DEFAULT_TLD,
MAP_LANG_TLD,
SUPPORT_LANGUAGES,
SUPPORT_TLD,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_LANG = "en"
SUPPORT_OPTIONS = ["tld"]
DEFAULT_TLD = "com"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES),
vol.Optional("tld", default=DEFAULT_TLD): vol.In(SUPPORT_TLD),
vol.Optional(CONF_TLD, default=DEFAULT_TLD): vol.In(SUPPORT_TLD),
}
)
async def async_get_engine(hass, config, discovery_info=None):
async def async_get_engine(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> GoogleProvider:
"""Set up Google speech component."""
return GoogleProvider(hass, config[CONF_LANG], config["tld"])
return GoogleProvider(hass, config[CONF_LANG], config[CONF_TLD])
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Google Translate speech platform via config entry."""
default_language = config_entry.data[CONF_LANG]
default_tld = config_entry.data[CONF_TLD]
async_add_entities([GoogleTTSEntity(config_entry, default_language, default_tld)])
class GoogleTTSEntity(TextToSpeechEntity):
"""The Google speech API entity."""
def __init__(self, config_entry: ConfigEntry, lang: str, tld: str) -> None:
"""Init Google TTS service."""
if lang in MAP_LANG_TLD:
self._lang = MAP_LANG_TLD[lang].lang
self._tld = MAP_LANG_TLD[lang].tld
else:
self._lang = lang
self._tld = tld
self._attr_name = f"Google {self._lang} {self._tld}"
self._attr_unique_id = config_entry.entry_id
@property
def default_language(self):
"""Return the default language."""
return self._lang
@property
def supported_languages(self):
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
@property
def supported_options(self):
"""Return a list of supported options."""
return SUPPORT_OPTIONS
def get_tts_audio(
self, message: str, language: str, options: dict[str, Any] | None = None
) -> TtsAudioType:
"""Load TTS from google."""
tld = self._tld
if language in MAP_LANG_TLD:
tld_language = MAP_LANG_TLD[language]
tld = tld_language.tld
language = tld_language.lang
if options is not None and "tld" in options:
tld = options["tld"]
tts = gTTS(text=message, lang=language, tld=tld)
mp3_data = BytesIO()
try:
tts.write_to_fp(mp3_data)
except gTTSError as exc:
_LOGGER.debug(
"Error during processing of TTS request %s", exc, exc_info=True
)
raise HomeAssistantError(exc) from exc
return "mp3", mp3_data.getvalue()
class GoogleProvider(Provider):

View file

@ -173,6 +173,7 @@ FLOWS = {
"google_generative_ai_conversation",
"google_mail",
"google_sheets",
"google_translate",
"google_travel_time",
"govee_ble",
"gpslogger",

View file

@ -2084,7 +2084,7 @@
},
"google_translate": {
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "cloud_push",
"name": "Google Translate text-to-speech"
},

View file

@ -0,0 +1,14 @@
"""Common fixtures for the Google Translate text-to-speech tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.google_translate.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry

View file

@ -0,0 +1,68 @@
"""Test the Google Translate text-to-speech config flow."""
from unittest.mock import AsyncMock
import pytest
from homeassistant import config_entries
from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN
from homeassistant.components.tts import CONF_LANG
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_user_step(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
"""Test user step create entry result."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_LANG: "de",
CONF_TLD: "de",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Google Translate text-to-speech"
assert result["data"] == {
CONF_LANG: "de",
CONF_TLD: "de",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_already_configured(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test user step already configured entry."""
config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_LANG: "de", CONF_TLD: "de"}
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_LANG: "de",
CONF_TLD: "de",
},
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert len(mock_setup_entry.mock_calls) == 0

View file

@ -1,21 +1,27 @@
"""The tests for the Google speech platform."""
from unittest.mock import patch
from __future__ import annotations
from collections.abc import Generator
from typing import Any
from unittest.mock import MagicMock, patch
from gtts import gTTSError
import pytest
from homeassistant.components import media_source, tts
from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN
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.core import HomeAssistant
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
from tests.common import MockConfigEntry, async_mock_service
@pytest.fixture(autouse=True)
@ -29,7 +35,7 @@ def mock_tts_cache_dir_autouse(mock_tts_cache_dir):
return mock_tts_cache_dir
async def get_media_source_url(hass, media_content_id):
async def get_media_source_url(hass: HomeAssistant, media_content_id: str) -> str:
"""Get the media source url."""
if media_source.DOMAIN not in hass.config.components:
assert await async_setup_component(hass, media_source.DOMAIN, {})
@ -39,13 +45,13 @@ async def get_media_source_url(hass, media_content_id):
@pytest.fixture
async def calls(hass):
async def calls(hass: HomeAssistant) -> list[ServiceCall]:
"""Mock media player calls."""
return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
@pytest.fixture(autouse=True)
async def setup_internal_url(hass):
async def setup_internal_url(hass: HomeAssistant) -> None:
"""Set up internal url."""
await async_process_ha_core_config(
hass, {"internal_url": "http://example.local:8123"}
@ -53,26 +59,85 @@ async def setup_internal_url(hass):
@pytest.fixture
def mock_gtts():
def mock_gtts() -> Generator[MagicMock, None, None]:
"""Mock gtts."""
with patch("homeassistant.components.google_translate.tts.gTTS") as mock_gtts:
yield mock_gtts
async def test_service_say(hass: HomeAssistant, mock_gtts, calls) -> None:
"""Test service call say."""
@pytest.fixture(name="setup")
async def setup_fixture(
hass: HomeAssistant,
config: dict[str, Any],
request: pytest.FixtureRequest,
) -> None:
"""Set up the test environment."""
if request.param == "mock_setup":
await mock_setup(hass, config)
elif request.param == "mock_config_entry_setup":
await mock_config_entry_setup(hass, config)
else:
raise RuntimeError("Invalid setup fixture")
await async_setup_component(
hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}}
@pytest.fixture(name="config")
def config_fixture() -> dict[str, Any]:
"""Return config."""
return {}
async def mock_setup(hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Mock setup."""
assert await async_setup_component(
hass, tts.DOMAIN, {tts.DOMAIN: {CONF_PLATFORM: DOMAIN} | config}
)
await hass.services.async_call(
tts.DOMAIN,
async def mock_config_entry_setup(hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Mock config entry setup."""
default_config = {tts.CONF_LANG: "en", CONF_TLD: "com"}
config_entry = MockConfigEntry(domain=DOMAIN, data=default_config | config)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
@pytest.mark.parametrize(
("setup", "tts_service", "service_data"),
[
(
"mock_setup",
"google_translate_say",
{
"entity_id": "media_player.something",
ATTR_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
(
"mock_config_entry_setup",
"speak",
{
ATTR_ENTITY_ID: "tts.google_en_com",
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
],
indirect=["setup"],
)
async def test_tts_service(
hass: HomeAssistant,
mock_gtts: MagicMock,
calls: list[ServiceCall],
setup: str,
tts_service: str,
service_data: dict[str, Any],
) -> None:
"""Test tts service."""
await hass.services.async_call(
tts.DOMAIN,
tts_service,
service_data,
blocking=True,
)
@ -88,22 +153,43 @@ async def test_service_say(hass: HomeAssistant, mock_gtts, calls) -> None:
}
async def test_service_say_german_config(hass: HomeAssistant, mock_gtts, calls) -> None:
"""Test service call say with german code in the config."""
await async_setup_component(
hass,
tts.DOMAIN,
{tts.DOMAIN: {"platform": "google_translate", "language": "de"}},
)
await hass.services.async_call(
tts.DOMAIN,
@pytest.mark.parametrize("config", [{tts.CONF_LANG: "de"}])
@pytest.mark.parametrize(
("setup", "tts_service", "service_data"),
[
(
"mock_setup",
"google_translate_say",
{
"entity_id": "media_player.something",
ATTR_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
(
"mock_config_entry_setup",
"speak",
{
ATTR_ENTITY_ID: "tts.google_de_com",
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
],
indirect=["setup"],
)
async def test_service_say_german_config(
hass: HomeAssistant,
mock_gtts: MagicMock,
calls: list[ServiceCall],
setup: str,
tts_service: str,
service_data: dict[str, Any],
) -> None:
"""Test service call say with german code in the config."""
await hass.services.async_call(
tts.DOMAIN,
tts_service,
service_data,
blocking=True,
)
@ -117,25 +203,44 @@ async def test_service_say_german_config(hass: HomeAssistant, mock_gtts, calls)
}
async def test_service_say_german_service(
hass: HomeAssistant, mock_gtts, calls
) -> None:
"""Test service call say with german code in the service."""
config = {
tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"}
}
await async_setup_component(hass, tts.DOMAIN, config)
await hass.services.async_call(
tts.DOMAIN,
"google_say",
@pytest.mark.parametrize(
("setup", "tts_service", "service_data"),
[
(
"mock_setup",
"google_translate_say",
{
"entity_id": "media_player.something",
ATTR_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
tts.ATTR_LANGUAGE: "de",
},
),
(
"mock_config_entry_setup",
"speak",
{
ATTR_ENTITY_ID: "tts.google_en_com",
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
tts.ATTR_LANGUAGE: "de",
},
),
],
indirect=["setup"],
)
async def test_service_say_german_service(
hass: HomeAssistant,
mock_gtts: MagicMock,
calls: list[ServiceCall],
setup: str,
tts_service: str,
service_data: dict[str, Any],
) -> None:
"""Test service call say with german code in the service."""
await hass.services.async_call(
tts.DOMAIN,
tts_service,
service_data,
blocking=True,
)
@ -149,22 +254,43 @@ async def test_service_say_german_service(
}
async def test_service_say_en_uk_config(hass: HomeAssistant, mock_gtts, calls) -> None:
"""Test service call say with en-uk code in the config."""
await async_setup_component(
hass,
tts.DOMAIN,
{tts.DOMAIN: {"platform": "google_translate", "language": "en-uk"}},
)
await hass.services.async_call(
tts.DOMAIN,
@pytest.mark.parametrize("config", [{tts.CONF_LANG: "en-uk"}])
@pytest.mark.parametrize(
("setup", "tts_service", "service_data"),
[
(
"mock_setup",
"google_translate_say",
{
"entity_id": "media_player.something",
ATTR_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
(
"mock_config_entry_setup",
"speak",
{
ATTR_ENTITY_ID: "tts.google_en_co_uk",
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
],
indirect=["setup"],
)
async def test_service_say_en_uk_config(
hass: HomeAssistant,
mock_gtts: MagicMock,
calls: list[ServiceCall],
setup: str,
tts_service: str,
service_data: dict[str, Any],
) -> None:
"""Test service call say with en-uk code in the config."""
await hass.services.async_call(
tts.DOMAIN,
tts_service,
service_data,
blocking=True,
)
@ -178,23 +304,44 @@ async def test_service_say_en_uk_config(hass: HomeAssistant, mock_gtts, calls) -
}
async def test_service_say_en_uk_service(hass: HomeAssistant, mock_gtts, calls) -> None:
"""Test service call say with en-uk code in the config."""
await async_setup_component(
hass,
tts.DOMAIN,
{tts.DOMAIN: {"platform": "google_translate"}},
)
await hass.services.async_call(
tts.DOMAIN,
@pytest.mark.parametrize(
("setup", "tts_service", "service_data"),
[
(
"mock_setup",
"google_translate_say",
{
"entity_id": "media_player.something",
ATTR_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
tts.ATTR_LANGUAGE: "en-uk",
},
),
(
"mock_config_entry_setup",
"speak",
{
ATTR_ENTITY_ID: "tts.google_en_com",
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
tts.ATTR_LANGUAGE: "en-uk",
},
),
],
indirect=["setup"],
)
async def test_service_say_en_uk_service(
hass: HomeAssistant,
mock_gtts: MagicMock,
calls: list[ServiceCall],
setup: str,
tts_service: str,
service_data: dict[str, Any],
) -> None:
"""Test service call say with en-uk code in the config."""
await hass.services.async_call(
tts.DOMAIN,
tts_service,
service_data,
blocking=True,
)
@ -208,21 +355,44 @@ async def test_service_say_en_uk_service(hass: HomeAssistant, mock_gtts, calls)
}
async def test_service_say_en_couk(hass: HomeAssistant, mock_gtts, calls) -> None:
"""Test service call say in co.uk tld accent."""
await async_setup_component(
hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}}
)
await hass.services.async_call(
tts.DOMAIN,
@pytest.mark.parametrize(
("setup", "tts_service", "service_data"),
[
(
"mock_setup",
"google_translate_say",
{
"entity_id": "media_player.something",
ATTR_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
tts.ATTR_OPTIONS: {"tld": "co.uk"},
},
),
(
"mock_config_entry_setup",
"speak",
{
ATTR_ENTITY_ID: "tts.google_en_com",
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
tts.ATTR_OPTIONS: {"tld": "co.uk"},
},
),
],
indirect=["setup"],
)
async def test_service_say_en_couk(
hass: HomeAssistant,
mock_gtts: MagicMock,
calls: list[ServiceCall],
setup: str,
tts_service: str,
service_data: dict[str, Any],
) -> None:
"""Test service call say in co.uk tld accent."""
await hass.services.async_call(
tts.DOMAIN,
tts_service,
service_data,
blocking=True,
)
@ -238,20 +408,44 @@ async def test_service_say_en_couk(hass: HomeAssistant, mock_gtts, calls) -> Non
}
async def test_service_say_error(hass: HomeAssistant, mock_gtts, calls) -> None:
@pytest.mark.parametrize(
("setup", "tts_service", "service_data"),
[
(
"mock_setup",
"google_translate_say",
{
ATTR_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
(
"mock_config_entry_setup",
"speak",
{
ATTR_ENTITY_ID: "tts.google_en_com",
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
),
],
indirect=["setup"],
)
async def test_service_say_error(
hass: HomeAssistant,
mock_gtts: MagicMock,
calls: list[ServiceCall],
setup: str,
tts_service: str,
service_data: dict[str, Any],
) -> None:
"""Test service call say with http response 400."""
mock_gtts.return_value.write_to_fp.side_effect = gTTSError
await async_setup_component(
hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}}
)
await hass.services.async_call(
tts.DOMAIN,
"google_translate_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
tts_service,
service_data,
blocking=True,
)