Initial release v0.1.0

Home Assistant custom integration for NVIDIA Shield TV that composes
the Android TV Remote, Jellyfin, smart plug, and receiver entities
into a single unified media player with ADB-based now-playing from
any app. Part of the Media Zone ecosystem.

Entities:
- media_player: power (with smart plug fallback), transport, input,
  volume routed to receiver, rich stream info overlay when Jellyfin
  is active
- sensor: DAG attributes and stream info
- button: Pair ADB

Config:
- Two-step flow: basic setup + optional ADB pairing on the Shield
- Configurable ADB port (default 5555)
- Pick an existing Jellyfin media_player entity — stream info is read
  from the HA Jellyfin integration's own DataUpdateCoordinator, so
  no server URL / API key / extra config needed here
- Privacy mode (adb_store_app_info=False) suppresses non-Jellyfin
  media details, showing only the app name

Ecosystem:
- Exposes media_dag_* attributes for ha-media-zone orchestration
- Exposes media_dag_running_apps and async_bring_to_foreground for
  the Media Zone card's app switcher

Shared ADB code lives in ltomes/ha-adb-common and is vendored at
custom_components/nvidia_shield/ha_adb_common/ for HACS installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
LeviandClaude Opus 4.7 committed 2026-04-17 17:04:30 -05:00
commit 0ad51879ae
24 files changed
+2538

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
"""Jellyfin session stream-info reader.
Piggybacks on HA's built-in Jellyfin integration: its coordinator polls
/Sessions every 10s and exposes the parsed result on
``config_entry.runtime_data.data``. We reach across to that cached dict
by walking from the user-selected Jellyfin media_player entity back to
its owning Jellyfin config entry. Zero additional config, zero extra
HTTP.
"""
import logging
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
_LOGGER = logging.getLogger(__name__)
def _parse_streams(item: dict) -> dict | None:
"""Extract friendly video/audio labels from a Jellyfin NowPlayingItem."""
streams = item.get("MediaStreams", [])
result: dict = {}
for stream in streams:
stype = stream.get("Type", "")
if stype == "Video" and "video" not in result:
width = stream.get("Width", 0)
height = stream.get("Height", 0)
codec = (stream.get("Codec") or "").upper()
display = stream.get("DisplayTitle", "")
if height >= 2160:
res = "4K"
elif height >= 1080:
res = "1080p"
elif height >= 720:
res = "720p"
elif height > 0:
res = f"{height}p"
else:
res = None
result["video"] = {
"resolution": res,
"width": width,
"height": height,
"codec": codec,
"display": display,
"hdr": "HDR" in display or "DV" in display or "Dolby Vision" in display,
}
elif stype == "Audio" and "audio" not in result:
codec = (stream.get("Codec") or "").upper()
layout = stream.get("ChannelLayout", "")
display = stream.get("DisplayTitle", "")
if "atmos" in display.lower():
audio_label = "Atmos"
elif "truehd" in codec.lower():
audio_label = "TrueHD"
elif "dts" in codec.lower():
audio_label = "DTS-HD" if "hd" in display.lower() else "DTS"
elif "eac3" in codec.lower() or "e-ac-3" in codec.lower():
audio_label = "DD+"
elif "ac3" in codec.lower():
audio_label = "DD"
elif "aac" in codec.lower():
audio_label = "AAC"
elif "flac" in codec.lower():
audio_label = "FLAC"
else:
audio_label = codec
result["audio"] = {
"codec": codec,
"channels": layout,
"label": audio_label,
"display": display,
}
return result or None
def _get_jellyfin_coordinator(hass: HomeAssistant, jellyfin_entity_id: str) -> Any:
"""Resolve the Jellyfin integration's data-update coordinator for the
entity the user picked. Returns the coordinator, or None if the
entity is not registered / its config entry has no runtime_data yet."""
if not jellyfin_entity_id:
return None
registry = er.async_get(hass)
entity_entry = registry.async_get(jellyfin_entity_id)
if not entity_entry or not entity_entry.config_entry_id:
return None
config_entry = hass.config_entries.async_get_entry(entity_entry.config_entry_id)
if not config_entry:
return None
return getattr(config_entry, "runtime_data", None)
async def fetch_session_media_info(
hass: HomeAssistant,
jellyfin_entity_id: str,
device_name: str,
) -> dict | None:
"""Find stream info for the session whose DeviceName matches ``device_name``.
Reads from the already-authenticated Jellyfin coordinator — no new
HTTP calls, no credentials in this integration's config.
"""
coordinator = _get_jellyfin_coordinator(hass, jellyfin_entity_id)
if coordinator is None:
return None
sessions = getattr(coordinator, "data", None) or {}
device_lower = device_name.lower()
for sess in sessions.values():
sess_device = (sess.get("DeviceName") or "").lower()
if device_lower not in sess_device:
continue
item = sess.get("NowPlayingItem")
if not item:
continue
return _parse_streams(item)
return None