diff --git a/custom_components/nvidia_shield/remote.py b/custom_components/nvidia_shield/remote.py index 7f6c9af..cd25a31 100644 --- a/custom_components/nvidia_shield/remote.py +++ b/custom_components/nvidia_shield/remote.py @@ -1,19 +1,176 @@ -"""Remote entities for NVIDIA Shield — Phase 2 step 3 (t10). +"""Remote entities for NVIDIA Shield — protocol-backed (Phase 2, t10). -Remotely controlling the Shield runs over the absorbed remote-protocol -transport (androidtvremote2) managed by ShieldConnectionManager. This -placeholder keeps the Platform.REMOTE forward from failing while t10 -lands the real entity on top. +Port of HA core androidtv_remote/remote.py semantics onto the absorbed +remote-protocol transport (androidtvremote2) owned by ShieldConnectionManager: + +- ``send_command`` accepts SHORT/START_LONG/END_LONG direction prefixes + (``"START_LONG:"``; no prefix = SHORT) and core-style service keys; + the core port's hold_secs+direction validation quirks are not carried over — + long presses are expressed as the START_LONG/END_LONG pair instead; +- ``num_repeats`` / ``delay_secs`` are honored; long-press duration uses the + ``hold_secs`` parameter; +- ``activity`` = the protocol current-app push (package; name resolution is + deferred app-map work); +- turn on/off send the POWER key and honor the is_on push (no superfluous + toggling when the state already matches). """ +import asyncio + +from homeassistant.components.remote import RemoteEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .connection import ShieldConnectionManager +from .const import DOMAIN, PROTOCOL_REMOTE_KEYS + +_START_PREFIX = "START_LONG:" +_END_PREFIX = "END_LONG:" +_SHORT_PREFIX = "SHORT:" +_POWER_KEY = "KEYCODE_POWER" + + +def _split_command(command: str) -> tuple[str, str]: + """Key + direction from a command string (default SHORT).""" + lowered = command.strip() + if lowered.startswith(_START_PREFIX): + return lowered[len(_START_PREFIX):], "START_LONG" + if lowered.startswith(_END_PREFIX): + return lowered[len(_END_PREFIX):], "END_LONG" + if lowered.startswith(_SHORT_PREFIX): + return lowered[len(_SHORT_PREFIX):], "SHORT" + return lowered, "SHORT" + + +def _key_code_name(command: str) -> str: + """Command kind → protocol keycode (raw KEYCODE_ names pass through).""" + mapped = PROTOCOL_REMOTE_KEYS.get(command.lower()) + if mapped is not None: + return mapped + upper = command.upper() + if upper.startswith("KEYCODE_"): + return upper + return upper # unknown names: the library reports/validates them + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Remote entities arrive with the Phase 2 command-routing unit (t10).""" + """Add the protocol-backed remote entity for this entry.""" + async_add_entities([NvidiaShieldRemoteEntity(hass, entry)]) + + +class NvidiaShieldRemoteEntity(RemoteEntity): + """Remote entity over the manager's androidtvremote2 transport.""" + + _attr_has_entity_name = True + _attr_name = None + _attr_should_poll = False + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + self.hass = hass + self._entry = entry + self._manager: ShieldConnectionManager | None = getattr( + entry, "runtime_data", None + ) + self._attr_unique_id = f"{DOMAIN}_{entry.entry_id}_remote" + + def _manager_of(self) -> ShieldConnectionManager | None: + return getattr(self._entry, "runtime_data", None) + + @property + def device_info(self) -> DeviceInfo: + return DeviceInfo( + identifiers={(DOMAIN, self._entry.entry_id)}, + name=self._entry.title, + manufacturer="NVIDIA", + model="Shield TV", + ) + + @property + def available(self) -> bool: + """Unavailable while the transport is active but unpaired/down. + + A passive manager (no protocol transport) keeps the entity available: + nothing to transport is not a broken one. + """ + manager = self._manager_of() + if not isinstance(manager, ShieldConnectionManager): + return True + if manager._protocol is None: + return True + return manager.is_available is not False + + @property + def activity(self) -> str | None: + """Current foreground app reported by the protocol push.""" + manager = self._manager_of() + if isinstance(manager, ShieldConnectionManager): + return manager.current_app + return None + + @callback + def _refresh_state(self) -> None: + """Manager store changed — re-evaluate state (activity/available).""" + self._manager = self._manager_of() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + manager = self._manager_of() + if isinstance(manager, ShieldConnectionManager): + self._manager_unregistered = manager.register_listener( + self._refresh_state + ) + + async def async_will_remove_from_hass(self) -> None: + unregistered = getattr(self, "_manager_unregistered", None) + if unregistered is not None: + unregistered() + self._manager_unregistered = None + + async def async_turn_on(self, **kwargs) -> None: + """Wake the device with the POWER key when it reports off.""" + manager = self._manager_of() + if not isinstance(manager, ShieldConnectionManager): + return + if manager.is_on is not False: + # Unknown counts as "maybe on" — no superfluous toggle. + return + await manager.send_protocol_key(_POWER_KEY, "SHORT") + + async def async_turn_off(self, **kwargs) -> None: + """Sleep the device with the POWER key when it reports on.""" + manager = self._manager_of() + if not isinstance(manager, ShieldConnectionManager): + return + if manager.is_on is not True: + return + await manager.send_protocol_key(_POWER_KEY, "SHORT") + + async def async_send_command( + self, + command: list[str], + num_repeats: int = 1, + delay_secs: float = 0.0, + hold_secs: float = 0.0, + ) -> None: + """Send protocol keys: repeats × (key, or START_LONG/hold/END_LONG).""" + manager = self._manager_of() + if not isinstance(manager, ShieldConnectionManager): + return + for _ in range(max(1, num_repeats)): + for cmd in command: + key_part, direction = _split_command(cmd) + key_name = _key_code_name(key_part) + if hold_secs > 0 and direction == "SHORT": + if await manager.send_protocol_key(key_name, "START_LONG"): + await asyncio.sleep(max(0.0, hold_secs)) + await manager.send_protocol_key(key_name, "END_LONG") + else: + await manager.send_protocol_key(key_name, direction) + if delay_secs > 0: + await asyncio.sleep(delay_secs)