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:
commit
0ad51879ae
24 files changed
+2538
No files matched your search
@@ -0,0 +1,122 @@
|
||||
"""Android media session parsing.
|
||||
|
||||
Parses `dumpsys media_session` output from ADB to extract now-playing
|
||||
information from any app using Android's MediaSession API.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MediaSessionInfo:
|
||||
"""Parsed media session data from Android."""
|
||||
|
||||
title: str | None = None
|
||||
artist: str | None = None
|
||||
album: str | None = None
|
||||
package: str | None = None
|
||||
playing: bool = False
|
||||
paused: bool = False
|
||||
|
||||
|
||||
def parse_all_media_sessions(dump: str) -> list[MediaSessionInfo]:
|
||||
"""Parse `dumpsys media_session` output for ALL active media sessions.
|
||||
|
||||
Returns a list of every active session that has metadata or a known
|
||||
playback state, sorted: playing first, then paused, then others.
|
||||
"""
|
||||
if not dump:
|
||||
return []
|
||||
|
||||
sessions: list[MediaSessionInfo] = []
|
||||
current_pkg: str | None = None
|
||||
in_active = False
|
||||
current_state = 0 # 0=unknown, 2=paused, 3=playing
|
||||
current_meta: MediaSessionInfo | None = None
|
||||
|
||||
for line in dump.split("\n"):
|
||||
stripped = line.strip()
|
||||
|
||||
if "package=" in stripped:
|
||||
pkg = stripped.split("package=", 1)
|
||||
if len(pkg) > 1:
|
||||
# New session block — save the previous one if valid
|
||||
if in_active and current_meta:
|
||||
current_meta.package = current_pkg
|
||||
sessions.append(current_meta)
|
||||
current_pkg = pkg[1].strip()
|
||||
in_active = False
|
||||
current_state = 0
|
||||
current_meta = None
|
||||
|
||||
if "active=true" in stripped:
|
||||
in_active = True
|
||||
|
||||
if in_active and "state=PlaybackState" in stripped:
|
||||
if "state=3" in stripped:
|
||||
current_state = 3
|
||||
elif "state=2" in stripped:
|
||||
current_state = 2
|
||||
|
||||
if in_active and "metadata: size=" in stripped and "description=" in stripped:
|
||||
desc_start = stripped.find("description=")
|
||||
if desc_start < 0:
|
||||
continue
|
||||
desc = stripped[desc_start + 12:]
|
||||
parts = [p.strip() for p in desc.split(", ")]
|
||||
|
||||
info = MediaSessionInfo()
|
||||
info.package = current_pkg
|
||||
info.playing = current_state == 3
|
||||
info.paused = current_state == 2
|
||||
|
||||
if len(parts) >= 1 and parts[0] not in ("null", ""):
|
||||
info.title = parts[0]
|
||||
if len(parts) >= 2 and parts[1] not in ("null", ""):
|
||||
info.artist = parts[1]
|
||||
if len(parts) >= 3 and parts[2] not in ("null", ""):
|
||||
info.album = parts[2]
|
||||
|
||||
if info.title:
|
||||
current_meta = info
|
||||
|
||||
# Don't forget the last session
|
||||
if in_active and current_meta:
|
||||
current_meta.package = current_pkg
|
||||
sessions.append(current_meta)
|
||||
|
||||
# Sort: playing first, then paused, then others
|
||||
def sort_key(s: MediaSessionInfo) -> int:
|
||||
if s.playing:
|
||||
return 0
|
||||
if s.paused:
|
||||
return 1
|
||||
return 2
|
||||
|
||||
sessions.sort(key=sort_key)
|
||||
return sessions
|
||||
|
||||
|
||||
def parse_media_session(
|
||||
dump: str, foreground_package: str | None = None
|
||||
) -> MediaSessionInfo | None:
|
||||
"""Parse `dumpsys media_session` output for the best active session.
|
||||
|
||||
Returns the best match from all active sessions:
|
||||
1. The session whose package matches the foreground app (if provided)
|
||||
2. The session that is currently playing (state=3)
|
||||
3. The session that is paused (state=2)
|
||||
4. Any active session with metadata
|
||||
"""
|
||||
sessions = parse_all_media_sessions(dump)
|
||||
if not sessions:
|
||||
return None
|
||||
|
||||
# Prefer the session matching the foreground app
|
||||
if foreground_package:
|
||||
for s in sessions:
|
||||
if s.package == foreground_package:
|
||||
return s
|
||||
|
||||
# Already sorted: playing > paused > other
|
||||
return sessions[0]
|
||||
Reference in new issue
Block a user