feat(connection,media_player): on-demand manifest-label cache (≥24h TTL, one pm dump per watchdog cycle via the task pool) + app_name order map > label > entity > raw package (P5-1)
This commit is contained in:
1 parent
80e9a691bc
commit
cd0e634a76
3 files changed
+80
-9
No files matched your search
@@ -36,10 +36,15 @@ from .const import (
|
||||
ADB_RECONNECT_BACKOFF_CAP,
|
||||
ADB_RECONNECT_BACKOFF_START,
|
||||
ADB_RECONNECT_JITTER,
|
||||
APP_LABEL_TTL,
|
||||
STATE_STORE_TTL,
|
||||
SYSTEM_MEDIA_PACKAGES,
|
||||
)
|
||||
from .ha_adb_common import MediaSessionInfo, parse_audio_playback
|
||||
from .ha_adb_common import (
|
||||
MediaSessionInfo,
|
||||
parse_application_label,
|
||||
parse_audio_playback,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -195,6 +200,11 @@ class ShieldConnectionManager:
|
||||
# publishes no MediaSession (TiviMate, R2). TTL-capped: 'playing'
|
||||
# is NEVER resurrected past STATE_STORE_TTL.
|
||||
self._last_known: dict[str, tuple[MediaSessionInfo, float]] = {}
|
||||
# P5-1: device manifest labels — package → (label, stamp). Long-TTL
|
||||
# (APP_LABEL_TTL) cache resolved via `pm dump` on demand, one
|
||||
# package per watchdog cycle so a burst never blocks the watchdog.
|
||||
self._app_labels: dict[str, tuple[str, float]] = {}
|
||||
self._app_label_requests: set[str] = set()
|
||||
# Injectable clock keeps the TTL expiry tests deterministic.
|
||||
self._clock = clock
|
||||
self._tasks: list[asyncio.Task] = []
|
||||
@@ -470,6 +480,51 @@ class ShieldConnectionManager:
|
||||
snapshot[name] = {"age": round(time.monotonic() - stamp, 3), "value": value}
|
||||
return snapshot
|
||||
|
||||
def app_label(self, package: str | None) -> str | None:
|
||||
"""Cached manifest label for a package (P5-1), or None.
|
||||
|
||||
A cache miss enqueues an on-demand `pm dump` fetch — drained one
|
||||
package per watchdog cycle via the manager task pool, so entity
|
||||
refreshes and the watchdog are never blocked by a burst. A failed
|
||||
resolution simply falls through to the next name tier.
|
||||
"""
|
||||
if not package:
|
||||
return None
|
||||
cached = self._app_labels.get(package)
|
||||
if cached is not None:
|
||||
label, stamp = cached
|
||||
if self._clock() - stamp <= APP_LABEL_TTL:
|
||||
return label
|
||||
self._app_labels.pop(package, None) # stale — refetch on demand
|
||||
self._app_label_requests.add(package)
|
||||
return None
|
||||
|
||||
async def _fetch_app_label(self, package: str) -> None:
|
||||
"""One `pm dump` label fetch; failure falls through silently."""
|
||||
poller = self._poller
|
||||
if poller is None or not self.connected:
|
||||
return
|
||||
try:
|
||||
dump = await poller.shell(
|
||||
f"pm dump {package} 2>/dev/null | head -300"
|
||||
)
|
||||
label = parse_application_label(dump or "")
|
||||
except Exception as err:
|
||||
_LOGGER.debug("app label fetch failed for %s: %s", package, err)
|
||||
return
|
||||
if label:
|
||||
self._app_labels[package] = (label, self._clock())
|
||||
|
||||
def _drain_app_label_requests(self) -> None:
|
||||
"""Start at most one label fetch per watchdog cycle."""
|
||||
if not self._app_label_requests or self._poller is None:
|
||||
return
|
||||
package = min(self._app_label_requests)
|
||||
self._app_label_requests.discard(package)
|
||||
self._tasks.append(
|
||||
self._create_task(self._fetch_app_label(package), f"app_label_{package}")
|
||||
)
|
||||
|
||||
def last_known_map(self) -> dict[str, MediaSessionInfo]:
|
||||
"""TTL-filtered snapshot of the per-package last-known cache."""
|
||||
result: dict[str, MediaSessionInfo] = {}
|
||||
@@ -593,6 +648,8 @@ class ShieldConnectionManager:
|
||||
task.cancel()
|
||||
self._listeners.clear()
|
||||
self._last_known.clear()
|
||||
self._app_labels.clear()
|
||||
self._app_label_requests.clear()
|
||||
if self._poller is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self._poller.disconnect()
|
||||
@@ -719,6 +776,8 @@ class ShieldConnectionManager:
|
||||
foreground = await poller.poll_foreground()
|
||||
if foreground:
|
||||
self.store.set(FIELD_FOREGROUND, foreground)
|
||||
# P5-1: manifest labels resolve on demand, one package per cycle
|
||||
self._drain_app_label_requests()
|
||||
else:
|
||||
# Standby: playback data is not current — expire it.
|
||||
self.store.set(FIELD_SESSIONS, [])
|
||||
|
||||
Reference in new issue
Block a user