Merge pull request #43964 from home-assistant/0.118.5

This commit is contained in:
Franck Nijhof 2020-12-05 13:07:27 +01:00 committed by GitHub
commit 7130b7a4b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 187 additions and 318 deletions

View File

@ -48,7 +48,7 @@ jobs:
run: |
python -m venv venv
. venv/bin/activate
pip install -U pip setuptools
pip install -U "pip<20.3" setuptools
pip install -r requirements.txt -r requirements_test.txt
- name: Restore pre-commit environment from cache
id: cache-precommit
@ -611,7 +611,7 @@ jobs:
run: |
python -m venv venv
. venv/bin/activate
pip install -U pip setuptools wheel
pip install -U "pip<20.3" setuptools wheel
pip install -r requirements_all.txt
pip install -r requirements_test.txt
pip install -e .

View File

@ -2,6 +2,6 @@
"domain": "google_translate",
"name": "Google Translate Text-to-Speech",
"documentation": "https://www.home-assistant.io/integrations/google_translate",
"requirements": ["gTTS-token==1.1.4"],
"requirements": ["gTTS==2.2.1"],
"codeowners": []
}

View File

@ -1,78 +1,95 @@
"""Support for the Google speech service."""
import asyncio
from io import BytesIO
import logging
import re
import aiohttp
from aiohttp.hdrs import REFERER, USER_AGENT
import async_timeout
from gtts_token import gtts_token
from gtts import gTTS, gTTSError
import voluptuous as vol
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
from homeassistant.const import HTTP_OK
from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__)
GOOGLE_SPEECH_URL = "https://translate.google.com/translate_tts"
MESSAGE_SIZE = 148
SUPPORT_LANGUAGES = [
"af",
"sq",
"ar",
"hy",
"bn",
"bs",
"ca",
"zh",
"zh-cn",
"zh-tw",
"zh-yue",
"hr",
"cs",
"cy",
"da",
"nl",
"en",
"en-au",
"en-uk",
"en-us",
"eo",
"fi",
"fr",
"de",
"el",
"en",
"eo",
"es",
"et",
"fi",
"fr",
"gu",
"hi",
"hr",
"hu",
"is",
"hy",
"id",
"is",
"it",
"ja",
"jw",
"km",
"kn",
"ko",
"la",
"lv",
"mk",
"ml",
"mr",
"my",
"ne",
"nl",
"no",
"pl",
"pt",
"pt-br",
"ro",
"ru",
"sr",
"si",
"sk",
"es",
"es-es",
"es-mx",
"es-us",
"sw",
"sq",
"sr",
"su",
"sv",
"sw",
"ta",
"te",
"th",
"tl",
"tr",
"vi",
"cy",
"uk",
"bg-BG",
"ur",
"vi",
# dialects
"zh-CN",
"zh-cn",
"zh-tw",
"en-us",
"en-ca",
"en-uk",
"en-gb",
"en-au",
"en-gh",
"en-in",
"en-ie",
"en-nz",
"en-ng",
"en-ph",
"en-za",
"en-tz",
"fr-ca",
"fr-fr",
"pt-br",
"pt-pt",
"es-es",
"es-us",
]
DEFAULT_LANG = "en"
@ -94,14 +111,6 @@ class GoogleProvider(Provider):
"""Init Google TTS service."""
self.hass = hass
self._lang = lang
self.headers = {
REFERER: "http://translate.google.com/",
USER_AGENT: (
"Mozilla/5.0 (Windows NT 10.0; WOW64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/47.0.2526.106 Safari/537.36"
),
}
self.name = "Google"
@property
@ -114,74 +123,15 @@ class GoogleProvider(Provider):
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
async def async_get_tts_audio(self, message, language, options=None):
def get_tts_audio(self, message, language, options=None):
"""Load TTS from google."""
token = gtts_token.Token()
websession = async_get_clientsession(self.hass)
message_parts = self._split_message_to_parts(message)
data = b""
for idx, part in enumerate(message_parts):
try:
part_token = await self.hass.async_add_executor_job(
token.calculate_token, part
)
except ValueError as err:
# If token seed fetching fails.
_LOGGER.warning(err)
return None, None
url_param = {
"ie": "UTF-8",
"tl": language,
"q": part,
"tk": part_token,
"total": len(message_parts),
"idx": idx,
"client": "tw-ob",
"textlen": len(part),
}
tts = gTTS(text=message, lang=language)
mp3_data = BytesIO()
try:
with async_timeout.timeout(10):
request = await websession.get(
GOOGLE_SPEECH_URL, params=url_param, headers=self.headers
)
if request.status != HTTP_OK:
_LOGGER.error(
"Error %d on load URL %s", request.status, request.url
)
return None, None
data += await request.read()
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout for google speech")
tts.write_to_fp(mp3_data)
except gTTSError as exc:
_LOGGER.exception("Error during processing of TTS request %s", exc)
return None, None
return "mp3", data
@staticmethod
def _split_message_to_parts(message):
"""Split message into single parts."""
if len(message) <= MESSAGE_SIZE:
return [message]
punc = "!()[]?.,;:"
punc_list = [re.escape(c) for c in punc]
pattern = "|".join(punc_list)
parts = re.split(pattern, message)
def split_by_space(fullstring):
"""Split a string by space."""
if len(fullstring) > MESSAGE_SIZE:
idx = fullstring.rfind(" ", 0, MESSAGE_SIZE)
return [fullstring[:idx]] + split_by_space(fullstring[idx:])
return [fullstring]
msg_parts = []
for part in parts:
msg_parts += split_by_space(part)
return [msg for msg in msg_parts if len(msg) > 0]
return "mp3", mp3_data.getvalue()

View File

@ -77,6 +77,7 @@ class HassIOView(HomeAssistantView):
This method is a coroutine.
"""
read_timeout = _get_timeout(path)
client_timeout = 10
data = None
headers = _init_header(request)
if path == "snapshots/new/upload":
@ -89,9 +90,10 @@ class HassIOView(HomeAssistantView):
request._client_max_size = ( # pylint: disable=protected-access
MAX_UPLOAD_SIZE
)
client_timeout = 300
try:
with async_timeout.timeout(10):
with async_timeout.timeout(client_timeout):
data = await request.read()
method = getattr(self._websession, request.method.lower())

View File

@ -3,7 +3,7 @@
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [
"pyatmo==4.2.0"
"pyatmo==4.2.1"
],
"after_dependencies": [
"cloud",

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 118
PATCH_VERSION = "4"
PATCH_VERSION = "5"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1)

View File

@ -20,7 +20,7 @@ jinja2>=2.11.2
netdisco==2.8.2
paho-mqtt==1.5.1
pillow==7.2.0
pip>=8.0.3
pip>=8.0.3,<20.3
python-slugify==4.0.1
pytz>=2020.1
pyyaml==5.3.1

View File

@ -13,7 +13,7 @@ importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.2
PyJWT==1.7.1
cryptography==3.2
pip>=8.0.3
pip>=8.0.3,<20.3
python-slugify==4.0.1
pytz>=2020.1
pyyaml==5.3.1

View File

@ -625,7 +625,7 @@ freesms==0.1.2
fritzconnection==1.3.4
# homeassistant.components.google_translate
gTTS-token==1.1.4
gTTS==2.2.1
# homeassistant.components.garmin_connect
garminconnect==0.1.16
@ -1274,7 +1274,7 @@ pyarlo==0.2.3
pyatag==0.3.4.4
# homeassistant.components.netatmo
pyatmo==4.2.0
pyatmo==4.2.1
# homeassistant.components.atome
pyatome==0.1.1

View File

@ -308,7 +308,7 @@ fnvhash==0.1.0
foobot_async==0.3.2
# homeassistant.components.google_translate
gTTS-token==1.1.4
gTTS==2.2.1
# homeassistant.components.garmin_connect
garminconnect==0.1.16
@ -640,7 +640,7 @@ pyarlo==0.2.3
pyatag==0.3.4.4
# homeassistant.components.netatmo
pyatmo==4.2.0
pyatmo==4.2.1
# homeassistant.components.blackbird
pyblackbird==0.5

View File

@ -45,7 +45,7 @@ REQUIRES = [
"PyJWT==1.7.1",
# PyJWT has loose dependency. We want the latest one.
"cryptography==3.2",
"pip>=8.0.3",
"pip>=8.0.3,<20.3",
"python-slugify==4.0.1",
"pytz>=2020.1",
"pyyaml==5.3.1",

View File

@ -1,8 +1,10 @@
"""The tests for the Google speech platform."""
import asyncio
import os
import shutil
from gtts import gTTSError
import pytest
from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID,
DOMAIN as DOMAIN_MP,
@ -10,226 +12,141 @@ from homeassistant.components.media_player.const import (
)
import homeassistant.components.tts as tts
from homeassistant.config import async_process_ha_core_config
from homeassistant.setup import setup_component
from homeassistant.setup import async_setup_component
from tests.async_mock import patch
from tests.common import assert_setup_component, get_test_home_assistant, mock_service
from tests.common import async_mock_service
from tests.components.tts.test_init import mutagen_mock # noqa: F401
class TestTTSGooglePlatform:
"""Test the Google speech component."""
def setup_method(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
asyncio.run_coroutine_threadsafe(
async_process_ha_core_config(
self.hass, {"internal_url": "http://example.local:8123"}
),
self.hass.loop,
)
self.url = "https://translate.google.com/translate_tts"
self.url_param = {
"tl": "en",
"q": "90%25%20of%20I%20person%20is%20on%20front%20of%20your%20door.",
"tk": 5,
"client": "tw-ob",
"textlen": 41,
"total": 1,
"idx": 0,
"ie": "UTF-8",
}
def teardown_method(self):
"""Stop everything that was started."""
default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR)
@pytest.fixture(autouse=True)
def cleanup_cache(hass):
"""Clean up TTS cache."""
yield
default_tts = 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": "google_translate"}}
@pytest.fixture
async def calls(hass):
"""Mock media player calls."""
return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
@patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5)
def test_service_say(self, mock_calculate, aioclient_mock):
@pytest.fixture(autouse=True)
async def setup_internal_url(hass):
"""Set up internal url."""
await async_process_ha_core_config(
hass, {"internal_url": "http://example.local:8123"}
)
@pytest.fixture
def mock_gtts():
"""Mock gtts."""
with patch("homeassistant.components.google_translate.tts.gTTS") as mock_gtts:
yield mock_gtts
async def test_service_say(hass, mock_gtts, calls):
"""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")
await async_setup_component(
hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}}
)
config = {tts.DOMAIN: {"platform": "google_translate"}}
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
await hass.services.async_call(
tts.DOMAIN,
"google_translate_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
blocking=True,
)
self.hass.block_till_done()
assert len(calls) == 1
assert len(aioclient_mock.mock_calls) == 1
assert len(mock_gtts.mock_calls) == 2
assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1
@patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5)
def test_service_say_german_config(self, mock_calculate, aioclient_mock):
assert mock_gtts.mock_calls[0][2] == {
"text": "There is a person at the front door.",
"lang": "en",
}
async def test_service_say_german_config(hass, mock_gtts, calls):
"""Test service call say with german code in the config."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
self.url_param["tl"] = "de"
aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test")
await async_setup_component(
hass,
tts.DOMAIN,
{tts.DOMAIN: {"platform": "google_translate", "language": "de"}},
)
config = {tts.DOMAIN: {"platform": "google_translate", "language": "de"}}
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
await hass.services.async_call(
tts.DOMAIN,
"google_translate_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
blocking=True,
)
self.hass.block_till_done()
assert len(calls) == 1
assert len(aioclient_mock.mock_calls) == 1
assert len(mock_gtts.mock_calls) == 2
assert mock_gtts.mock_calls[0][2] == {
"text": "There is a person at the front door.",
"lang": "de",
}
@patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5)
def test_service_say_german_service(self, mock_calculate, aioclient_mock):
async def test_service_say_german_service(hass, mock_gtts, calls):
"""Test service call say with german code in the service."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
self.url_param["tl"] = "de"
aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test")
config = {
tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"}
}
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
await async_setup_component(hass, tts.DOMAIN, config)
self.hass.services.call(
await hass.services.async_call(
tts.DOMAIN,
"google_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
tts.ATTR_MESSAGE: "There is a person at the front door.",
tts.ATTR_LANGUAGE: "de",
},
blocking=True,
)
self.hass.block_till_done()
assert len(calls) == 1
assert len(aioclient_mock.mock_calls) == 1
@patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5)
def test_service_say_error(self, mock_calculate, aioclient_mock):
"""Test service call say with http response 400."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.get(self.url, params=self.url_param, status=400, content=b"test")
config = {tts.DOMAIN: {"platform": "google_translate"}}
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
tts.DOMAIN,
"google_translate_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
},
)
self.hass.block_till_done()
assert len(calls) == 0
assert len(aioclient_mock.mock_calls) == 1
@patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5)
def test_service_say_timeout(self, mock_calculate, aioclient_mock):
"""Test service call say with http timeout."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.get(self.url, params=self.url_param, exc=asyncio.TimeoutError())
config = {tts.DOMAIN: {"platform": "google_translate"}}
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
tts.DOMAIN,
"google_translate_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: "90% of I person is on front of your door.",
},
)
self.hass.block_till_done()
assert len(calls) == 0
assert len(aioclient_mock.mock_calls) == 1
@patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5)
def test_service_say_long_size(self, mock_calculate, aioclient_mock):
"""Test service call say with a lot of text."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
self.url_param["total"] = 9
self.url_param["q"] = "I%20person%20is%20on%20front%20of%20your%20door"
self.url_param["textlen"] = 33
for idx in range(9):
self.url_param["idx"] = idx
aioclient_mock.get(
self.url, params=self.url_param, status=200, content=b"test"
)
config = {
tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"}
assert len(mock_gtts.mock_calls) == 2
assert mock_gtts.mock_calls[0][2] == {
"text": "There is a person at the front door.",
"lang": "de",
}
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(
async def test_service_say_error(hass, mock_gtts, calls):
"""Test service call say with http response 400."""
mock_gtts.return_value.write_to_fp.side_effect = gTTSError
await async_setup_component(
hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}}
)
await hass.services.async_call(
tts.DOMAIN,
"google_say",
"google_translate_say",
{
"entity_id": "media_player.something",
tts.ATTR_MESSAGE: (
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
"I person is on front of your door."
),
tts.ATTR_MESSAGE: "There is a person at the front door.",
},
blocking=True,
)
self.hass.block_till_done()
assert len(calls) == 1
assert len(aioclient_mock.mock_calls) == 9
assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1
assert len(calls) == 0
assert len(mock_gtts.mock_calls) == 2