feat(config_flow): remote-protocol cert/PIN pairing step + reauth after device hard reset (shared .storage cert paths; ADB step body byte-intact, routes onward when protocol enabled)
This commit is contained in:
1 parent
e37a3be75f
commit
b5fc0dacf4
4 files changed
+193
-10
No files matched your search
@@ -1,5 +1,6 @@
|
||||
"""Config flow for Nvidia Shield integration."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -18,14 +19,17 @@ from .const import (
|
||||
CONF_ADB_STORE_APP_INFO,
|
||||
CONF_ANDROIDTV_ENTITY,
|
||||
CONF_JELLYFIN_ENTITY,
|
||||
CONF_PAIRING_CODE,
|
||||
CONF_POWER_ENTITY,
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
CONF_PROTOCOL_HOST,
|
||||
CONF_RECEIVER_ENTITY,
|
||||
CONF_RECEIVER_INPUT,
|
||||
DEFAULT_ADB_PORT,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from .helpers import discover_adb_host
|
||||
from .helpers import discover_adb_host, protocol_cert_paths
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -96,6 +100,8 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self):
|
||||
self._user_data = {}
|
||||
self._protocol_client = None
|
||||
self._reauth_entry = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Step 1: Basic setup."""
|
||||
@@ -134,12 +140,17 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_ADB_STORE_APP_INFO: user_input.get(CONF_ADB_STORE_APP_INFO, True),
|
||||
CONF_ADB_HOST: user_input.get(CONF_ADB_HOST, ""),
|
||||
CONF_ADB_PORT: user_input.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
|
||||
CONF_PROTOCOL_ENABLED: user_input.get(CONF_PROTOCOL_ENABLED, False),
|
||||
CONF_PROTOCOL_HOST: user_input.get(CONF_PROTOCOL_HOST, ""),
|
||||
}
|
||||
|
||||
# If ADB is enabled, go to pairing step
|
||||
if self._user_data[CONF_ADB_ENABLED]:
|
||||
return await self.async_step_adb_pair()
|
||||
|
||||
if self._user_data[CONF_PROTOCOL_ENABLED]:
|
||||
return await self.async_step_protocol_pair()
|
||||
|
||||
return self._create_entry()
|
||||
|
||||
|
||||
@@ -162,6 +173,8 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO, default=True)] = bool
|
||||
schema_dict[vol.Optional(CONF_ADB_HOST)] = str
|
||||
schema_dict[vol.Optional(CONF_ADB_PORT, default=DEFAULT_ADB_PORT)] = int
|
||||
schema_dict[vol.Optional(CONF_PROTOCOL_ENABLED, default=False)] = bool
|
||||
schema_dict[vol.Optional(CONF_PROTOCOL_HOST)] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -184,9 +197,126 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_ADB_STORE_APP_INFO: d.get(CONF_ADB_STORE_APP_INFO, True),
|
||||
CONF_ADB_HOST: d.get(CONF_ADB_HOST, ""),
|
||||
CONF_ADB_PORT: d.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
|
||||
CONF_PROTOCOL_ENABLED: d.get(CONF_PROTOCOL_ENABLED, False),
|
||||
CONF_PROTOCOL_HOST: d.get(CONF_PROTOCOL_HOST, ""),
|
||||
},
|
||||
)
|
||||
|
||||
async def _async_after_pairing(self):
|
||||
"""Route after the pairing steps: protocol next, else create the entry."""
|
||||
if self._user_data.get(CONF_PROTOCOL_ENABLED):
|
||||
return await self.async_step_protocol_pair()
|
||||
return self._create_entry()
|
||||
|
||||
def _protocol_host(self) -> str | None:
|
||||
"""Protocol host: explicit override, else the ADB host fallback."""
|
||||
return self._user_data.get(CONF_PROTOCOL_HOST) or self._resolve_adb_host()
|
||||
|
||||
async def _start_protocol_pairing(self):
|
||||
"""Build a throwaway protocol client and make the Shield show its PIN.
|
||||
|
||||
Certificates live under ``.storage/<domain>/`` (core androidtv_remote
|
||||
helpers.py pattern) and are shared between pairing and runtime.
|
||||
"""
|
||||
host = self._protocol_host()
|
||||
if not host:
|
||||
return None
|
||||
certfile, keyfile = await self.hass.async_add_executor_job(
|
||||
protocol_cert_paths, self.hass
|
||||
)
|
||||
client = ShieldConnectionManager.build_protocol_client(
|
||||
host,
|
||||
client_name=self._user_data.get("name", DEFAULT_NAME) or DEFAULT_NAME,
|
||||
certfile=certfile,
|
||||
keyfile=keyfile,
|
||||
)
|
||||
await client.async_generate_cert_if_missing()
|
||||
await client.async_start_pairing()
|
||||
return client
|
||||
|
||||
async def async_step_protocol_pair(self, user_input=None):
|
||||
"""Pair with the remote protocol — the PIN is displayed on the Shield."""
|
||||
errors = {}
|
||||
if user_input is None:
|
||||
# First visit: proactively start pairing so the PIN shows on the
|
||||
# Shield while the form is open.
|
||||
client = await self._start_protocol_pairing()
|
||||
if client is None:
|
||||
errors["base"] = "protocol_no_host"
|
||||
else:
|
||||
self._protocol_client = client
|
||||
else:
|
||||
client = self._protocol_client
|
||||
code = user_input.get(CONF_PAIRING_CODE, "")
|
||||
if client is None or not code:
|
||||
errors["base"] = "protocol_invalid_code"
|
||||
else:
|
||||
try:
|
||||
await client.async_finish_pairing(code)
|
||||
except Exception:
|
||||
errors["base"] = "protocol_pair_failed"
|
||||
else:
|
||||
try:
|
||||
_name, mac = await client.async_get_name_and_mac()
|
||||
except Exception:
|
||||
mac = None
|
||||
with contextlib.suppress(Exception):
|
||||
client.disconnect()
|
||||
self._protocol_client = None
|
||||
if mac:
|
||||
await self.async_set_unique_id(mac)
|
||||
self._abort_if_unique_id_configured()
|
||||
if self._reauth_entry is not None:
|
||||
return self._finish_reauth()
|
||||
return self._create_entry()
|
||||
return self.async_show_form(
|
||||
step_id="protocol_pair",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_PAIRING_CODE): str,
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, entry_data):
|
||||
"""Device hard reset invalidated our certificates (review trigger)."""
|
||||
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context.get("entry_id")
|
||||
)
|
||||
if self._reauth_entry is None:
|
||||
return self.async_abort(reason="reauth_entry_missing")
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", data_schema=vol.Schema({})
|
||||
)
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input=None):
|
||||
"""Re-pair after a device hard reset, then reload the entry."""
|
||||
if user_input is not None:
|
||||
entry = self._reauth_entry
|
||||
self._user_data = {
|
||||
"name": entry.title,
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: entry.options.get(CONF_PROTOCOL_HOST, ""),
|
||||
}
|
||||
return await self.async_step_protocol_pair() # first visit re-pairs
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", data_schema=vol.Schema({})
|
||||
)
|
||||
|
||||
def _finish_reauth(self):
|
||||
"""Reauth pairing done: update options, schedule the reload, abort."""
|
||||
entry = self._reauth_entry
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={
|
||||
**entry.options,
|
||||
CONF_PROTOCOL_ENABLED: True,
|
||||
CONF_PROTOCOL_HOST: self._user_data.get(CONF_PROTOCOL_HOST, ""),
|
||||
},
|
||||
)
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
self._reauth_entry = None
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
def _resolve_adb_host(self) -> str | None:
|
||||
"""Resolve ADB host from explicit config or the Android TV entity."""
|
||||
return self._user_data.get(CONF_ADB_HOST) or discover_adb_host(
|
||||
@@ -212,7 +342,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
host, port, key_path
|
||||
)
|
||||
if connected:
|
||||
return self._create_entry()
|
||||
return await self._async_after_pairing()
|
||||
else:
|
||||
errors["base"] = "adb_failed"
|
||||
|
||||
|
||||
@@ -29,6 +29,19 @@ def adb_key_path(hass: HomeAssistant) -> str:
|
||||
return os.path.join(hass.config.config_dir, ".storage", DOMAIN, "adbkey")
|
||||
|
||||
|
||||
def protocol_cert_paths(hass: HomeAssistant) -> tuple[str, str]:
|
||||
"""Local (certfile, keyfile) paths for the remote protocol client.
|
||||
|
||||
Mirrors core androidtv_remote's storage pattern: private material under
|
||||
``.storage/<domain>/``. Shared between config-flow pairing and runtime —
|
||||
an entry aborts duplicate setup by unique_id (= device MAC), so one pair
|
||||
of files suffices per install.
|
||||
"""
|
||||
base = os.path.join(hass.config.config_dir, ".storage", DOMAIN)
|
||||
os.makedirs(base, exist_ok=True)
|
||||
return os.path.join(base, "client.pem"), os.path.join(base, "client.key")
|
||||
|
||||
|
||||
def discover_adb_host(
|
||||
hass: HomeAssistant, androidtv_entity_id: str | None
|
||||
) -> str | None:
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"adb_enabled": "Enable ADB Media Session",
|
||||
"adb_store_app_info": "Show App Media Info",
|
||||
"adb_host": "ADB Host",
|
||||
"adb_port": "ADB Port"
|
||||
"adb_port": "ADB Port",
|
||||
"protocol_enabled": "Pair with Nvidia Remote Protocol",
|
||||
"protocol_host": "Protocol Host"
|
||||
},
|
||||
"data_description": {
|
||||
"androidtv_entity": "The media_player entity from the Android TV Remote integration.",
|
||||
@@ -25,20 +27,38 @@
|
||||
"adb_enabled": "Use ADB to read what's playing from any app (NewPipe, Spotify, etc.). Requires ADB debugging enabled on the Shield.",
|
||||
"adb_store_app_info": "Show title and artist from non-Jellyfin apps. Disable to only show the app name (privacy mode).",
|
||||
"adb_host": "Override the ADB host IP. Leave blank to auto-detect from the Android TV entity.",
|
||||
"adb_port": "ADB port number. Default is 5555."
|
||||
"adb_port": "ADB port number. Default is 5555.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power/availability/app push and volume readout). Adds a PIN pairing step; the Shield shows the code.",
|
||||
"protocol_host": "Override the protocol host IP. Leave blank to auto-detect (same fallback as the ADB host)."
|
||||
}
|
||||
},
|
||||
"adb_pair": {
|
||||
"title": "Pair ADB",
|
||||
"description": "On the Shield, go to Settings > Developer options and enable Network debugging. Then click Submit. You will see a prompt on the Shield to allow the connection -- approve it. If the connection fails, click Submit to try again."
|
||||
},
|
||||
"protocol_pair": {
|
||||
"title": "Pair Remote Protocol",
|
||||
"description": "The Shield is now showing a pairing code. Enter it here. Click Submit to confirm.",
|
||||
"data": {
|
||||
"pairing_code": "Pairing code shown on the Shield"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Re-pair Nvidia Shield",
|
||||
"description": "The Shield reset to factory settings, so the saved certificate is no longer trusted. A pairing code will be shown on the Shield."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"adb_failed": "Could not connect via ADB. Make sure Network debugging is enabled on the Shield and you approved the connection prompt. Click Submit to try again.",
|
||||
"adb_no_host": "Could not determine the Shield's IP address. Set the ADB Host field on the previous step."
|
||||
"adb_no_host": "Could not determine the Shield's IP address. Set the ADB Host field on the previous step.",
|
||||
"protocol_no_host": "Could not determine the Shield's IP address. Set the Protocol Host or ADB Host field.",
|
||||
"protocol_invalid_code": "Pairing has not started or the code is empty. Click Submit to retry.",
|
||||
"protocol_pair_failed": "The Shield rejected that pairing code. Click Submit to try again."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This Shield entity is already configured."
|
||||
"already_configured": "This Shield entity is already configured.",
|
||||
"reauth_successful": "Re-paired successfully.",
|
||||
"reauth_entry_missing": "This entry no longer exists."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"adb_enabled": "Enable ADB Media Session",
|
||||
"adb_store_app_info": "Show App Media Info",
|
||||
"adb_host": "ADB Host",
|
||||
"adb_port": "ADB Port"
|
||||
"adb_port": "ADB Port",
|
||||
"protocol_enabled": "Pair with Nvidia Remote Protocol",
|
||||
"protocol_host": "Protocol Host"
|
||||
},
|
||||
"data_description": {
|
||||
"androidtv_entity": "The media_player entity from the Android TV Remote integration.",
|
||||
@@ -25,20 +27,38 @@
|
||||
"adb_enabled": "Use ADB to read what's playing from any app (NewPipe, Spotify, etc.). Requires ADB debugging enabled on the Shield.",
|
||||
"adb_store_app_info": "Show title and artist from non-Jellyfin apps. Disable to only show the app name (privacy mode).",
|
||||
"adb_host": "Override the ADB host IP. Leave blank to auto-detect from the Android TV entity.",
|
||||
"adb_port": "ADB port number. Default is 5555."
|
||||
"adb_port": "ADB port number. Default is 5555.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power/availability/app push and volume readout). Adds a PIN pairing step; the Shield shows the code.",
|
||||
"protocol_host": "Override the protocol host IP. Leave blank to auto-detect (same fallback as the ADB host)."
|
||||
}
|
||||
},
|
||||
"adb_pair": {
|
||||
"title": "Pair ADB",
|
||||
"description": "On the Shield, go to Settings > Developer options and enable Network debugging. Then click Submit. You will see a prompt on the Shield to allow the connection -- approve it. If the connection fails, click Submit to try again."
|
||||
},
|
||||
"protocol_pair": {
|
||||
"title": "Pair Remote Protocol",
|
||||
"description": "The Shield is now showing a pairing code. Enter it here. Click Submit to confirm.",
|
||||
"data": {
|
||||
"pairing_code": "Pairing code shown on the Shield"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Re-pair Nvidia Shield",
|
||||
"description": "The Shield reset to factory settings, so the saved certificate is no longer trusted. A pairing code will be shown on the Shield."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"adb_failed": "Could not connect via ADB. Make sure Network debugging is enabled on the Shield and you approved the connection prompt. Click Submit to try again.",
|
||||
"adb_no_host": "Could not determine the Shield's IP address. Set the ADB Host field on the previous step."
|
||||
"adb_no_host": "Could not determine the Shield's IP address. Set the ADB Host field on the previous step.",
|
||||
"protocol_no_host": "Could not determine the Shield's IP address. Set the Protocol Host or ADB Host field.",
|
||||
"protocol_invalid_code": "Pairing has not started or the code is empty. Click Submit to retry.",
|
||||
"protocol_pair_failed": "The Shield rejected that pairing code. Click Submit to try again."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This Shield entity is already configured."
|
||||
"already_configured": "This Shield entity is already configured.",
|
||||
"reauth_successful": "Re-paired successfully.",
|
||||
"reauth_entry_missing": "This entry no longer exists."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
Reference in new issue
Block a user