core/homeassistant/components/tts/entity.py

160 lines
5.2 KiB
Python
Raw Normal View History

"""Entity for Text-to-Speech."""
from collections.abc import Mapping
from functools import partial
from typing import Any, final
from propcache.api import cached_property
from homeassistant.components.media_player import (
ATTR_MEDIA_ANNOUNCE,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA,
MediaType,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import callback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import dt as dt_util
from .const import TtsAudioType
from .media_source import generate_media_source_id
from .models import Voice
CACHED_PROPERTIES_WITH_ATTR_ = {
"default_language",
"default_options",
"supported_languages",
"supported_options",
}
class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Represent a single TTS engine."""
_attr_should_poll = False
__last_tts_loaded: str | None = None
_attr_default_language: str
_attr_default_options: Mapping[str, Any] | None = None
_attr_supported_languages: list[str]
_attr_supported_options: list[str] | None = None
@property
@final
def state(self) -> str | None:
"""Return the state of the entity."""
if self.__last_tts_loaded is None:
return None
return self.__last_tts_loaded
@cached_property
def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
return self._attr_supported_languages
@cached_property
def default_language(self) -> str:
"""Return the default language."""
return self._attr_default_language
@cached_property
def supported_options(self) -> list[str] | None:
"""Return a list of supported options like voice, emotions."""
return self._attr_supported_options
@cached_property
def default_options(self) -> Mapping[str, Any] | None:
"""Return a mapping with the default options."""
return self._attr_default_options
@callback
def async_get_supported_voices(self, language: str) -> list[Voice] | None:
"""Return a list of supported voices for a language."""
return None
async def async_internal_added_to_hass(self) -> None:
"""Call when the entity is added to hass."""
await super().async_internal_added_to_hass()
try:
_ = self.default_language
except AttributeError as err:
raise AttributeError(
"TTS entities must either set the '_attr_default_language' attribute or override the 'default_language' property"
) from err
try:
_ = self.supported_languages
except AttributeError as err:
raise AttributeError(
"TTS entities must either set the '_attr_supported_languages' attribute or override the 'supported_languages' property"
) from err
state = await self.async_get_last_state()
if (
state is not None
and state.state is not None
and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
):
self.__last_tts_loaded = state.state
async def async_speak(
self,
media_player_entity_id: list[str],
message: str,
cache: bool,
language: str | None = None,
options: dict | None = None,
) -> None:
"""Speak via a Media Player."""
await self.hass.services.async_call(
DOMAIN_MP,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_entity_id,
ATTR_MEDIA_CONTENT_ID: generate_media_source_id(
self.hass,
message=message,
engine=self.entity_id,
language=language,
options=options,
cache=cache,
),
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_ANNOUNCE: True,
},
blocking=True,
context=self._context,
)
@final
async def internal_async_get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Process an audio stream to TTS service.
Only streaming content is allowed!
"""
self.__last_tts_loaded = dt_util.utcnow().isoformat()
self.async_write_ha_state()
return await self.async_get_tts_audio(
message=message, language=language, options=options
)
def get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load tts audio file from the engine."""
raise NotImplementedError
async def async_get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load tts audio file from the engine.
Return a tuple of file extension and data as bytes.
"""
return await self.hass.async_add_executor_job(
partial(self.get_tts_audio, message, language, options=options)
)