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."""
|
"""Config flow for Nvidia Shield integration."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -18,14 +19,17 @@ from .const import (
|
|||||||
CONF_ADB_STORE_APP_INFO,
|
CONF_ADB_STORE_APP_INFO,
|
||||||
CONF_ANDROIDTV_ENTITY,
|
CONF_ANDROIDTV_ENTITY,
|
||||||
CONF_JELLYFIN_ENTITY,
|
CONF_JELLYFIN_ENTITY,
|
||||||
|
CONF_PAIRING_CODE,
|
||||||
CONF_POWER_ENTITY,
|
CONF_POWER_ENTITY,
|
||||||
|
CONF_PROTOCOL_ENABLED,
|
||||||
|
CONF_PROTOCOL_HOST,
|
||||||
CONF_RECEIVER_ENTITY,
|
CONF_RECEIVER_ENTITY,
|
||||||
CONF_RECEIVER_INPUT,
|
CONF_RECEIVER_INPUT,
|
||||||
DEFAULT_ADB_PORT,
|
DEFAULT_ADB_PORT,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .helpers import discover_adb_host
|
from .helpers import discover_adb_host, protocol_cert_paths
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -96,6 +100,8 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._user_data = {}
|
self._user_data = {}
|
||||||
|
self._protocol_client = None
|
||||||
|
self._reauth_entry = None
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Step 1: Basic setup."""
|
"""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_STORE_APP_INFO: user_input.get(CONF_ADB_STORE_APP_INFO, True),
|
||||||
CONF_ADB_HOST: user_input.get(CONF_ADB_HOST, ""),
|
CONF_ADB_HOST: user_input.get(CONF_ADB_HOST, ""),
|
||||||
CONF_ADB_PORT: user_input.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
|
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 ADB is enabled, go to pairing step
|
||||||
if self._user_data[CONF_ADB_ENABLED]:
|
if self._user_data[CONF_ADB_ENABLED]:
|
||||||
return await self.async_step_adb_pair()
|
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()
|
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_STORE_APP_INFO, default=True)] = bool
|
||||||
schema_dict[vol.Optional(CONF_ADB_HOST)] = str
|
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_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(
|
return self.async_show_form(
|
||||||
step_id="user",
|
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_STORE_APP_INFO: d.get(CONF_ADB_STORE_APP_INFO, True),
|
||||||
CONF_ADB_HOST: d.get(CONF_ADB_HOST, ""),
|
CONF_ADB_HOST: d.get(CONF_ADB_HOST, ""),
|
||||||
CONF_ADB_PORT: d.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
|
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:
|
def _resolve_adb_host(self) -> str | None:
|
||||||
"""Resolve ADB host from explicit config or the Android TV entity."""
|
"""Resolve ADB host from explicit config or the Android TV entity."""
|
||||||
return self._user_data.get(CONF_ADB_HOST) or discover_adb_host(
|
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
|
host, port, key_path
|
||||||
)
|
)
|
||||||
if connected:
|
if connected:
|
||||||
return self._create_entry()
|
return await self._async_after_pairing()
|
||||||
else:
|
else:
|
||||||
errors["base"] = "adb_failed"
|
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")
|
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(
|
def discover_adb_host(
|
||||||
hass: HomeAssistant, androidtv_entity_id: str | None
|
hass: HomeAssistant, androidtv_entity_id: str | None
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
"adb_enabled": "Enable ADB Media Session",
|
"adb_enabled": "Enable ADB Media Session",
|
||||||
"adb_store_app_info": "Show App Media Info",
|
"adb_store_app_info": "Show App Media Info",
|
||||||
"adb_host": "ADB Host",
|
"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": {
|
"data_description": {
|
||||||
"androidtv_entity": "The media_player entity from the Android TV Remote integration.",
|
"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_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_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_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": {
|
"adb_pair": {
|
||||||
"title": "Pair ADB",
|
"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."
|
"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": {
|
"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_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": {
|
"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": {
|
"options": {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
"adb_enabled": "Enable ADB Media Session",
|
"adb_enabled": "Enable ADB Media Session",
|
||||||
"adb_store_app_info": "Show App Media Info",
|
"adb_store_app_info": "Show App Media Info",
|
||||||
"adb_host": "ADB Host",
|
"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": {
|
"data_description": {
|
||||||
"androidtv_entity": "The media_player entity from the Android TV Remote integration.",
|
"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_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_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_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": {
|
"adb_pair": {
|
||||||
"title": "Pair ADB",
|
"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."
|
"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": {
|
"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_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": {
|
"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": {
|
"options": {
|
||||||
|
|||||||
Reference in new issue
Block a user