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)

This commit is contained in:
James Mackay committed 2026-10-08 10:28:56 +01:00
1 parent d35b61078d
commit 34d09e9f70
3 files changed
+59 -6

No files matched your search

@@ -11,7 +11,7 @@
"issue_tracker": "https://git.notexpectedyet.com/notexpectedyet/ha-nvidia-shield/issues", "issue_tracker": "https://git.notexpectedyet.com/notexpectedyet/ha-nvidia-shield/issues",
"requirements": [ "requirements": [
"adb-shell>=0.4.0", "adb-shell>=0.4.0",
"androidtvremote2>=0.1.0" "androidtvremote2>=0.2.0"
], ],
"version": "2026.10.08" "version": "2026.10.08"
} }
@@ -110,7 +110,7 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity):
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
self.hass = hass self.hass = hass
self._entry = entry 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._attr_unique_id = f"{DOMAIN}_{entry.entry_id}_media_player"
self._unsub_listeners: list = [] self._unsub_listeners: list = []
@@ -489,13 +489,28 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity):
@property @property
def app_id(self): def app_id(self):
atv = self._read(self._androidtv_entity) """Android TV entity first; standalone setups fall back to the
return atv.attributes.get("app_id") if atv else None 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 @property
def app_name(self): def app_name(self):
atv = self._read(self._androidtv_entity) atv = self._read(self._androidtv_entity) if self._androidtv_entity else None
return atv.attributes.get("app_name") if atv 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 @property
def media_content_type(self): def media_content_type(self):
+38
View File
@@ -342,6 +342,44 @@ class TestRemoteEntity:
assert protocol.key_calls == [] 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: class TestVolumeAndPowerLadder:
async def test_volume_readout_from_protocol_push(self, modules): async def test_volume_readout_from_protocol_push(self, modules):
player, served, _intents = make_player(modules) player, served, _intents = make_player(modules)