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"
|
||||
|
||||
|
||||
Reference in new issue
Block a user