From 61aca9d4135f40f59453430b23d3808a11ae2fb0 Mon Sep 17 00:00:00 2001 From: James Mackay Date: Thu, 8 Oct 2026 09:41:45 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20protocol=20key/launch=20command=20layer?= =?UTF-8?q?=20=E2=80=94=20RemoteKeyCode=20maps,=20C-LAUNCH=20app=20launch,?= =?UTF-8?q?=20ADB=20digit=20keyevent=20fallback,=20PlaybackIntent=20protoc?= =?UTF-8?q?ol=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/nvidia_shield/connection.py | 38 +++++++++++++++++++ custom_components/nvidia_shield/const.py | 38 +++++++++++++++++++ .../nvidia_shield/ha_adb_common/adb_client.py | 10 +++++ .../nvidia_shield/ha_adb_common/intent.py | 7 +++- 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/custom_components/nvidia_shield/connection.py b/custom_components/nvidia_shield/connection.py index 54875f8..f0a5f43 100644 --- a/custom_components/nvidia_shield/connection.py +++ b/custom_components/nvidia_shield/connection.py @@ -403,6 +403,44 @@ class ShieldConnectionManager: return False return await self._poller.bring_to_foreground(package) + # -- Protocol command delegation (Phase 2) -- + + async def send_protocol_key(self, key: str, direction: str = "SHORT") -> bool: + """Send a remote-protocol key push (fast, non-blocking). + + Returns False when the transport is down or the send raises — entities + then fall back to the ADB keyevent path. + """ + if self._protocol is None: + return False + try: + self._protocol.send_key_command(key, direction) + return True + except Exception as err: + _LOGGER.debug("Protocol key %s failed: %s", key, err) + return False + + async def launch_app(self, app_link_or_app_id: str) -> bool: + """Launch an app or URL over the protocol transport. + + ``send_launch_app_command`` auto-prefixes ``market://launch?id=`` for + bare package ids (C-LAUNCH); deep links pass through unchanged. + """ + if self._protocol is None: + return False + try: + self._protocol.send_launch_app_command(app_link_or_app_id) + return True + except Exception as err: + _LOGGER.debug("Protocol launch %s failed: %s", app_link_or_app_id, err) + return False + + async def send_digit_key(self, digit: str) -> bool: + """Channel digit via the ADB keyevent path (fallback for §7).""" + if not self.available: + return False + return await self._poller.send_digit_keyevent(digit) + # ── Lifecycle ── async def async_start(self) -> None: diff --git a/custom_components/nvidia_shield/const.py b/custom_components/nvidia_shield/const.py index 44599c0..e2d270e 100644 --- a/custom_components/nvidia_shield/const.py +++ b/custom_components/nvidia_shield/const.py @@ -54,6 +54,44 @@ JELLYFIN_APP_IDS = [ "com.github.damontecres.wholphin", ] +# Remote-protocol key names (RemoteKeyCode enum in remotemessage.proto). +# HA service kind → protocol keycode string. +PROTOCOL_MEDIA_KEYS = { + "media_play": "KEYCODE_MEDIA_PLAY", + "media_pause": "KEYCODE_MEDIA_PAUSE", + "media_stop": "KEYCODE_MEDIA_STOP", + "media_next_track": "KEYCODE_MEDIA_NEXT", + "media_previous_track": "KEYCODE_MEDIA_PREVIOUS", +} +PROTOCOL_VOLUME_KEYS = { + "volume_up": "KEYCODE_VOLUME_UP", + "volume_down": "KEYCODE_VOLUME_DOWN", + "volume_mute": "KEYCODE_VOLUME_MUTE", +} +PROTOCOL_REMOTE_KEYS = { + **PROTOCOL_VOLUME_KEYS, + "play": "KEYCODE_MEDIA_PLAY", + "pause": "KEYCODE_MEDIA_PAUSE", + "stop": "KEYCODE_MEDIA_STOP", + "up": "KEYCODE_DPAD_UP", + "down": "KEYCODE_DPAD_DOWN", + "left": "KEYCODE_DPAD_LEFT", + "right": "KEYCODE_DPAD_RIGHT", + "enter": "KEYCODE_DPAD_CENTER", + "home": "KEYCODE_HOME", + "back": "KEYCODE_BACK", + "menu": "KEYCODE_MENU", + "search": "KEYCODE_SEARCH", + "power": "KEYCODE_POWER", + "channel_up": "KEYCODE_CHANNEL_UP", + "channel_down": "KEYCODE_CHANNEL_DOWN", +} + +# §7 channel selection: digits only, ~0.1 s apart, no ENTER by default +# (replicates HA core androidtv_remote's verified behavior). +CHANNEL_DIGIT_DELAY = 0.1 +CONF_CHANNEL_SEND_ENTER = "channel_send_enter" + # Verified on-device package names (docs/RESEARCH_SHIELD.md): TiviMate does # not publish a MediaSession on Shield Android 11 — it is the headline # Phase-3 fallback app. diff --git a/custom_components/nvidia_shield/ha_adb_common/adb_client.py b/custom_components/nvidia_shield/ha_adb_common/adb_client.py index 6387f59..aafb8b4 100644 --- a/custom_components/nvidia_shield/ha_adb_common/adb_client.py +++ b/custom_components/nvidia_shield/ha_adb_common/adb_client.py @@ -164,6 +164,16 @@ class AdbClientBase: return True return False + async def send_digit_keyevent(self, digit: str) -> bool: + """Send a channel digit via `input keyevent` (KEYCODE_0..9 = 7..16).""" + if not digit.isdigit() or len(digit) != 1: + _LOGGER.warning("Not a single digit: %s", digit) + return False + result = await self.shell(f"input keyevent {7 + int(digit)}") + if result is not None: + _LOGGER.debug("ADB digit keyevent %d (%s)", 7 + int(digit), digit) + return result is not None + async def bring_to_foreground(self, package: str) -> bool: """Bring an app to the foreground using monkey command.""" result = await self.shell( diff --git a/custom_components/nvidia_shield/ha_adb_common/intent.py b/custom_components/nvidia_shield/ha_adb_common/intent.py index 59d7633..ef0d6e7 100644 --- a/custom_components/nvidia_shield/ha_adb_common/intent.py +++ b/custom_components/nvidia_shield/ha_adb_common/intent.py @@ -22,8 +22,11 @@ class PlaybackIntent: the remote that did not serve it. adb_command: The ADB keycode command name matching the intent (play/pause/stop/next/previous); set together with via_adb. + via_protocol: True when the command was served by the remote-protocol + key push (Phase 2) — the retry then goes over the protocol key + again instead of the other transports. + protocol_key: The protocol keycode string served with via_protocol. """ - target_state: str # MediaPlayerState value (e.g. "playing", "paused") command: str created_at: float @@ -31,6 +34,8 @@ class PlaybackIntent: retry_count: int = 0 via_adb: bool = False adb_command: str | None = None + via_protocol: bool = False + protocol_key: str | None = None @classmethod def create(