test(ha_adb_common): fixture-driven foreground-parser tests — TiviMate capture, R4 launcher, wrap join, topResumed fallback ordering, duplicates tolerated (P3-2a)
This commit is contained in:
1 parent
8885ddd5b5
commit
bb2f787fcd
3 files changed
+266
-6
No files matched your search
@@ -167,10 +167,13 @@ class ShieldConnectionManager:
|
||||
protocol: Any = None,
|
||||
protocol_settings: dict | None = None,
|
||||
invalid_auth_callback: Callable[[], None] | None = None,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
):
|
||||
self.hass = hass
|
||||
self.entry = entry
|
||||
self.store = store if store is not None else StateStore(ttl=STATE_STORE_TTL)
|
||||
self.store = (
|
||||
store if store is not None else StateStore(ttl=STATE_STORE_TTL, clock=clock)
|
||||
)
|
||||
self._poller_factory = poller_factory
|
||||
self._poller = poller
|
||||
# Remote-protocol transport (androidtvremote2): entry passes either a
|
||||
@@ -182,6 +185,13 @@ class ShieldConnectionManager:
|
||||
self._invalid_auth_callback = invalid_auth_callback
|
||||
self._protocol_loop: asyncio.AbstractEventLoop | None = None
|
||||
self._listeners: list[Callable[[], None]] = []
|
||||
# P3-2d last-known-state cache: package → (session info, stamp).
|
||||
# Serves the foreground app's last known playback state when it
|
||||
# publishes no MediaSession (TiviMate, R2). TTL-capped: 'playing'
|
||||
# is NEVER resurrected past STATE_STORE_TTL.
|
||||
self._last_known: dict[str, tuple[MediaSessionInfo, float]] = {}
|
||||
# Injectable clock keeps the TTL expiry tests deterministic.
|
||||
self._clock = clock
|
||||
self._tasks: list[asyncio.Task] = []
|
||||
self._stopping = False
|
||||
self._failures = 0
|
||||
@@ -397,15 +407,58 @@ class ShieldConnectionManager:
|
||||
def best_session(
|
||||
self, foreground_package: str | None = None
|
||||
) -> MediaSessionInfo | None:
|
||||
"""Best media session: foreground app first, then playing > paused."""
|
||||
"""Best media session: foreground app first, then playing > paused.
|
||||
|
||||
P3-2d: when there is no live session at all, the foreground app's
|
||||
last known state is served from the TTL-capped cache (TiviMate
|
||||
publishes no MediaSession — RESEARCH_SHIELD R2).
|
||||
"""
|
||||
sessions = self.active_sessions
|
||||
if not sessions:
|
||||
return None
|
||||
if foreground_package:
|
||||
for session in sessions:
|
||||
if session.package == foreground_package:
|
||||
return session
|
||||
return sessions[0]
|
||||
if sessions:
|
||||
return sessions[0]
|
||||
if foreground_package:
|
||||
return self.last_known_session(foreground_package)
|
||||
return None
|
||||
|
||||
def _cache_last_known_sessions(self) -> None:
|
||||
"""Stamp every live session's state as the package's last known."""
|
||||
now = self._clock()
|
||||
for session in self.active_sessions:
|
||||
if session.package:
|
||||
self._last_known[session.package] = (session, now)
|
||||
|
||||
def last_known_session(self, package: str) -> MediaSessionInfo | None:
|
||||
"""Last known session state for a package, TTL-capped.
|
||||
|
||||
Returns None once STATE_STORE_TTL passes — 'playing' must never be
|
||||
resurrected from a stale cache (P3-2d expiry contract).
|
||||
"""
|
||||
cached = self._last_known.get(package)
|
||||
if cached is None:
|
||||
return None
|
||||
info, stamp = cached
|
||||
if self._clock() - stamp > STATE_STORE_TTL:
|
||||
self._last_known.pop(package, None)
|
||||
return None
|
||||
return info
|
||||
|
||||
def last_known_state(
|
||||
self, package: str
|
||||
) -> tuple[MediaSessionInfo | None, float | None]:
|
||||
"""(last known session, freshness stamp) for a package — the stamp
|
||||
lets callers age the served state in their UI."""
|
||||
cached = self._last_known.get(package)
|
||||
if cached is None:
|
||||
return None, None
|
||||
info, stamp = cached
|
||||
if self._clock() - stamp > STATE_STORE_TTL:
|
||||
self._last_known.pop(package, None)
|
||||
return None, None
|
||||
return info, stamp
|
||||
|
||||
# ── Listener API (entities are thin callback-views) ──
|
||||
|
||||
@@ -506,6 +559,7 @@ class ShieldConnectionManager:
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
self._listeners.clear()
|
||||
self._last_known.clear()
|
||||
if self._poller is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self._poller.disconnect()
|
||||
@@ -615,6 +669,7 @@ class ShieldConnectionManager:
|
||||
# Full dump, no truncation (B4) — low sessions must not be lost.
|
||||
await poller.poll_media_sessions(head_lines=None)
|
||||
self.store.set(FIELD_SESSIONS, list(poller.all_sessions))
|
||||
self._cache_last_known_sessions()
|
||||
# Zero-cost hint from the same dump; system processes filtered
|
||||
# (mediashell/systemui/tv settings/diagtools report "audio").
|
||||
playback = [
|
||||
@@ -631,7 +686,6 @@ class ShieldConnectionManager:
|
||||
foreground = await poller.poll_foreground()
|
||||
if foreground:
|
||||
self.store.set(FIELD_FOREGROUND, foreground)
|
||||
self._cache_last_known(foreground)
|
||||
else:
|
||||
# Standby: playback data is not current — expire it.
|
||||
self.store.set(FIELD_SESSIONS, [])
|
||||
|
||||
Reference in new issue
Block a user