diff --git a/.coveragerc b/.coveragerc index d23a4cc2ce4..2afa01bbefa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -379,6 +379,7 @@ omit = homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py homeassistant/components/thingspeak.py + homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/picotts.py homeassistant/components/upnp.py homeassistant/components/weather/bom.py diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index f9427d4f9d6..2a498198e3c 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv DOMAIN = 'ffmpeg' -REQUIREMENTS = ["ha-ffmpeg==1.1"] +REQUIREMENTS = ["ha-ffmpeg==1.2"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index c338c6dffd8..852ce522559 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -182,7 +182,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - if 'power' in self._status and self._status['power'] == '0': + if 'power' in self._status and self._status['power'] == 0: return STATE_OFF if 'mode' in self._status: if self._status['mode'] == 'pause': @@ -213,8 +213,16 @@ class SqueezeBoxDevice(MediaPlayerDevice): "status", "-", "1", "tags:{tags}" .format(tags=tags)) + if response is False: + return + + self._status = response.copy() + + try: + self._status.update(response["playlist_loop"][0]) + except KeyError: + pass try: - self._status = response.copy() self._status.update(response["remoteMeta"]) except KeyError: pass diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index d18da5ae2f0..7bb83db7f82 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -17,7 +17,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["boto3==1.3.1"] +REQUIREMENTS = ["boto3==1.4.3"] CONF_REGION = 'region_name' CONF_ACCESS_KEY_ID = 'aws_access_key_id' diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py index f02b6b75a84..9b95c486b4d 100644 --- a/homeassistant/components/notify/aws_sns.py +++ b/homeassistant/components/notify/aws_sns.py @@ -17,7 +17,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["boto3==1.3.1"] +REQUIREMENTS = ["boto3==1.4.3"] CONF_REGION = 'region_name' CONF_ACCESS_KEY_ID = 'aws_access_key_id' diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py index ecbadac46ce..76a137734d3 100644 --- a/homeassistant/components/notify/aws_sqs.py +++ b/homeassistant/components/notify/aws_sqs.py @@ -16,7 +16,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["boto3==1.3.1"] +REQUIREMENTS = ["boto3==1.4.3"] CONF_REGION = 'region_name' CONF_ACCESS_KEY_ID = 'aws_access_key_id' diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index eda0fa27f53..9b4df2749c0 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -54,7 +54,7 @@ ATTR_LANGUAGE = 'language' ATTR_OPTIONS = 'options' _RE_VOICE_FILE = re.compile( - r"([a-f0-9]{40})_([^_]+)_([^_]+)_([a-z]+)\.[a-z0-9]{3,4}") + r"([a-f0-9]{40})_([^_]+)_([^_]+)_([a-z_]+)\.[a-z0-9]{3,4}") KEY_PATTERN = '{0}_{1}_{2}_{3}' PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py new file mode 100644 index 00000000000..fe63d72b632 --- /dev/null +++ b/homeassistant/components/tts/amazon_polly.py @@ -0,0 +1,180 @@ +""" +Support for the Amazon Polly text to speech service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tts/amazon_polly/ +""" +import logging +import voluptuous as vol + +from homeassistant.components.tts import Provider, PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ["boto3==1.4.3"] + +CONF_REGION = "region_name" +CONF_ACCESS_KEY_ID = "aws_access_key_id" +CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" +CONF_PROFILE_NAME = "profile_name" +ATTR_CREDENTIALS = "credentials" + +DEFAULT_REGION = "us-east-1" +SUPPORTED_REGIONS = ["us-east-1", "us-east-2", "us-west-2", "eu-west-1"] + +CONF_VOICE = "voice" +CONF_OUTPUT_FORMAT = "output_format" +CONF_SAMPLE_RATE = "sample_rate" +CONF_TEXT_TYPE = "text_type" + +SUPPORTED_VOICES = ["Geraint", "Gwyneth", "Mads", "Naja", "Hans", "Marlene", + "Nicole", "Russell", "Amy", "Brian", "Emma", "Raveena", + "Ivy", "Joanna", "Joey", "Justin", "Kendra", "Kimberly", + "Salli", "Conchita", "Enrique", "Miguel", "Penelope", + "Chantal", "Celine", "Mathieu", "Dora", "Karl", "Carla", + "Giorgio", "Mizuki", "Liv", "Lotte", "Ruben", "Ewa", + "Jacek", "Jan", "Maja", "Ricardo", "Vitoria", "Cristiano", + "Ines", "Carmen", "Maxim", "Tatyana", "Astrid", "Filiz"] + +SUPPORTED_OUTPUT_FORMATS = ["mp3", "ogg_vorbis", "pcm"] + +SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050"] + +SUPPORTED_SAMPLE_RATES_MAP = { + "mp3": ["8000", "16000", "22050"], + "ogg_vorbis": ["8000", "16000", "22050"], + "pcm": ["8000", "16000"] +} + +SUPPORTED_TEXT_TYPES = ["text", "ssml"] + +CONTENT_TYPE_EXTENSIONS = { + "audio/mpeg": "mp3", + "audio/ogg": "ogg", + "audio/pcm": "pcm" +} + +DEFAULT_VOICE = "Joanna" +DEFAULT_OUTPUT_FORMAT = "mp3" +DEFAULT_TEXT_TYPE = "text" + +DEFAULT_SAMPLE_RATES = { + "mp3": "22050", + "ogg_vorbis": "22050", + "pcm": "16000" +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_REGION, default=DEFAULT_REGION): + vol.In(SUPPORTED_REGIONS), + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), + vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): + vol.In(SUPPORTED_OUTPUT_FORMATS), + vol.Optional(CONF_SAMPLE_RATE): vol.All(cv.string, + vol.In(SUPPORTED_SAMPLE_RATES)), + vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): + vol.In(SUPPORTED_TEXT_TYPES), +}) + + +def get_engine(hass, config): + """Setup Amazon Polly speech component.""" + # pylint: disable=import-error + output_format = config.get(CONF_OUTPUT_FORMAT) + sample_rate = config.get(CONF_SAMPLE_RATE, + DEFAULT_SAMPLE_RATES[output_format]) + if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP.get(output_format): + _LOGGER.error("%s is not a valid sample rate for %s", + sample_rate, output_format) + return None + + config[CONF_SAMPLE_RATE] = sample_rate + + import boto3 + + profile = config.get(CONF_PROFILE_NAME) + + if profile is not None: + boto3.setup_default_session(profile_name=profile) + + aws_config = { + CONF_REGION: config.get(CONF_REGION), + CONF_ACCESS_KEY_ID: config.get(CONF_ACCESS_KEY_ID), + CONF_SECRET_ACCESS_KEY: config.get(CONF_SECRET_ACCESS_KEY), + } + + del config[CONF_REGION] + del config[CONF_ACCESS_KEY_ID] + del config[CONF_SECRET_ACCESS_KEY] + + polly_client = boto3.client("polly", **aws_config) + + supported_languages = [] + + all_voices = {} + + all_voices_req = polly_client.describe_voices() + + for voice in all_voices_req.get("Voices"): + all_voices[voice.get("Id")] = voice + if voice.get("LanguageCode") not in supported_languages: + supported_languages.append(voice.get("LanguageCode")) + + return AmazonPollyProvider(polly_client, config, supported_languages, + all_voices) + + +class AmazonPollyProvider(Provider): + """Amazon Polly speech api provider.""" + + def __init__(self, polly_client, config, supported_languages, + all_voices): + """Initialize Amazon Polly provider for TTS.""" + self.client = polly_client + self.config = config + self.supported_langs = supported_languages + self.all_voices = all_voices + self.default_voice = self.config.get(CONF_VOICE) + + @property + def supported_languages(self): + """List of supported languages.""" + return self.supported_langs + + @property + def default_language(self): + """Default language.""" + return self.all_voices.get(self.default_voice).get("LanguageCode") + + @property + def default_options(self): + """Dict include default options.""" + return {CONF_VOICE: self.default_voice} + + @property + def supported_options(self): + """List of supported options.""" + return [CONF_VOICE] + + def get_tts_audio(self, message, language=None, options=None): + """Request TTS file from Polly.""" + voice_id = options.get(CONF_VOICE, self.default_voice) + voice_in_dict = self.all_voices.get(voice_id) + if language is not voice_in_dict.get("LanguageCode"): + _LOGGER.error("%s does not support the %s language", + voice_id, language) + return (None, None) + + resp = self.client.synthesize_speech( + OutputFormat=self.config[CONF_OUTPUT_FORMAT], + SampleRate=self.config[CONF_SAMPLE_RATE], + Text=message, + TextType=self.config[CONF_TEXT_TYPE], + VoiceId=voice_id + ) + + return (CONTENT_TYPE_EXTENSIONS[resp.get("ContentType")], + resp.get("AudioStream").read()) diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml index b44ef6ac66c..7e69d4939f0 100644 --- a/homeassistant/components/tts/services.yaml +++ b/homeassistant/components/tts/services.yaml @@ -16,7 +16,11 @@ say: language: description: Language to use for speech generation. - example: 'ru' + example: 'ru' + + options: + description: A dictionary containing platform-specific options. Optional depending on the platform. + example: platform specific clear_cache: description: Remove cache files and RAM cache. diff --git a/requirements_all.txt b/requirements_all.txt index c52282662d0..de4a2cbeab3 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -72,7 +72,8 @@ blockchain==1.3.3 # homeassistant.components.notify.aws_lambda # homeassistant.components.notify.aws_sns # homeassistant.components.notify.aws_sqs -boto3==1.3.1 +# homeassistant.components.tts.amazon_polly +boto3==1.4.3 # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink @@ -174,7 +175,7 @@ googlemaps==2.4.4 gps3==0.33.3 # homeassistant.components.ffmpeg -ha-ffmpeg==1.1 +ha-ffmpeg==1.2 # homeassistant.components.media_player.philips_js ha-philipsjs==0.0.1