diff --git a/homeassistant/components/tts/marytts.py b/homeassistant/components/tts/marytts.py new file mode 100644 index 00000000000..ffb6950d79b --- /dev/null +++ b/homeassistant/components/tts/marytts.py @@ -0,0 +1,117 @@ +""" +Support for the MaryTTS service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tts.marytts/ +""" +import asyncio +import logging +import re + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_LANGUAGES = [ + 'de', 'en-GB', 'en-US', 'fr', 'it', 'lb', 'ru', 'sv', 'te', 'tr' +] + +SUPPORT_VOICES = [ + 'cmu-slt-hsmm' +] + +SUPPORT_CODEC = [ + 'aiff', 'au', 'wav' +] + +CONF_VOICE = 'voice' +CONF_CODEC = 'codec' + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 59125 +DEFAULT_LANG = 'en-US' +DEFAULT_VOICE = 'cmu-slt-hsmm' +DEFAULT_CODEC = 'wav' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORT_VOICES), + vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODEC) +}) + + +@asyncio.coroutine +def async_get_engine(hass, config): + """Setup MaryTTS speech component.""" + return MaryTTSProvider(hass, config) + + +class MaryTTSProvider(Provider): + """MaryTTS speech api provider.""" + + def __init__(self, hass, conf): + """Init MaryTTS TTS service.""" + self.hass = hass + self._host = conf.get(CONF_HOST) + self._port = conf.get(CONF_PORT) + self._codec = conf.get(CONF_CODEC) + self._voice = conf.get(CONF_VOICE) + self._language = conf.get(CONF_LANG) + self.name = 'MaryTTS' + + @property + def default_language(self): + """Default language.""" + return self._language + + @property + def supported_languages(self): + """List of supported languages.""" + return SUPPORT_LANGUAGES + + @asyncio.coroutine + def async_get_tts_audio(self, message, language, options=None): + """Load TTS from MaryTTS.""" + websession = async_get_clientsession(self.hass) + + actual_language = re.sub('-', '_', language) + + try: + with async_timeout.timeout(10, loop=self.hass.loop): + url = 'http://{}:{}/process?'.format(self._host, self._port) + + audio = self._codec.upper() + if audio == 'WAV': + audio = 'WAVE' + + url_param = { + 'INPUT_TEXT': message, + 'INPUT_TYPE': 'TEXT', + 'AUDIO': audio, + 'VOICE': self._voice, + 'OUTPUT_TYPE': 'AUDIO', + 'LOCALE': actual_language + } + + request = yield from websession.get(url, params=url_param) + + if request.status != 200: + _LOGGER.error("Error %d on load url %s.", + request.status, request.url) + return (None, None) + data = yield from request.read() + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout for MaryTTS API.") + return (None, None) + + return (self._codec, data) diff --git a/tests/components/tts/test_marytts.py b/tests/components/tts/test_marytts.py new file mode 100644 index 00000000000..29e1a635462 --- /dev/null +++ b/tests/components/tts/test_marytts.py @@ -0,0 +1,121 @@ +"""The tests for the MaryTTS speech platform.""" +import asyncio +import os +import shutil + +import homeassistant.components.tts as tts +from homeassistant.setup import setup_component +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP) + +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_service) + + +class TestTTSMaryTTSPlatform(object): + """Test the speech component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.url = "http://localhost:59125/process?" + self.url_param = { + 'INPUT_TEXT': 'HomeAssistant', + 'INPUT_TYPE': 'TEXT', + 'AUDIO': 'WAVE', + 'VOICE': 'cmu-slt-hsmm', + 'OUTPUT_TYPE': 'AUDIO', + 'LOCALE': 'en_US' + } + + def teardown_method(self): + """Stop everything that was started.""" + default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) + + self.hass.stop() + + def test_setup_component(self): + """Test setup component.""" + config = { + tts.DOMAIN: { + 'platform': 'marytts' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + def test_service_say(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'marytts', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'marytts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert len(calls) == 1 + + def test_service_say_timeout(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=200, + exc=asyncio.TimeoutError()) + + config = { + tts.DOMAIN: { + 'platform': 'marytts', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'marytts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + assert len(aioclient_mock.mock_calls) == 1 + + def test_service_say_http_error(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=403, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'marytts', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'marytts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(calls) == 0