diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 39e4702e855..c7c0b986878 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -321,7 +321,9 @@ class SpeechManager: else: options_key = "-" - key = KEY_PATTERN.format(msg_hash, language, options_key, engine).lower() + key = KEY_PATTERN.format( + msg_hash, language.replace("_", "-"), options_key, engine + ).lower() # Is speech already in memory if key in self.mem_cache: @@ -352,9 +354,14 @@ class SpeechManager: # Create file infos filename = f"{key}.{extension}".lower() - data = self.write_tags(filename, data, provider, message, language, options) + # Validate filename + if not _RE_VOICE_FILE.match(filename): + raise HomeAssistantError( + f"TTS filename '{filename}' from {engine} is invalid!" + ) # Save to memory + data = self.write_tags(filename, data, provider, message, language, options) self._async_store_to_memcache(key, filename, data) if cache: diff --git a/homeassistant/const.py b/homeassistant/const.py index 938e108d58b..9dd74f6709c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 114 -PATCH_VERSION = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 3f0c2db3b2f..be002a1b204 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -620,13 +620,19 @@ def async_track_utc_time_change( calculate_next(now + timedelta(seconds=1)) + # We always get time.time() first to avoid time.time() + # ticking forward after fetching hass.loop.time() + # and callback being scheduled a few microseconds early cancel_callback = hass.loop.call_at( - hass.loop.time() + next_time.timestamp() - time.time(), + -time.time() + hass.loop.time() + next_time.timestamp(), pattern_time_change_listener, ) + # We always get time.time() first to avoid time.time() + # ticking forward after fetching hass.loop.time() + # and callback being scheduled a few microseconds early cancel_callback = hass.loop.call_at( - hass.loop.time() + next_time.timestamp() - time.time(), + -time.time() + hass.loop.time() + next_time.timestamp(), pattern_time_change_listener, ) diff --git a/tests/common.py b/tests/common.py index bcb66428f6b..16f349de800 100644 --- a/tests/common.py +++ b/tests/common.py @@ -295,8 +295,8 @@ def async_fire_time_changed(hass, datetime_, fire_all=False): if task.cancelled(): continue - future_seconds = task.when() - hass.loop.time() mock_seconds_into_future = datetime_.timestamp() - time.time() + future_seconds = task.when() - hass.loop.time() if fire_all or mock_seconds_into_future >= future_seconds: with patch( diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index b4cb9c67af3..a100ef22c65 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -176,6 +176,41 @@ async def test_setup_component_and_test_service_with_config_language( ).is_file() +async def test_setup_component_and_test_service_with_config_language_special( + hass, empty_cache_dir +): + """Set up the demo platform and call service with extend language.""" + import homeassistant.components.demo.tts as demo_tts + + demo_tts.SUPPORT_LANGUAGES.append("en_US") + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = {tts.DOMAIN: {"platform": "demo", "language": "en_US"}} + + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_demo.mp3" + ) + await hass.async_block_till_done() + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_demo.mp3" + ).is_file() + + async def test_setup_component_and_test_service_with_wrong_conf_language(hass): """Set up the demo platform and call service with wrong config.""" config = {tts.DOMAIN: {"platform": "demo", "language": "ru"}}