diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index aa8864ad23d..119a013ebf6 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -136,6 +136,44 @@ class TTSCache(TypedDict): voice: bytes +@callback +def async_resolve_engine(hass: HomeAssistant, engine: str | None) -> str | None: + """Resolve engine. + + Returns None if no engines found or invalid engine passed in. + """ + manager: SpeechManager = hass.data[DOMAIN] + + if engine is not None: + if engine not in manager.providers: + return None + return engine + + if not manager.providers: + return None + + if "cloud" in manager.providers: + return "cloud" + + return next(iter(manager.providers)) + + +async def async_support_options( + hass: HomeAssistant, + engine: str, + language: str | None = None, + options: dict | None = None, +) -> bool: + """Return if an engine supports options.""" + manager: SpeechManager = hass.data[DOMAIN] + try: + manager.process_options(engine, language, options) + except HomeAssistantError: + return False + + return True + + async def async_get_media_source_audio( hass: HomeAssistant, media_source_id: str, diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index c197632c11e..f52292e8096 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -40,16 +40,12 @@ def generate_media_source_id( cache: bool | None = None, ) -> str: """Generate a media source ID for text-to-speech.""" + from . import async_resolve_engine # pylint: disable=import-outside-toplevel + manager: SpeechManager = hass.data[DOMAIN] - if engine is not None: - pass - elif not manager.providers: - raise HomeAssistantError("No TTS providers available") - elif "cloud" in manager.providers: - engine = "cloud" - else: - engine = next(iter(manager.providers)) + if (engine := async_resolve_engine(hass, engine)) is None: + raise HomeAssistantError("Invalid TTS provider selected") manager.process_options(engine, language, options) params = { diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 251ed9b30c0..694c9ff676c 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,6 +1,7 @@ """The tests for the TTS component.""" from http import HTTPStatus from typing import Any +from unittest.mock import patch import pytest import voluptuous as vol @@ -972,3 +973,26 @@ async def test_generate_media_source_id_invalid_options( """Test generating a media source ID.""" with pytest.raises(HomeAssistantError): tts.generate_media_source_id(hass, "msg", engine, language, options, None) + + +def test_resolve_engine(hass: HomeAssistant, setup_tts) -> None: + """Test resolving engine.""" + assert tts.async_resolve_engine(hass, None) == "test" + assert tts.async_resolve_engine(hass, "test") == "test" + assert tts.async_resolve_engine(hass, "non-existing") is None + + with patch.dict(hass.data[tts.DOMAIN].providers, {}, clear=True): + assert tts.async_resolve_engine(hass, "test") is None + + with patch.dict(hass.data[tts.DOMAIN].providers, {"cloud": object()}): + assert tts.async_resolve_engine(hass, None) == "cloud" + + +async def test_support_options(hass: HomeAssistant, setup_tts) -> None: + """Test supporting options.""" + assert await tts.async_support_options(hass, "test", "en") is True + assert await tts.async_support_options(hass, "test", "nl") is False + assert ( + await tts.async_support_options(hass, "test", "en", {"invalid_option": "yo"}) + is False + )