diff --git a/custom_components/nvidia_shield/button.py b/custom_components/nvidia_shield/button.py index ed9c9d4..620bba0 100644 --- a/custom_components/nvidia_shield/button.py +++ b/custom_components/nvidia_shield/button.py @@ -5,7 +5,6 @@ import logging from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,6 +17,7 @@ from .const import ( DEFAULT_ADB_PORT, DOMAIN, ) +from .helpers import discover_adb_host _LOGGER = logging.getLogger(__name__) @@ -52,29 +52,11 @@ class NvidiaShieldPairAdbButton(ButtonEntity): model="Shield TV", ) - def _resolve_host(self) -> str | None: - """Resolve ADB host from config or Android TV entity.""" - host = self._entry.options.get(CONF_ADB_HOST) - if host: - return host - - atv_entity = self._entry.data.get(CONF_ANDROIDTV_ENTITY) - if not atv_entity: - return None - - registry = er.async_get(self.hass) - entity_entry = registry.async_get(atv_entity) - if not entity_entry or not entity_entry.config_entry_id: - return None - - config_entry = self.hass.config_entries.async_get_entry(entity_entry.config_entry_id) - if config_entry: - return config_entry.data.get("host") - return None - async def async_press(self) -> None: """Attempt ADB connection. The Shield will show a pairing prompt.""" - host = self._resolve_host() + host = self._entry.options.get(CONF_ADB_HOST) or discover_adb_host( + self.hass, self._entry.data.get(CONF_ANDROIDTV_ENTITY) + ) port = self._entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT) if not host: diff --git a/custom_components/nvidia_shield/config_flow.py b/custom_components/nvidia_shield/config_flow.py index 73ec0c1..91c479d 100644 --- a/custom_components/nvidia_shield/config_flow.py +++ b/custom_components/nvidia_shield/config_flow.py @@ -24,6 +24,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, ) +from .helpers import discover_adb_host _LOGGER = logging.getLogger(__name__) @@ -187,23 +188,9 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _resolve_adb_host(self) -> str | None: """Resolve ADB host from explicit config or the Android TV entity.""" - host = self._user_data.get(CONF_ADB_HOST) - if host: - return host - - atv_entity = self._user_data.get(CONF_ANDROIDTV_ENTITY) - if not atv_entity: - return None - - registry = er.async_get(self.hass) - entity_entry = registry.async_get(atv_entity) - if not entity_entry or not entity_entry.config_entry_id: - return None - - config_entry = self.hass.config_entries.async_get_entry(entity_entry.config_entry_id) - if config_entry: - return config_entry.data.get("host") - return None + return self._user_data.get(CONF_ADB_HOST) or discover_adb_host( + self.hass, self._user_data.get(CONF_ANDROIDTV_ENTITY) + ) async def async_step_adb_pair(self, user_input=None): """Step 2: ADB pairing. User clicks Submit after approving on the Shield.""" diff --git a/custom_components/nvidia_shield/helpers.py b/custom_components/nvidia_shield/helpers.py new file mode 100644 index 0000000..b9c630c --- /dev/null +++ b/custom_components/nvidia_shield/helpers.py @@ -0,0 +1,60 @@ +"""Shared config helpers for the NVIDIA Shield integration.""" + +import os +from dataclasses import dataclass + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .const import ( + CONF_ADB_HOST, + CONF_ADB_PORT, + CONF_ANDROIDTV_ENTITY, + DEFAULT_ADB_PORT, + DOMAIN, +) + + +@dataclass +class AdbConfig: + """Resolved ADB connection settings for a config entry.""" + + host: str + port: int + key_path: str + + +def adb_key_path(hass: HomeAssistant) -> str: + """Path of the ADB private key for this integration (may not exist yet).""" + return os.path.join(hass.config.config_dir, ".storage", DOMAIN, "adbkey") + + +def discover_adb_host( + hass: HomeAssistant, androidtv_entity_id: str | None +) -> str | None: + """Best-effort host for an AndroidTV entity, from its owning config entry.""" + if not androidtv_entity_id: + return None + registry = er.async_get(hass) + entity_entry = registry.async_get(androidtv_entity_id) + if not entity_entry or not entity_entry.config_entry_id: + return None + config_entry = hass.config_entries.async_get_entry(entity_entry.config_entry_id) + if config_entry: + return config_entry.data.get("host") + return None + + +def resolve_adb_config(hass: HomeAssistant, entry) -> AdbConfig | None: + """Resolve host/port/key_path for the entry's ADB connection, or None. + + Priority: explicit ADB host/port options > host from the AndroidTV + entity's own config entry. + """ + host = entry.options.get(CONF_ADB_HOST) or discover_adb_host( + hass, entry.data.get(CONF_ANDROIDTV_ENTITY) + ) + if not host: + return None + port = int(entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT)) + return AdbConfig(host=host, port=port, key_path=adb_key_path(hass)) diff --git a/custom_components/nvidia_shield/media_player.py b/custom_components/nvidia_shield/media_player.py index daadf45..0e350b7 100644 --- a/custom_components/nvidia_shield/media_player.py +++ b/custom_components/nvidia_shield/media_player.py @@ -14,6 +14,7 @@ Intent tracking bridges the gap between instant commands and slow Jellyfin polli """ import logging +import os import time from homeassistant.components.media_player import ( @@ -40,18 +41,16 @@ from .const import ( ATTR_MEDIA_DAG_ROLE, ATTR_MEDIA_DAG_RUNNING_APPS, CONF_ADB_ENABLED, - CONF_ADB_HOST, - CONF_ADB_PORT, CONF_ADB_STORE_APP_INFO, CONF_ANDROIDTV_ENTITY, CONF_JELLYFIN_ENTITY, CONF_POWER_ENTITY, CONF_RECEIVER_ENTITY, CONF_RECEIVER_INPUT, - DEFAULT_ADB_PORT, DOMAIN, ) from .ha_adb_common import PlaybackIntent, running_apps_from_tasks +from .helpers import adb_key_path, resolve_adb_config from .jellyfin import fetch_session_media_info from .power import discover_power_siblings @@ -336,60 +335,38 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity): return None return self.hass.states.get(entity_id) - def _discover_adb_host(self) -> str | None: - """Auto-detect ADB host from the androidtv_remote config entry for our entity.""" - from homeassistant.helpers import entity_registry as er - - registry = er.async_get(self.hass) - entity_entry = registry.async_get(self._androidtv_entity) - if not entity_entry or not entity_entry.config_entry_id: - return None - - config_entry = self.hass.config_entries.async_get_entry(entity_entry.config_entry_id) - if config_entry: - return config_entry.data.get("host") - return None - async def _setup_adb(self) -> None: """Initialize ADB connection and start polling.""" try: # Only auto-connect if ADB was previously paired (key exists) - import os - key_dir = os.path.join(self.hass.config.config_dir, ".storage", DOMAIN) - key_file = os.path.join(key_dir, "adbkey") - if not await self.hass.async_add_executor_job(os.path.exists, key_file): + key_path = adb_key_path(self.hass) + if not await self.hass.async_add_executor_job(os.path.exists, key_path): _LOGGER.info( "ADB key not found — skipping auto-connect. " "Use the Pair ADB button or reconfigure the integration." ) return - # Get device IP: explicit config > auto-detect from androidtv_remote - host = self._entry.options.get(CONF_ADB_HOST) - port = self._entry.options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT) - - if not host: - host = self._discover_adb_host() - - if not host: - _LOGGER.warning("Could not determine ADB host. Set it in integration options.") + adb = resolve_adb_config(self.hass, self._entry) + if adb is None: + _LOGGER.warning( + "Could not determine ADB host. Set it in integration options." + ) return - - key_path = key_file - self._adb_poller = AdbMediaSessionPoller(host, port, key_path) + self._adb_poller = AdbMediaSessionPoller(adb.host, adb.port, adb.key_path) if await self._adb_poller.connect(): # Start periodic polling self._adb_poll_unsub = async_call_later( self.hass, ADB_POLL_INTERVAL, self._adb_poll_callback ) - _LOGGER.info("ADB media session polling started for %s", host) + _LOGGER.info("ADB media session polling started for %s", adb.host) else: _LOGGER.warning( "ADB connection to %s failed. If this is the first connection, " "approve the ADB debugging prompt on the device. " "Retrying in %d seconds.", - host, ADB_RETRY_INTERVAL, + adb.host, ADB_RETRY_INTERVAL, ) self._adb_poll_unsub = async_call_later( self.hass, ADB_RETRY_INTERVAL, self._adb_poll_callback