diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 49ade045518..d172a5d0663 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==7.2.1"], - "after_dependencies": ["cloud","zeroconf"], + "after_dependencies": ["cloud","tts","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 3b63dd91ca9..7cd4697558c 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -44,6 +44,7 @@ from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro @@ -339,6 +340,48 @@ class CastDevice(MediaPlayerEntity): def new_media_status(self, 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_received = dt_util.utcnow() self.schedule_update_ha_state() @@ -387,7 +430,7 @@ class CastDevice(MediaPlayerEntity): # ========== Service Calls ========== 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. """ diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 39e4702e855..948db0f5d46 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -46,6 +46,8 @@ ATTR_MESSAGE = "message" ATTR_OPTIONS = "options" ATTR_PLATFORM = "platform" +BASE_URL_KEY = "tts_base_url" + CONF_BASE_URL = "base_url" CONF_CACHE = "cache" 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) time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY) 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) - except (HomeAssistantError, KeyError) as err: - _LOGGER.error("Error on cache init %s", err) + except (HomeAssistantError, KeyError): + _LOGGER.exception("Error on cache init") return False hass.http.register_view(TextToSpeechView(tts)) @@ -592,3 +595,8 @@ class TextToSpeechView(HomeAssistantView): return web.Response(status=HTTP_NOT_FOUND) return web.Response(body=data, content_type=content) + + +def get_base_url(hass): + """Get base URL.""" + return hass.data[BASE_URL_KEY] diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index ad18430c37a..5d8a54dc691 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -6,15 +6,17 @@ from uuid import UUID import attr import pytest +from homeassistant.components import tts from homeassistant.components.cast import media_player as cast 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.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component 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) @@ -577,6 +579,126 @@ async def test_group_media_control(hass: HomeAssistantType): 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): """Test cast device disconnects socket on stop.""" info = get_fake_chromecast_info()