From 34d09e9f70a5723e05bde394a500113f7dcaa053 Mon Sep 17 00:00:00 2001 From: James Mackay Date: Thu, 8 Oct 2026 10:28:56 +0100 Subject: [PATCH] feat(media_player): app_id/app_name prefer the androidtv entity, fall back to the protocol current_app push when absent/unavailable; manifest androidtvremote2 floor aligned to >=0.2.0 (P3-1b,c) --- custom_components/nvidia_shield/manifest.json | 2 +- .../nvidia_shield/media_player.py | 25 +++++++++--- tests/test_command_routing.py | 38 +++++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/custom_components/nvidia_shield/manifest.json b/custom_components/nvidia_shield/manifest.json index 2712306..3529ce3 100644 --- a/custom_components/nvidia_shield/manifest.json +++ b/custom_components/nvidia_shield/manifest.json @@ -11,7 +11,7 @@ "issue_tracker": "https://git.notexpectedyet.com/notexpectedyet/ha-nvidia-shield/issues", "requirements": [ "adb-shell>=0.4.0", - "androidtvremote2>=0.1.0" + "androidtvremote2>=0.2.0" ], "version": "2026.10.08" } diff --git a/custom_components/nvidia_shield/media_player.py b/custom_components/nvidia_shield/media_player.py index 6b0980d..af6be8a 100644 --- a/custom_components/nvidia_shield/media_player.py +++ b/custom_components/nvidia_shield/media_player.py @@ -110,7 +110,7 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity): def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.hass = hass self._entry = entry - self._androidtv_entity = entry.data[CONF_ANDROIDTV_ENTITY] + self._androidtv_entity = entry.data.get(CONF_ANDROIDTV_ENTITY, "") self._attr_unique_id = f"{DOMAIN}_{entry.entry_id}_media_player" self._unsub_listeners: list = [] @@ -489,13 +489,28 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity): @property def app_id(self): - atv = self._read(self._androidtv_entity) - return atv.attributes.get("app_id") if atv else None + """Android TV entity first; standalone setups fall back to the + protocol current-app push (raw package — both stamp the store).""" + atv = self._read(self._androidtv_entity) if self._androidtv_entity else None + if atv is not None: + app_id = atv.attributes.get("app_id") + if app_id: + return app_id + if self._manager is not None: + return self._manager.current_app + return None @property def app_name(self): - atv = self._read(self._androidtv_entity) - return atv.attributes.get("app_name") if atv else None + atv = self._read(self._androidtv_entity) if self._androidtv_entity else None + if atv is not None: + app_name = atv.attributes.get("app_name") + if app_name: + return app_name + if self._manager is not None: + # Name resolution is deferred app-map work; the package is truth. + return self._manager.current_app + return None @property def media_content_type(self): diff --git a/tests/test_command_routing.py b/tests/test_command_routing.py index 0a12c28..fa320fe 100644 --- a/tests/test_command_routing.py +++ b/tests/test_command_routing.py @@ -342,6 +342,44 @@ class TestRemoteEntity: assert protocol.key_calls == [] +class TestAppFallback: + async def test_app_id_prefers_the_androidtv_entity(self, modules): + player, _s, _i = make_player(modules) + player._androidtv_entity = "media_player.atv" + from types import SimpleNamespace as FakeState + + state = FakeState(attributes={"app_id": "com.plex.hdx", "app_name": "Plex"}) + states = FakeState(states=FakeState( + get=lambda eid: state if eid == "media_player.atv" else None + )) + player.hass = states + player._read = lambda eid: state if eid == "media_player.atv" else None + + assert player.app_id == "com.plex.hdx" + assert player.app_name == "Plex" + + async def test_app_falls_back_to_protocol_current_app(self, modules): + player, _s, _i = make_player(modules) + player._androidtv_entity = "" # standalone setup (P3-1) + player._manager._on_current_app("com.netflix.ninja") + await asyncio.sleep(0.05) # drain the call_soon_threadsafe push + + assert player.app_id == "com.netflix.ninja" + assert player.app_name == "com.netflix.ninja" # package is truth; map later + + async def test_app_unavailable_entity_falls_back(self, modules): + player, _s, _i = make_player(modules) + player._androidtv_entity = "media_player.atv" + from types import SimpleNamespace as FakeState + + player.hass = FakeState(states=FakeState(get=lambda eid: None)) # entity gone + player._read = lambda eid: None + player._manager._on_current_app("com.google.android.youtube.tv") + await asyncio.sleep(0.05) + + assert player.app_id == "com.google.android.youtube.tv" + + class TestVolumeAndPowerLadder: async def test_volume_readout_from_protocol_push(self, modules): player, served, _intents = make_player(modules)