mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Warn when casting of tts fails (#38603)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
ee043d8614
commit
91ba8c0ef0
@ -4,7 +4,7 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||||
"requirements": ["pychromecast==7.2.1"],
|
"requirements": ["pychromecast==7.2.1"],
|
||||||
"after_dependencies": ["cloud","zeroconf"],
|
"after_dependencies": ["cloud","tts","zeroconf"],
|
||||||
"zeroconf": ["_googlecast._tcp.local."],
|
"zeroconf": ["_googlecast._tcp.local."],
|
||||||
"codeowners": ["@emontnemery"]
|
"codeowners": ["@emontnemery"]
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ from homeassistant.core import callback
|
|||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.logging import async_create_catching_coro
|
from homeassistant.util.logging import async_create_catching_coro
|
||||||
@ -339,6 +340,48 @@ class CastDevice(MediaPlayerEntity):
|
|||||||
|
|
||||||
def new_media_status(self, media_status):
|
def new_media_status(self, media_status):
|
||||||
"""Handle updates of the media status."""
|
"""Handle updates of the media status."""
|
||||||
|
if (
|
||||||
|
media_status
|
||||||
|
and media_status.player_is_idle
|
||||||
|
and media_status.idle_reason == "ERROR"
|
||||||
|
):
|
||||||
|
external_url = None
|
||||||
|
internal_url = None
|
||||||
|
tts_base_url = None
|
||||||
|
url_description = ""
|
||||||
|
if "tts" in self.hass.config.components:
|
||||||
|
try:
|
||||||
|
tts_base_url = self.hass.components.tts.get_base_url(self.hass)
|
||||||
|
except KeyError:
|
||||||
|
# base_url not configured, ignore
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
external_url = get_url(self.hass, allow_internal=False)
|
||||||
|
except NoURLAvailableError:
|
||||||
|
# external_url not configured, ignore
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
internal_url = get_url(self.hass, allow_external=False)
|
||||||
|
except NoURLAvailableError:
|
||||||
|
# internal_url not configured, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
if media_status.content_id:
|
||||||
|
if tts_base_url and media_status.content_id.startswith(tts_base_url):
|
||||||
|
url_description = f" from tts.base_url ({tts_base_url})"
|
||||||
|
if external_url and media_status.content_id.startswith(external_url):
|
||||||
|
url_description = " from external_url ({external_url})"
|
||||||
|
if internal_url and media_status.content_id.startswith(internal_url):
|
||||||
|
url_description = " from internal_url ({internal_url})"
|
||||||
|
|
||||||
|
_LOGGER.error(
|
||||||
|
"Failed to cast media %s%s. Please make sure the URL is: "
|
||||||
|
"Reachable from the cast device and either a publicly resolvable "
|
||||||
|
"hostname or an IP address.",
|
||||||
|
media_status.content_id,
|
||||||
|
url_description,
|
||||||
|
)
|
||||||
|
|
||||||
self.media_status = media_status
|
self.media_status = media_status
|
||||||
self.media_status_received = dt_util.utcnow()
|
self.media_status_received = dt_util.utcnow()
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
@ -387,7 +430,7 @@ class CastDevice(MediaPlayerEntity):
|
|||||||
# ========== Service Calls ==========
|
# ========== Service Calls ==========
|
||||||
def _media_controller(self):
|
def _media_controller(self):
|
||||||
"""
|
"""
|
||||||
Return media status.
|
Return media controller.
|
||||||
|
|
||||||
First try from our own cast, then groups which our cast is a member in.
|
First try from our own cast, then groups which our cast is a member in.
|
||||||
"""
|
"""
|
||||||
|
@ -46,6 +46,8 @@ ATTR_MESSAGE = "message"
|
|||||||
ATTR_OPTIONS = "options"
|
ATTR_OPTIONS = "options"
|
||||||
ATTR_PLATFORM = "platform"
|
ATTR_PLATFORM = "platform"
|
||||||
|
|
||||||
|
BASE_URL_KEY = "tts_base_url"
|
||||||
|
|
||||||
CONF_BASE_URL = "base_url"
|
CONF_BASE_URL = "base_url"
|
||||||
CONF_CACHE = "cache"
|
CONF_CACHE = "cache"
|
||||||
CONF_CACHE_DIR = "cache_dir"
|
CONF_CACHE_DIR = "cache_dir"
|
||||||
@ -115,10 +117,11 @@ async def async_setup(hass, config):
|
|||||||
cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR)
|
cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR)
|
||||||
time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY)
|
time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY)
|
||||||
base_url = conf.get(CONF_BASE_URL) or get_url(hass)
|
base_url = conf.get(CONF_BASE_URL) or get_url(hass)
|
||||||
|
hass.data[BASE_URL_KEY] = base_url
|
||||||
|
|
||||||
await tts.async_init_cache(use_cache, cache_dir, time_memory, base_url)
|
await tts.async_init_cache(use_cache, cache_dir, time_memory, base_url)
|
||||||
except (HomeAssistantError, KeyError) as err:
|
except (HomeAssistantError, KeyError):
|
||||||
_LOGGER.error("Error on cache init %s", err)
|
_LOGGER.exception("Error on cache init")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
hass.http.register_view(TextToSpeechView(tts))
|
hass.http.register_view(TextToSpeechView(tts))
|
||||||
@ -592,3 +595,8 @@ class TextToSpeechView(HomeAssistantView):
|
|||||||
return web.Response(status=HTTP_NOT_FOUND)
|
return web.Response(status=HTTP_NOT_FOUND)
|
||||||
|
|
||||||
return web.Response(body=data, content_type=content)
|
return web.Response(body=data, content_type=content)
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_url(hass):
|
||||||
|
"""Get base URL."""
|
||||||
|
return hass.data[BASE_URL_KEY]
|
||||||
|
@ -6,15 +6,17 @@ from uuid import UUID
|
|||||||
import attr
|
import attr
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components import tts
|
||||||
from homeassistant.components.cast import media_player as cast
|
from homeassistant.components.cast import media_player as cast
|
||||||
from homeassistant.components.cast.media_player import ChromecastInfo
|
from homeassistant.components.cast.media_player import ChromecastInfo
|
||||||
|
from homeassistant.config import async_process_ha_core_config
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.async_mock import AsyncMock, MagicMock, Mock, patch
|
from tests.async_mock import AsyncMock, MagicMock, Mock, patch
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, assert_setup_component
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -577,6 +579,126 @@ async def test_group_media_control(hass: HomeAssistantType):
|
|||||||
assert chromecast.media_controller.play_media.called
|
assert chromecast.media_controller.play_media.called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_failed_cast_on_idle(hass, caplog):
|
||||||
|
"""Test no warning when unless player went idle with reason "ERROR"."""
|
||||||
|
info = get_fake_chromecast_info()
|
||||||
|
chromecast, entity = await async_setup_media_player_cast(hass, info)
|
||||||
|
|
||||||
|
media_status = MagicMock(images=None)
|
||||||
|
media_status.player_is_idle = False
|
||||||
|
media_status.idle_reason = "ERROR"
|
||||||
|
media_status.content_id = "http://example.com:8123/tts.mp3"
|
||||||
|
entity.new_media_status(media_status)
|
||||||
|
assert "Failed to cast media" not in caplog.text
|
||||||
|
|
||||||
|
media_status = MagicMock(images=None)
|
||||||
|
media_status.player_is_idle = True
|
||||||
|
media_status.idle_reason = "Other"
|
||||||
|
media_status.content_id = "http://example.com:8123/tts.mp3"
|
||||||
|
entity.new_media_status(media_status)
|
||||||
|
assert "Failed to cast media" not in caplog.text
|
||||||
|
|
||||||
|
media_status = MagicMock(images=None)
|
||||||
|
media_status.player_is_idle = True
|
||||||
|
media_status.idle_reason = "ERROR"
|
||||||
|
media_status.content_id = "http://example.com:8123/tts.mp3"
|
||||||
|
entity.new_media_status(media_status)
|
||||||
|
assert "Failed to cast media http://example.com:8123/tts.mp3." in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_failed_cast_other_url(hass, caplog):
|
||||||
|
"""Test warning when casting from internal_url fails."""
|
||||||
|
with assert_setup_component(1, tts.DOMAIN):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
tts.DOMAIN,
|
||||||
|
{tts.DOMAIN: {"platform": "demo", "base_url": "http://example.local:8123"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
info = get_fake_chromecast_info()
|
||||||
|
chromecast, entity = await async_setup_media_player_cast(hass, info)
|
||||||
|
|
||||||
|
media_status = MagicMock(images=None)
|
||||||
|
media_status.player_is_idle = True
|
||||||
|
media_status.idle_reason = "ERROR"
|
||||||
|
media_status.content_id = "http://example.com:8123/tts.mp3"
|
||||||
|
entity.new_media_status(media_status)
|
||||||
|
assert "Failed to cast media http://example.com:8123/tts.mp3." in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_failed_cast_internal_url(hass, caplog):
|
||||||
|
"""Test warning when casting from internal_url fails."""
|
||||||
|
await async_process_ha_core_config(
|
||||||
|
hass, {"internal_url": "http://example.local:8123"},
|
||||||
|
)
|
||||||
|
with assert_setup_component(1, tts.DOMAIN):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "demo"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
info = get_fake_chromecast_info()
|
||||||
|
chromecast, entity = await async_setup_media_player_cast(hass, info)
|
||||||
|
|
||||||
|
media_status = MagicMock(images=None)
|
||||||
|
media_status.player_is_idle = True
|
||||||
|
media_status.idle_reason = "ERROR"
|
||||||
|
media_status.content_id = "http://example.local:8123/tts.mp3"
|
||||||
|
entity.new_media_status(media_status)
|
||||||
|
assert (
|
||||||
|
"Failed to cast media http://example.local:8123/tts.mp3 from internal_url"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_failed_cast_external_url(hass, caplog):
|
||||||
|
"""Test warning when casting from external_url fails."""
|
||||||
|
await async_process_ha_core_config(
|
||||||
|
hass, {"external_url": "http://example.com:8123"},
|
||||||
|
)
|
||||||
|
with assert_setup_component(1, tts.DOMAIN):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
tts.DOMAIN,
|
||||||
|
{tts.DOMAIN: {"platform": "demo", "base_url": "http://example.com:8123"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
info = get_fake_chromecast_info()
|
||||||
|
chromecast, entity = await async_setup_media_player_cast(hass, info)
|
||||||
|
|
||||||
|
media_status = MagicMock(images=None)
|
||||||
|
media_status.player_is_idle = True
|
||||||
|
media_status.idle_reason = "ERROR"
|
||||||
|
media_status.content_id = "http://example.com:8123/tts.mp3"
|
||||||
|
entity.new_media_status(media_status)
|
||||||
|
assert (
|
||||||
|
"Failed to cast media http://example.com:8123/tts.mp3 from external_url"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_failed_cast_tts_base_url(hass, caplog):
|
||||||
|
"""Test warning when casting from tts.base_url fails."""
|
||||||
|
with assert_setup_component(1, tts.DOMAIN):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
tts.DOMAIN,
|
||||||
|
{tts.DOMAIN: {"platform": "demo", "base_url": "http://example.local:8123"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
info = get_fake_chromecast_info()
|
||||||
|
chromecast, entity = await async_setup_media_player_cast(hass, info)
|
||||||
|
|
||||||
|
media_status = MagicMock(images=None)
|
||||||
|
media_status.player_is_idle = True
|
||||||
|
media_status.idle_reason = "ERROR"
|
||||||
|
media_status.content_id = "http://example.local:8123/tts.mp3"
|
||||||
|
entity.new_media_status(media_status)
|
||||||
|
assert (
|
||||||
|
"Failed to cast media http://example.local:8123/tts.mp3 from tts.base_url"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_disconnect_on_stop(hass: HomeAssistantType):
|
async def test_disconnect_on_stop(hass: HomeAssistantType):
|
||||||
"""Test cast device disconnects socket on stop."""
|
"""Test cast device disconnects socket on stop."""
|
||||||
info = get_fake_chromecast_info()
|
info = get_fake_chromecast_info()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user