mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Deprecate cloud tts gender (#112256)
* Deprecate cloud tts gender option * Update http api and prefs * Test migration of prefs to minor version 4 * Adjust breaking date * Add test for bad voice in http api * Flatten tts info * Fix comments * Fix comment date Co-authored-by: Erik Montnemery <erik@montnemery.com> * Clarify voice validator --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
d31124d5d4
commit
ac008a4c6d
@ -33,7 +33,7 @@ PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
|
|||||||
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
|
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
|
||||||
PREF_GOOGLE_CONNECTED = "google_connected"
|
PREF_GOOGLE_CONNECTED = "google_connected"
|
||||||
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
|
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
|
||||||
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
|
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural")
|
||||||
DEFAULT_DISABLE_2FA = False
|
DEFAULT_DISABLE_2FA = False
|
||||||
DEFAULT_ALEXA_REPORT_STATE = True
|
DEFAULT_ALEXA_REPORT_STATE = True
|
||||||
DEFAULT_GOOGLE_REPORT_STATE = True
|
DEFAULT_GOOGLE_REPORT_STATE = True
|
||||||
|
@ -16,7 +16,7 @@ from aiohttp import web
|
|||||||
import attr
|
import attr
|
||||||
from hass_nabucasa import Cloud, auth, thingtalk
|
from hass_nabucasa import Cloud, auth, thingtalk
|
||||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||||
from hass_nabucasa.voice import MAP_VOICE
|
from hass_nabucasa.voice import TTS_VOICES
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
@ -426,6 +426,16 @@ async def websocket_subscription(
|
|||||||
async_manage_legacy_subscription_issue(hass, data)
|
async_manage_legacy_subscription_issue(hass, data)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
|
||||||
|
"""Validate language and voice."""
|
||||||
|
language, voice = value
|
||||||
|
if language not in TTS_VOICES:
|
||||||
|
raise vol.Invalid(f"Invalid language {language}")
|
||||||
|
if voice not in TTS_VOICES[language]:
|
||||||
|
raise vol.Invalid(f"Invalid voice {voice} for language {language}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
@_require_cloud_login
|
@_require_cloud_login
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
@ -436,7 +446,7 @@ async def websocket_subscription(
|
|||||||
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
|
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
|
||||||
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||||
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
||||||
vol.Coerce(tuple), vol.In(MAP_VOICE)
|
vol.Coerce(tuple), validate_language_voice
|
||||||
),
|
),
|
||||||
vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
|
vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
|
||||||
}
|
}
|
||||||
@ -840,5 +850,12 @@ def tts_info(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Fetch available tts info."""
|
"""Fetch available tts info."""
|
||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"], {"languages": [(lang, gender.value) for lang, gender in MAP_VOICE]}
|
msg["id"],
|
||||||
|
{
|
||||||
|
"languages": [
|
||||||
|
(language, voice)
|
||||||
|
for language, voices in TTS_VOICES.items()
|
||||||
|
for voice in voices
|
||||||
|
]
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,8 @@ from collections.abc import Callable, Coroutine
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from hass_nabucasa.voice import MAP_VOICE
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.auth.models import User
|
from homeassistant.auth.models import User
|
||||||
from homeassistant.components import webhook
|
from homeassistant.components import webhook
|
||||||
@ -48,7 +50,7 @@ from .const import (
|
|||||||
|
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_VERSION_MINOR = 3
|
STORAGE_VERSION_MINOR = 4
|
||||||
|
|
||||||
ALEXA_SETTINGS_VERSION = 3
|
ALEXA_SETTINGS_VERSION = 3
|
||||||
GOOGLE_SETTINGS_VERSION = 3
|
GOOGLE_SETTINGS_VERSION = 3
|
||||||
@ -82,6 +84,24 @@ class CloudPreferencesStore(Store):
|
|||||||
# In HA Core 2024.9, remove the import and also remove the Google
|
# In HA Core 2024.9, remove the import and also remove the Google
|
||||||
# assistant store if it's not been migrated by manual Google assistant
|
# assistant store if it's not been migrated by manual Google assistant
|
||||||
old_data.setdefault(PREF_GOOGLE_CONNECTED, await google_connected())
|
old_data.setdefault(PREF_GOOGLE_CONNECTED, await google_connected())
|
||||||
|
if old_minor_version < 4:
|
||||||
|
# Update the default TTS voice to the new default.
|
||||||
|
# The default tts voice is a tuple.
|
||||||
|
# The first item is the language, the second item used to be gender.
|
||||||
|
# The new second item is the voice name.
|
||||||
|
default_tts_voice = old_data.get(PREF_TTS_DEFAULT_VOICE)
|
||||||
|
if default_tts_voice and (voice_item_two := default_tts_voice[1]) in (
|
||||||
|
"female",
|
||||||
|
"male",
|
||||||
|
):
|
||||||
|
language: str = default_tts_voice[0]
|
||||||
|
if voice := MAP_VOICE.get((language, voice_item_two)):
|
||||||
|
old_data[PREF_TTS_DEFAULT_VOICE] = (
|
||||||
|
language,
|
||||||
|
voice,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
old_data[PREF_TTS_DEFAULT_VOICE] = DEFAULT_TTS_DEFAULT_VOICE
|
||||||
|
|
||||||
return old_data
|
return old_data
|
||||||
|
|
||||||
@ -332,7 +352,10 @@ class CloudPreferences:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def tts_default_voice(self) -> tuple[str, str]:
|
def tts_default_voice(self) -> tuple[str, str]:
|
||||||
"""Return the default TTS voice."""
|
"""Return the default TTS voice.
|
||||||
|
|
||||||
|
The return value is a tuple of language and voice.
|
||||||
|
"""
|
||||||
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return]
|
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return]
|
||||||
|
|
||||||
async def get_cloud_user(self) -> str:
|
async def get_cloud_user(self) -> str:
|
||||||
|
@ -24,6 +24,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
|
"deprecated_gender": {
|
||||||
|
"title": "The '{deprecated_option}' text-to-speech option is deprecated",
|
||||||
|
"fix_flow": {
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"title": "[%key:component::cloud::issues::deprecated_voice::title%]",
|
||||||
|
"description": "The '{deprecated_option}' option for text-to-speech in the {integration_name} integration is deprecated and will be removed.\nPlease update your automations and scripts to replace the '{deprecated_option}' option with an option for a supported '{replacement_option}' instead."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"deprecated_tts_platform_config": {
|
"deprecated_tts_platform_config": {
|
||||||
"title": "The Cloud text-to-speech platform configuration is deprecated",
|
"title": "The Cloud text-to-speech platform configuration is deprecated",
|
||||||
"description": "The whole `platform: cloud` entry under the `tts:` section in configuration.yaml is deprecated and should be removed. You can use the UI to change settings for the Cloud text-to-speech platform. Please adjust your configuration.yaml and restart Home Assistant to fix this issue."
|
"description": "The whole `platform: cloud` entry under the `tts:` section in configuration.yaml is deprecated and should be removed. You can use the UI to change settings for the Cloud text-to-speech platform. Please adjust your configuration.yaml and restart Home Assistant to fix this issue."
|
||||||
|
@ -7,7 +7,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from hass_nabucasa import Cloud
|
from hass_nabucasa import Cloud
|
||||||
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, VoiceError
|
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, Gender, VoiceError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.tts import (
|
from homeassistant.components.tts import (
|
||||||
@ -97,17 +97,7 @@ async def async_get_engine(
|
|||||||
) -> CloudProvider:
|
) -> CloudProvider:
|
||||||
"""Set up Cloud speech component."""
|
"""Set up Cloud speech component."""
|
||||||
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||||
|
cloud_provider = CloudProvider(cloud)
|
||||||
language: str | None
|
|
||||||
gender: str | None
|
|
||||||
if discovery_info is not None:
|
|
||||||
language = None
|
|
||||||
gender = None
|
|
||||||
else:
|
|
||||||
language = config[CONF_LANG]
|
|
||||||
gender = config[ATTR_GENDER]
|
|
||||||
|
|
||||||
cloud_provider = CloudProvider(cloud, language, gender)
|
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
discovery_info["platform_loaded"].set()
|
discovery_info["platform_loaded"].set()
|
||||||
return cloud_provider
|
return cloud_provider
|
||||||
@ -134,11 +124,11 @@ class CloudTTSEntity(TextToSpeechEntity):
|
|||||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||||
"""Initialize cloud text-to-speech entity."""
|
"""Initialize cloud text-to-speech entity."""
|
||||||
self.cloud = cloud
|
self.cloud = cloud
|
||||||
self._language, self._gender = cloud.client.prefs.tts_default_voice
|
self._language, self._voice = cloud.client.prefs.tts_default_voice
|
||||||
|
|
||||||
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
|
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
|
||||||
"""Sync preferences."""
|
"""Sync preferences."""
|
||||||
self._language, self._gender = prefs.tts_default_voice
|
self._language, self._voice = prefs.tts_default_voice
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_language(self) -> str:
|
def default_language(self) -> str:
|
||||||
@ -149,8 +139,8 @@ class CloudTTSEntity(TextToSpeechEntity):
|
|||||||
def default_options(self) -> dict[str, Any]:
|
def default_options(self) -> dict[str, Any]:
|
||||||
"""Return a dict include default options."""
|
"""Return a dict include default options."""
|
||||||
return {
|
return {
|
||||||
ATTR_GENDER: self._gender,
|
|
||||||
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
||||||
|
ATTR_VOICE: self._voice,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -161,6 +151,7 @@ class CloudTTSEntity(TextToSpeechEntity):
|
|||||||
@property
|
@property
|
||||||
def supported_options(self) -> list[str]:
|
def supported_options(self) -> list[str]:
|
||||||
"""Return list of supported options like voice, emotion."""
|
"""Return list of supported options like voice, emotion."""
|
||||||
|
# The gender option is deprecated and will be removed in 2024.10.0.
|
||||||
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
@ -184,6 +175,8 @@ class CloudTTSEntity(TextToSpeechEntity):
|
|||||||
self, message: str, language: str, options: dict[str, Any]
|
self, message: str, language: str, options: dict[str, Any]
|
||||||
) -> TtsAudioType:
|
) -> TtsAudioType:
|
||||||
"""Load TTS from Home Assistant Cloud."""
|
"""Load TTS from Home Assistant Cloud."""
|
||||||
|
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||||
|
gender = handle_deprecated_gender(self.hass, gender)
|
||||||
original_voice: str | None = options.get(ATTR_VOICE)
|
original_voice: str | None = options.get(ATTR_VOICE)
|
||||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||||
# Process TTS
|
# Process TTS
|
||||||
@ -191,7 +184,7 @@ class CloudTTSEntity(TextToSpeechEntity):
|
|||||||
data = await self.cloud.voice.process_tts(
|
data = await self.cloud.voice.process_tts(
|
||||||
text=message,
|
text=message,
|
||||||
language=language,
|
language=language,
|
||||||
gender=options.get(ATTR_GENDER),
|
gender=gender,
|
||||||
voice=voice,
|
voice=voice,
|
||||||
output=options[ATTR_AUDIO_OUTPUT],
|
output=options[ATTR_AUDIO_OUTPUT],
|
||||||
)
|
)
|
||||||
@ -205,24 +198,16 @@ class CloudTTSEntity(TextToSpeechEntity):
|
|||||||
class CloudProvider(Provider):
|
class CloudProvider(Provider):
|
||||||
"""Home Assistant Cloud speech API provider."""
|
"""Home Assistant Cloud speech API provider."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||||
self, cloud: Cloud[CloudClient], language: str | None, gender: str | None
|
|
||||||
) -> None:
|
|
||||||
"""Initialize cloud provider."""
|
"""Initialize cloud provider."""
|
||||||
self.cloud = cloud
|
self.cloud = cloud
|
||||||
self.name = "Cloud"
|
self.name = "Cloud"
|
||||||
self._language = language
|
self._language, self._voice = cloud.client.prefs.tts_default_voice
|
||||||
self._gender = gender
|
|
||||||
|
|
||||||
if self._language is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._language, self._gender = cloud.client.prefs.tts_default_voice
|
|
||||||
cloud.client.prefs.async_listen_updates(self._sync_prefs)
|
cloud.client.prefs.async_listen_updates(self._sync_prefs)
|
||||||
|
|
||||||
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
|
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
|
||||||
"""Sync preferences."""
|
"""Sync preferences."""
|
||||||
self._language, self._gender = prefs.tts_default_voice
|
self._language, self._voice = prefs.tts_default_voice
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_language(self) -> str | None:
|
def default_language(self) -> str | None:
|
||||||
@ -237,6 +222,7 @@ class CloudProvider(Provider):
|
|||||||
@property
|
@property
|
||||||
def supported_options(self) -> list[str]:
|
def supported_options(self) -> list[str]:
|
||||||
"""Return list of supported options like voice, emotion."""
|
"""Return list of supported options like voice, emotion."""
|
||||||
|
# The gender option is deprecated and will be removed in 2024.10.0.
|
||||||
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -250,23 +236,25 @@ class CloudProvider(Provider):
|
|||||||
def default_options(self) -> dict[str, Any]:
|
def default_options(self) -> dict[str, Any]:
|
||||||
"""Return a dict include default options."""
|
"""Return a dict include default options."""
|
||||||
return {
|
return {
|
||||||
ATTR_GENDER: self._gender,
|
|
||||||
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
||||||
|
ATTR_VOICE: self._voice,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_get_tts_audio(
|
async def async_get_tts_audio(
|
||||||
self, message: str, language: str, options: dict[str, Any]
|
self, message: str, language: str, options: dict[str, Any]
|
||||||
) -> TtsAudioType:
|
) -> TtsAudioType:
|
||||||
"""Load TTS from Home Assistant Cloud."""
|
"""Load TTS from Home Assistant Cloud."""
|
||||||
original_voice: str | None = options.get(ATTR_VOICE)
|
|
||||||
assert self.hass is not None
|
assert self.hass is not None
|
||||||
|
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||||
|
gender = handle_deprecated_gender(self.hass, gender)
|
||||||
|
original_voice: str | None = options.get(ATTR_VOICE)
|
||||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||||
# Process TTS
|
# Process TTS
|
||||||
try:
|
try:
|
||||||
data = await self.cloud.voice.process_tts(
|
data = await self.cloud.voice.process_tts(
|
||||||
text=message,
|
text=message,
|
||||||
language=language,
|
language=language,
|
||||||
gender=options.get(ATTR_GENDER),
|
gender=gender,
|
||||||
voice=voice,
|
voice=voice,
|
||||||
output=options[ATTR_AUDIO_OUTPUT],
|
output=options[ATTR_AUDIO_OUTPUT],
|
||||||
)
|
)
|
||||||
@ -277,6 +265,32 @@ class CloudProvider(Provider):
|
|||||||
return (str(options[ATTR_AUDIO_OUTPUT].value), data)
|
return (str(options[ATTR_AUDIO_OUTPUT].value), data)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def handle_deprecated_gender(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
gender: Gender | str | None,
|
||||||
|
) -> Gender | None:
|
||||||
|
"""Handle deprecated gender."""
|
||||||
|
if gender is None:
|
||||||
|
return None
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_gender",
|
||||||
|
is_fixable=True,
|
||||||
|
is_persistent=True,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
breaks_in_ha_version="2024.10.0",
|
||||||
|
translation_key="deprecated_gender",
|
||||||
|
translation_placeholders={
|
||||||
|
"integration_name": "Home Assistant Cloud",
|
||||||
|
"deprecated_option": "gender",
|
||||||
|
"replacement_option": "voice",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return Gender(gender)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def handle_deprecated_voice(
|
def handle_deprecated_voice(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from hass_nabucasa import thingtalk, voice
|
from hass_nabucasa import thingtalk
|
||||||
from hass_nabucasa.auth import Unauthenticated, UnknownError
|
from hass_nabucasa.auth import Unauthenticated, UnknownError
|
||||||
from hass_nabucasa.const import STATE_CONNECTED
|
from hass_nabucasa.const import STATE_CONNECTED
|
||||||
|
from hass_nabucasa.voice import TTS_VOICES
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.alexa import errors as alexa_errors
|
from homeassistant.components.alexa import errors as alexa_errors
|
||||||
@ -17,6 +19,7 @@ from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY
|
|||||||
from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN
|
from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN
|
||||||
from homeassistant.components.google_assistant.helpers import GoogleEntity
|
from homeassistant.components.google_assistant.helpers import GoogleEntity
|
||||||
from homeassistant.components.homeassistant import exposed_entities
|
from homeassistant.components.homeassistant import exposed_entities
|
||||||
|
from homeassistant.components.websocket_api import ERR_INVALID_FORMAT
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -774,7 +777,7 @@ async def test_websocket_status(
|
|||||||
"google_report_state": True,
|
"google_report_state": True,
|
||||||
"remote_allow_remote_enable": True,
|
"remote_allow_remote_enable": True,
|
||||||
"remote_enabled": False,
|
"remote_enabled": False,
|
||||||
"tts_default_voice": ["en-US", "female"],
|
"tts_default_voice": ["en-US", "JennyNeural"],
|
||||||
},
|
},
|
||||||
"alexa_entities": {
|
"alexa_entities": {
|
||||||
"include_domains": [],
|
"include_domains": [],
|
||||||
@ -896,14 +899,13 @@ async def test_websocket_update_preferences(
|
|||||||
|
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
await client.send_json(
|
await client.send_json_auto_id(
|
||||||
{
|
{
|
||||||
"id": 5,
|
|
||||||
"type": "cloud/update_prefs",
|
"type": "cloud/update_prefs",
|
||||||
"alexa_enabled": False,
|
"alexa_enabled": False,
|
||||||
"google_enabled": False,
|
"google_enabled": False,
|
||||||
"google_secure_devices_pin": "1234",
|
"google_secure_devices_pin": "1234",
|
||||||
"tts_default_voice": ["en-GB", "male"],
|
"tts_default_voice": ["en-GB", "RyanNeural"],
|
||||||
"remote_allow_remote_enable": False,
|
"remote_allow_remote_enable": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -914,7 +916,34 @@ async def test_websocket_update_preferences(
|
|||||||
assert not cloud.client.prefs.alexa_enabled
|
assert not cloud.client.prefs.alexa_enabled
|
||||||
assert cloud.client.prefs.google_secure_devices_pin == "1234"
|
assert cloud.client.prefs.google_secure_devices_pin == "1234"
|
||||||
assert cloud.client.prefs.remote_allow_remote_enable is False
|
assert cloud.client.prefs.remote_allow_remote_enable is False
|
||||||
assert cloud.client.prefs.tts_default_voice == ("en-GB", "male")
|
assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("language", "voice"), [("en-GB", "bad_voice"), ("bad_language", "RyanNeural")]
|
||||||
|
)
|
||||||
|
async def test_websocket_update_preferences_bad_voice(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
cloud: MagicMock,
|
||||||
|
setup_cloud: None,
|
||||||
|
language: str,
|
||||||
|
voice: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test updating preference."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "cloud/update_prefs",
|
||||||
|
"tts_default_voice": [language, voice],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await client.receive_json()
|
||||||
|
|
||||||
|
assert not response["success"]
|
||||||
|
assert response["error"]["code"] == ERR_INVALID_FORMAT
|
||||||
|
assert cloud.client.prefs.tts_default_voice == ("en-US", "JennyNeural")
|
||||||
|
|
||||||
|
|
||||||
async def test_websocket_update_preferences_alexa_report_state(
|
async def test_websocket_update_preferences_alexa_report_state(
|
||||||
@ -1596,24 +1625,23 @@ async def test_tts_info(
|
|||||||
setup_cloud: None,
|
setup_cloud: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we can get TTS info."""
|
"""Test that we can get TTS info."""
|
||||||
# Verify the format is as expected
|
|
||||||
assert voice.MAP_VOICE[("en-US", voice.Gender.FEMALE)] == "JennyNeural"
|
|
||||||
|
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
with patch.dict(
|
await client.send_json_auto_id({"type": "cloud/tts/info"})
|
||||||
"homeassistant.components.cloud.http_api.MAP_VOICE",
|
response = await client.receive_json()
|
||||||
{
|
|
||||||
("en-US", voice.Gender.MALE): "GuyNeural",
|
|
||||||
("en-US", voice.Gender.FEMALE): "JennyNeural",
|
|
||||||
},
|
|
||||||
clear=True,
|
|
||||||
):
|
|
||||||
await client.send_json({"id": 5, "type": "cloud/tts/info"})
|
|
||||||
response = await client.receive_json()
|
|
||||||
|
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {"languages": [["en-US", "male"], ["en-US", "female"]]}
|
assert response["result"] == {
|
||||||
|
"languages": json.loads(
|
||||||
|
json.dumps(
|
||||||
|
[
|
||||||
|
(language, voice)
|
||||||
|
for language, voices in TTS_VOICES.items()
|
||||||
|
for voice in voices
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
"""Test Cloud preferences."""
|
"""Test Cloud preferences."""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import ANY, patch
|
from unittest.mock import ANY, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
|
from homeassistant.components.cloud.const import DOMAIN, PREF_TTS_DEFAULT_VOICE
|
||||||
from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences
|
from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
async def test_set_username(hass: HomeAssistant) -> None:
|
async def test_set_username(hass: HomeAssistant) -> None:
|
||||||
@ -149,3 +151,26 @@ async def test_import_google_assistant_settings(
|
|||||||
prefs = CloudPreferences(hass)
|
prefs = CloudPreferences(hass)
|
||||||
await prefs.async_initialize()
|
await prefs.async_initialize()
|
||||||
assert prefs.google_connected == google_connected
|
assert prefs.google_connected == google_connected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("stored_language", "expected_language", "voice"),
|
||||||
|
[("en-US", "en-US", "GuyNeural"), ("missing_language", "en-US", "JennyNeural")],
|
||||||
|
)
|
||||||
|
async def test_tts_default_voice_legacy_gender(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cloud: MagicMock,
|
||||||
|
hass_storage: dict[str, Any],
|
||||||
|
stored_language: str,
|
||||||
|
expected_language: str,
|
||||||
|
voice: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test tts with legacy gender as default tts voice setting in storage."""
|
||||||
|
hass_storage[STORAGE_KEY] = {
|
||||||
|
"version": 1,
|
||||||
|
"data": {PREF_TTS_DEFAULT_VOICE: [stored_language, "male"]},
|
||||||
|
}
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert cloud.client.prefs.tts_default_voice == (expected_language, voice)
|
||||||
|
@ -6,7 +6,7 @@ from http import HTTPStatus
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from hass_nabucasa.voice import MAP_VOICE, VoiceError, VoiceTokenError
|
from hass_nabucasa.voice import TTS_VOICES, VoiceError, VoiceTokenError
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -44,7 +44,11 @@ async def internal_url_mock(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
def test_default_exists() -> None:
|
def test_default_exists() -> None:
|
||||||
"""Test our default language exists."""
|
"""Test our default language exists."""
|
||||||
assert const.DEFAULT_TTS_DEFAULT_VOICE in MAP_VOICE
|
assert const.DEFAULT_TTS_DEFAULT_VOICE[0] in TTS_VOICES
|
||||||
|
assert (
|
||||||
|
const.DEFAULT_TTS_DEFAULT_VOICE[1]
|
||||||
|
in TTS_VOICES[const.DEFAULT_TTS_DEFAULT_VOICE[0]]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_schema() -> None:
|
def test_schema() -> None:
|
||||||
@ -105,7 +109,7 @@ async def test_prefs_default_voice(
|
|||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert cloud.client.prefs.tts_default_voice == ("en-US", "female")
|
assert cloud.client.prefs.tts_default_voice == ("en-US", "JennyNeural")
|
||||||
|
|
||||||
on_start_callback = cloud.register_on_start.call_args[0][0]
|
on_start_callback = cloud.register_on_start.call_args[0][0]
|
||||||
await on_start_callback()
|
await on_start_callback()
|
||||||
@ -116,13 +120,13 @@ async def test_prefs_default_voice(
|
|||||||
assert engine is not None
|
assert engine is not None
|
||||||
# The platform config provider will be overridden by the discovery info provider.
|
# The platform config provider will be overridden by the discovery info provider.
|
||||||
assert engine.default_language == "en-US"
|
assert engine.default_language == "en-US"
|
||||||
assert engine.default_options == {"gender": "female", "audio_output": "mp3"}
|
assert engine.default_options == {"audio_output": "mp3", "voice": "JennyNeural"}
|
||||||
|
|
||||||
await set_cloud_prefs({"tts_default_voice": ("nl-NL", "male")})
|
await set_cloud_prefs({"tts_default_voice": ("nl-NL", "MaartenNeural")})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert engine.default_language == "nl-NL"
|
assert engine.default_language == "nl-NL"
|
||||||
assert engine.default_options == {"gender": "male", "audio_output": "mp3"}
|
assert engine.default_options == {"audio_output": "mp3", "voice": "MaartenNeural"}
|
||||||
|
|
||||||
|
|
||||||
async def test_deprecated_platform_config(
|
async def test_deprecated_platform_config(
|
||||||
@ -224,11 +228,11 @@ async def test_get_tts_audio(
|
|||||||
"url": (
|
"url": (
|
||||||
"http://example.local:8123/api/tts_proxy/"
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
"path": (
|
"path": (
|
||||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -237,7 +241,7 @@ async def test_get_tts_audio(
|
|||||||
assert mock_process_tts.call_args is not None
|
assert mock_process_tts.call_args is not None
|
||||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
|
|
||||||
|
|
||||||
@ -276,11 +280,11 @@ async def test_get_tts_audio_logged_out(
|
|||||||
"url": (
|
"url": (
|
||||||
"http://example.local:8123/api/tts_proxy/"
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
"path": (
|
"path": (
|
||||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -289,7 +293,7 @@ async def test_get_tts_audio_logged_out(
|
|||||||
assert mock_process_tts.call_args is not None
|
assert mock_process_tts.call_args is not None
|
||||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
|
|
||||||
|
|
||||||
@ -340,11 +344,11 @@ async def test_tts_entity(
|
|||||||
"url": (
|
"url": (
|
||||||
"http://example.local:8123/api/tts_proxy/"
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_en-us_e09b5a0968_{entity_id}.mp3"
|
f"_en-us_5c97d21c48_{entity_id}.mp3"
|
||||||
),
|
),
|
||||||
"path": (
|
"path": (
|
||||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_en-us_e09b5a0968_{entity_id}.mp3"
|
f"_en-us_5c97d21c48_{entity_id}.mp3"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -353,7 +357,7 @@ async def test_tts_entity(
|
|||||||
assert mock_process_tts.call_args is not None
|
assert mock_process_tts.call_args is not None
|
||||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
@ -480,11 +484,11 @@ async def test_deprecated_voice(
|
|||||||
"url": (
|
"url": (
|
||||||
"http://example.local:8123/api/tts_proxy/"
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_{language.lower()}_1c4ec2f170_{expected_url_suffix}.mp3"
|
f"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
"path": (
|
"path": (
|
||||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_{language.lower()}_1c4ec2f170_{expected_url_suffix}.mp3"
|
f"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -493,7 +497,7 @@ async def test_deprecated_voice(
|
|||||||
assert mock_process_tts.call_args is not None
|
assert mock_process_tts.call_args is not None
|
||||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
assert mock_process_tts.call_args.kwargs["language"] == language
|
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||||
assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice
|
assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice
|
||||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
issue = issue_registry.async_get_issue(
|
issue = issue_registry.async_get_issue(
|
||||||
@ -513,11 +517,11 @@ async def test_deprecated_voice(
|
|||||||
"url": (
|
"url": (
|
||||||
"http://example.local:8123/api/tts_proxy/"
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_{language.lower()}_a1c3b0ac0e_{expected_url_suffix}.mp3"
|
f"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
"path": (
|
"path": (
|
||||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
f"_{language.lower()}_a1c3b0ac0e_{expected_url_suffix}.mp3"
|
f"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -526,7 +530,7 @@ async def test_deprecated_voice(
|
|||||||
assert mock_process_tts.call_args is not None
|
assert mock_process_tts.call_args is not None
|
||||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
assert mock_process_tts.call_args.kwargs["language"] == language
|
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||||
assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice
|
assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice
|
||||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
issue = issue_registry.async_get_issue(
|
issue = issue_registry.async_get_issue(
|
||||||
@ -542,3 +546,107 @@ async def test_deprecated_voice(
|
|||||||
"deprecated_voice": deprecated_voice,
|
"deprecated_voice": deprecated_voice,
|
||||||
"replacement_voice": replacement_voice,
|
"replacement_voice": replacement_voice,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("data", "expected_url_suffix"),
|
||||||
|
[
|
||||||
|
({"platform": DOMAIN}, DOMAIN),
|
||||||
|
({"engine_id": DOMAIN}, DOMAIN),
|
||||||
|
({"engine_id": "tts.home_assistant_cloud"}, "tts.home_assistant_cloud"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_deprecated_gender(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
issue_registry: IssueRegistry,
|
||||||
|
cloud: MagicMock,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
data: dict[str, Any],
|
||||||
|
expected_url_suffix: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test we create an issue when a deprecated gender is used for text-to-speech."""
|
||||||
|
language = "zh-CN"
|
||||||
|
gender_option = "male"
|
||||||
|
mock_process_tts = AsyncMock(
|
||||||
|
return_value=b"",
|
||||||
|
)
|
||||||
|
cloud.voice.process_tts = mock_process_tts
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await cloud.login("test-user", "test-pass")
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
# Test without deprecated gender option.
|
||||||
|
url = "/api/tts_get_url"
|
||||||
|
data |= {
|
||||||
|
"message": "There is someone at the door.",
|
||||||
|
"language": language,
|
||||||
|
}
|
||||||
|
|
||||||
|
req = await client.post(url, json=data)
|
||||||
|
assert req.status == HTTPStatus.OK
|
||||||
|
response = await req.json()
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"url": (
|
||||||
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
|
f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3"
|
||||||
|
),
|
||||||
|
"path": (
|
||||||
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
|
f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_process_tts.call_count == 1
|
||||||
|
assert mock_process_tts.call_args is not None
|
||||||
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
|
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||||
|
assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural"
|
||||||
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
|
issue = issue_registry.async_get_issue("cloud", "deprecated_gender")
|
||||||
|
assert issue is None
|
||||||
|
mock_process_tts.reset_mock()
|
||||||
|
|
||||||
|
# Test with deprecated gender option.
|
||||||
|
data["options"] = {"gender": gender_option}
|
||||||
|
|
||||||
|
req = await client.post(url, json=data)
|
||||||
|
assert req.status == HTTPStatus.OK
|
||||||
|
response = await req.json()
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"url": (
|
||||||
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
|
f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3"
|
||||||
|
),
|
||||||
|
"path": (
|
||||||
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
|
f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_process_tts.call_count == 1
|
||||||
|
assert mock_process_tts.call_args is not None
|
||||||
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
|
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||||
|
assert mock_process_tts.call_args.kwargs["gender"] == gender_option
|
||||||
|
assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural"
|
||||||
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
|
issue = issue_registry.async_get_issue("cloud", "deprecated_gender")
|
||||||
|
assert issue is not None
|
||||||
|
assert issue.breaks_in_ha_version == "2024.10.0"
|
||||||
|
assert issue.is_fixable is True
|
||||||
|
assert issue.is_persistent is True
|
||||||
|
assert issue.severity == IssueSeverity.WARNING
|
||||||
|
assert issue.translation_key == "deprecated_gender"
|
||||||
|
assert issue.translation_placeholders == {
|
||||||
|
"integration_name": "Home Assistant Cloud",
|
||||||
|
"deprecated_option": "gender",
|
||||||
|
"replacement_option": "voice",
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user