feat:init,config_flow: YAML seed-only setup — an optional nvidia_shield block starts interactive SOURCE_IMPORT flows (parsed in async_setup), the import step guards duplicates by host/MAC-virtual unique_id and never re-seeds UI-configured hosts, protocol fields are prefill hints only (t30 U1)
This commit is contained in:
1 parent
2e70587871
commit
a6eeae86e9
2 files changed
+97
-5
No files matched your search
@@ -13,6 +13,7 @@ passive until the user pairs via the Pair ADB button.
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -21,9 +22,13 @@ from .adb_session import AdbMediaSessionPoller
|
|||||||
from .connection import ShieldConnectionManager
|
from .connection import ShieldConnectionManager
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ADB_ENABLED,
|
CONF_ADB_ENABLED,
|
||||||
|
CONF_ADB_HOST,
|
||||||
|
CONF_ADB_PORT,
|
||||||
CONF_ANDROIDTV_ENTITY,
|
CONF_ANDROIDTV_ENTITY,
|
||||||
CONF_PROTOCOL_ENABLED,
|
CONF_PROTOCOL_ENABLED,
|
||||||
CONF_PROTOCOL_HOST,
|
CONF_PROTOCOL_HOST,
|
||||||
|
DEFAULT_ADB_PORT,
|
||||||
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ADB_COMMAND,
|
SERVICE_ADB_COMMAND,
|
||||||
)
|
)
|
||||||
@@ -35,13 +40,30 @@ PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.REMOTE, Platform.B
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config) -> bool:
|
async def async_setup(hass: HomeAssistant, config) -> bool:
|
||||||
"""Register the integration's services (t29 G8, always-on).
|
"""Domain setup: actions (always-on) + optional YAML seeding (t30 U1).
|
||||||
|
|
||||||
``adb_command`` exists regardless of loaded entries or option state —
|
YAML is SEED-ONLY (user ruling): every ``nvidia_shield`` block starts an
|
||||||
the entry's adb_command_enabled option gates USE (participants self-
|
interactive SOURCE_IMPORT config flow — nothing is created without the
|
||||||
register from the media_player platform), so automations validate and
|
user walking the flow, and pairing stays interactive exactly as for UI
|
||||||
errors are informative instead of 'service not found'.
|
setup. Removing the YAML block creates OR deletes nothing. UI-configured
|
||||||
|
entries are never re-seeded (host/MAC duplicate guards).
|
||||||
"""
|
"""
|
||||||
|
domain_config = (config or {}).get(DOMAIN)
|
||||||
|
for block in _yaml_seed_blocks(domain_config or []):
|
||||||
|
if not block.get(CONF_ADB_HOST) and not block.get(CONF_PROTOCOL_HOST):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"nvidia_shield YAML block without protocol/adb host ignored: %s",
|
||||||
|
block.get("name", "<unnamed>"),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data=block,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# t29 G8 (kept here across the t30 splice): the action registers ALWAYS
|
||||||
from .media_player import _ADB_COMMAND_SCHEMA, _async_adb_command_service
|
from .media_player import _ADB_COMMAND_SCHEMA, _async_adb_command_service
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
@@ -53,6 +75,28 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _yaml_seed_blocks(domain_config) -> list[dict]:
|
||||||
|
"""Normalize the optional YAML domain key into seed blocks (t30 U1)."""
|
||||||
|
blocks: list[dict] = []
|
||||||
|
raw_list = domain_config if isinstance(domain_config, list) else [domain_config]
|
||||||
|
for block in raw_list:
|
||||||
|
if not isinstance(block, dict):
|
||||||
|
continue
|
||||||
|
name = block.get("name") or DEFAULT_NAME
|
||||||
|
seed = {
|
||||||
|
"name": name,
|
||||||
|
CONF_ADB_ENABLED: True,
|
||||||
|
CONF_ADB_HOST: str(block.get(CONF_ADB_HOST) or ""),
|
||||||
|
CONF_ADB_PORT: int(block.get(CONF_ADB_PORT) or DEFAULT_ADB_PORT),
|
||||||
|
# protocol fields are prefill hints ONLY — pairing stays interactive
|
||||||
|
CONF_PROTOCOL_HOST: str(block.get(CONF_PROTOCOL_HOST) or ""),
|
||||||
|
}
|
||||||
|
if block.get("unique_id"):
|
||||||
|
seed["unique_id"] = str(block["unique_id"])
|
||||||
|
blocks.append(seed)
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
async def _build_poller_factory(hass: HomeAssistant, entry: ConfigEntry):
|
async def _build_poller_factory(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Poller factory when ADB is configured AND usable today, else None.
|
"""Poller factory when ADB is configured AND usable today, else None.
|
||||||
|
|
||||||
|
|||||||
@@ -471,6 +471,54 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, data):
|
||||||
|
"""Seed an interactive flow from YAML (t30 U1, seed-only ruling).
|
||||||
|
|
||||||
|
The import validates the block and routes into the standard user
|
||||||
|
flow — pairing stays interactive, exactly one visit. Duplicate
|
||||||
|
guards: the block's host/MAC-virtual unique_id aborts when an
|
||||||
|
entry already holds it, and a UI-configured entry with the same
|
||||||
|
protocol/adb host is never re-seeded.
|
||||||
|
"""
|
||||||
|
block = dict(data or {})
|
||||||
|
name = block.get("name") or DEFAULT_NAME
|
||||||
|
seed_unique_id = block.get("unique_id")
|
||||||
|
if not seed_unique_id:
|
||||||
|
host = block.get(CONF_ADB_HOST) or block.get(CONF_PROTOCOL_HOST)
|
||||||
|
seed_unique_id = f"yaml:{name}:{host or 'nohost'}"
|
||||||
|
|
||||||
|
# duplicate guards
|
||||||
|
yhost = block.get(CONF_ADB_HOST) or block.get(CONF_PROTOCOL_HOST)
|
||||||
|
for existing in self.hass.config_entries.async_entries():
|
||||||
|
ehost = existing.options.get(CONF_PROTOCOL_HOST) or existing.options.get(
|
||||||
|
CONF_ADB_HOST
|
||||||
|
)
|
||||||
|
if existing.unique_id == seed_unique_id or (
|
||||||
|
yhost and ehost == yhost
|
||||||
|
):
|
||||||
|
_LOGGER.info(
|
||||||
|
"nvidia_shield YAML seed for %r matches an existing "
|
||||||
|
"entry — not re-seeded",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
await self.async_set_unique_id(seed_unique_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
# route into the standard user flow (single interactive visit)
|
||||||
|
self._user_data = {
|
||||||
|
"name": name,
|
||||||
|
CONF_ADB_ENABLED: True,
|
||||||
|
CONF_ADB_HOST: block.get(CONF_ADB_HOST, ""),
|
||||||
|
CONF_ADB_PORT: block.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
|
||||||
|
# protocol prefill hints ONLY (pairing stays interactive)
|
||||||
|
CONF_PROTOCOL_HOST: block.get(CONF_PROTOCOL_HOST, ""),
|
||||||
|
}
|
||||||
|
self._discovered_host = self._user_data[CONF_PROTOCOL_HOST] or self._user_data[
|
||||||
|
CONF_ADB_HOST
|
||||||
|
]
|
||||||
|
return await self.async_step_user(None)
|
||||||
|
|
||||||
async def async_step_reauth(self, entry_data):
|
async def async_step_reauth(self, entry_data):
|
||||||
"""Device hard reset invalidated our certificates (review trigger)."""
|
"""Device hard reset invalidated our certificates (review trigger)."""
|
||||||
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
||||||
|
|||||||
Reference in new issue
Block a user