From 98e2663822bdcec7ba9517474a754181b76ab04e Mon Sep 17 00:00:00 2001 From: Sean Cardus Date: Wed, 30 Sep 2026 11:45:32 +0100 Subject: [PATCH] Fix missing media player for a new console added at runtime (#182883) --- homeassistant/components/xbox/entity.py | 6 ++ homeassistant/components/xbox/media_player.py | 9 ++- tests/components/xbox/test_media_player.py | 55 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xbox/entity.py b/homeassistant/components/xbox/entity.py index fa1cd238fc13..3ae6f95f78b6 100644 --- a/homeassistant/components/xbox/entity.py +++ b/homeassistant/components/xbox/entity.py @@ -155,6 +155,12 @@ class XboxConsoleBaseEntity(CoordinatorEntity[XboxConsoleStatusCoordinator]): """Return if entity is available.""" return self.coordinator.data.get(self._console.id) is not None + @property + @override + def entity_picture(self) -> str | None: + """Return the entity picture.""" + return super().entity_picture if self.available else None + def to_https(image_url: str) -> str: """Convert image URLs to secure URLs.""" diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 8cd005e070c7..1cf6633b14fe 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -127,6 +127,7 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity): @override def state(self) -> MediaPlayerState | None: """State of the player.""" + status = self.data.status if status.playback_state in XBOX_STATE_MAP: return XBOX_STATE_MAP[status.playback_state] @@ -136,7 +137,10 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity): @override def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - if self.state not in [MediaPlayerState.PLAYING, MediaPlayerState.PAUSED]: + if not self.available or self.state not in [ + MediaPlayerState.PLAYING, + MediaPlayerState.PAUSED, + ]: return ( SUPPORT_XBOX & ~MediaPlayerEntityFeature.NEXT_TRACK @@ -181,7 +185,8 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity): return ( to_https(image.uri) - if (app_details := self.data.app_details) + if self.available + and (app_details := self.data.app_details) and (image := _find_media_image(app_details.localized_properties[0].images)) else None ) diff --git a/tests/components/xbox/test_media_player.py b/tests/components/xbox/test_media_player.py index c600a8ad14da..4de5b061222e 100644 --- a/tests/components/xbox/test_media_player.py +++ b/tests/components/xbox/test_media_player.py @@ -1,14 +1,17 @@ """Test the Xbox media_player platform.""" from collections.abc import Generator +from datetime import timedelta from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from httpx import HTTPStatusError, RequestError, TimeoutException import pytest from pythonxbox.api.provider.catalog.models import CatalogResponse from pythonxbox.api.provider.smartglass.models import ( CommandResponse, + SmartglassConsoleList, SmartglassConsoleStatus, VolumeDirection, ) @@ -35,6 +38,7 @@ from homeassistant.const import ( SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -45,6 +49,7 @@ from tests.common import ( AsyncMock, Mock, MockConfigEntry, + async_fire_time_changed, async_load_json_object_fixture, snapshot_platform, ) @@ -110,6 +115,56 @@ async def test_media_players( await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) +async def test_media_player_added_before_console_status( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + xbox_live_client: AsyncMock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a console that appears before it has been polled for status. + + The status coordinator only polls consoles an entity has registered for, so + a console added at runtime has no status data when its entity is created. + """ + + xbox_live_client.smartglass.get_console_list.return_value = SmartglassConsoleList( + **await async_load_json_object_fixture( + hass, "smartglass_console_list_empty.json", DOMAIN + ) # pyright: ignore[reportArgumentType] + ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + assert not config_entry.runtime_data.status.data + + xbox_live_client.smartglass.get_console_list.return_value = SmartglassConsoleList( + **await async_load_json_object_fixture( + hass, "smartglass_console_list.json", DOMAIN + ) # pyright: ignore[reportArgumentType] + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert "Error adding entity" not in caplog.text + assert entity_registry.async_get("media_player.xonex") is not None + assert (state := hass.states.get("media_player.xonex")) is not None + assert state.state == STATE_UNAVAILABLE + + freezer.tick(timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get("media_player.xonex")) is not None + assert state.state != STATE_UNAVAILABLE + + @pytest.mark.usefixtures("xbox_live_client") async def test_browse_media( hass: HomeAssistant,