feat(config_flow): the G2 unique_id fix lands — the androidtv entity_id is never a unique_id; MAC derived from the device registry mac connection (empty placeholders skipped), else no unique_id; rename-stability + absence pinned by test (t29 G2)

This commit is contained in:
James Mackay committed 2026-10-09 07:11:03 +01:00
1 parent 086c089cc1
commit e1a0fc3039
5 files changed
+229 -71

No files matched your search

+57 -64
View File
@@ -13,7 +13,6 @@ from .adb_session import ensure_adb_key
from .connection import ShieldConnectionManager
from .const import (
ATTR_MEDIA_DAG_INPUT_MAP,
CHANNEL_DIGIT_DELAY,
CONF_ADB_COMMAND_ENABLED,
CONF_ADB_ENABLED,
CONF_ADB_HOST,
@@ -145,15 +144,15 @@ def _user_schema_dict(sel, defaults: dict | None = None) -> dict:
schema_dict[vol.Optional(CONF_POWER_ENTITY)] = sel["power"]
schema_dict[vol.Optional(CONF_ADB_ENABLED, default=False)] = bool
schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO, default=True)] = bool
schema_dict[vol.Optional(CONF_ADB_ENABLED)] = bool
schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO)] = 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_ADB_PORT)] = int
schema_dict[vol.Optional(CONF_PROTOCOL_ENABLED)] = bool
schema_dict[vol.Optional(CONF_PROTOCOL_HOST)] = str
if "host" in defaults:
schema_dict[vol.Optional(CONF_ADB_HOST, default=defaults["host"])] = str
schema_dict[vol.Optional(CONF_PROTOCOL_HOST, default=defaults["host"])] = str
schema_dict[vol.Optional(CONF_ADB_HOST)] = str
schema_dict[vol.Optional(CONF_PROTOCOL_HOST)] = str
return schema_dict
@@ -515,15 +514,30 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(config_entry):
return NvidiaShieldOptionsFlow(config_entry)
return NvidiaShieldOptionsFlow()
class NvidiaShieldOptionsFlow(config_entries.OptionsFlow):
"""Handle options for Nvidia Shield — single unified page."""
class NvidiaShieldOptionsFlow(config_entries.OptionsFlowWithReload):
"""Handle options for Nvidia Shield — single unified page (t29 G3b).
def __init__(self, config_entry):
self._config_entry = config_entry
self._options = dict(config_entry.options)
The constructor takes NO arguments: HA assigns ``self.config_entry``
on instantiation (the pre-2026 "OptionsFlow(config_entry)" pattern is
deprecated). Suggested values come through
``add_suggested_values_to_schema``, and saving reloads via the
OptionsFlowWithReload base — no listener boomerangs.
"""
def __init__(self):
self._options: dict | None = None
@property
def _options_dict(self) -> dict:
if self._options is None:
if self.config_entry is not None:
self._options = dict(self.config_entry.options)
else:
self._options = {}
return self._options
async def async_step_init(self, user_input=None):
"""Single-page options."""
@@ -542,7 +556,7 @@ class NvidiaShieldOptionsFlow(config_entries.OptionsFlow):
app_map_text = user_input.pop(CONF_APP_NAME_MAP, None)
icons: dict[str, str] = {}
if app_map_text is None:
app_map = self._options.get(CONF_APP_NAME_MAP, {})
app_map = self._options_dict.get(CONF_APP_NAME_MAP, {})
elif app_map_text.strip() == "":
app_map = {}
else:
@@ -551,14 +565,14 @@ class NvidiaShieldOptionsFlow(config_entries.OptionsFlow):
errors["base"] = "invalid_app_map"
if not errors:
if delay is not None:
self._options[CONF_CHANNEL_DIGIT_DELAY] = delay
self._options.update(user_input)
self._options[CONF_APP_NAME_MAP] = app_map
self._options_dict[CONF_CHANNEL_DIGIT_DELAY] = delay
self._options_dict.update(user_input)
self._options_dict[CONF_APP_NAME_MAP] = app_map
# P5-2: the icon column — the editor is the source of
# truth, so a save without icons clears them (and a
# single-column map stays valid).
self._options[CONF_APP_ICON_MAP] = icons
return self.async_create_entry(title="", data=self._options)
self._options_dict[CONF_APP_ICON_MAP] = icons
return self.async_create_entry(title="", data=self._options_dict)
sel = _build_selectors()
schema_dict = {}
@@ -566,46 +580,36 @@ class NvidiaShieldOptionsFlow(config_entries.OptionsFlow):
# Apps name/icon map editor (P4-1, core androidtv_remote options
# pattern, simplified): one package=name[|icon] per line (P5-2 adds
# the optional icon column).
current_map = self._options.get(CONF_APP_NAME_MAP, {})
current_icons = self._options.get(CONF_APP_ICON_MAP, {})
if current_map:
app_map_default = _app_map_to_text(current_map, current_icons)
schema_dict[vol.Optional(CONF_APP_NAME_MAP, default=app_map_default)] = str
else:
schema_dict[vol.Optional(CONF_APP_NAME_MAP)] = str
schema_dict[vol.Optional(CONF_APP_NAME_MAP)] = str
# Jellyfin
jf = self._options.get(CONF_JELLYFIN_ENTITY)
jf = self._options_dict.get(CONF_JELLYFIN_ENTITY)
jf_key = vol.Optional(CONF_JELLYFIN_ENTITY)
if jf:
jf_key = vol.Optional(CONF_JELLYFIN_ENTITY, default=jf)
jf_key = vol.Optional(CONF_JELLYFIN_ENTITY)
schema_dict[jf_key] = sel["media_player"]
# Receiver
recv = self._options.get(CONF_RECEIVER_ENTITY)
recv = self._options_dict.get(CONF_RECEIVER_ENTITY)
recv_key = vol.Optional(CONF_RECEIVER_ENTITY)
if recv:
recv_key = vol.Optional(CONF_RECEIVER_ENTITY, default=recv)
recv_key = vol.Optional(CONF_RECEIVER_ENTITY)
schema_dict[recv_key] = sel["receiver"]
# P5-2: channel digit behavior (the user-validated §7 flow)
current_delay = self._options.get(CONF_CHANNEL_DIGIT_DELAY, CHANNEL_DIGIT_DELAY)
schema_dict[vol.Optional(CONF_CHANNEL_DIGIT_DELAY, default=current_delay)] = str
current_enter = self._options.get(CONF_CHANNEL_SEND_ENTER, False)
schema_dict[vol.Optional(CONF_CHANNEL_SEND_ENTER, default=current_enter)] = bool
schema_dict[vol.Optional(CONF_CHANNEL_DIGIT_DELAY)] = str
schema_dict[vol.Optional(CONF_CHANNEL_SEND_ENTER)] = bool
# P5-3: the live events push is opt-in (default off; the user
# tests stability on their device and reports)
current_logcat = self._options.get(CONF_LOGCAT_EVENTS, False)
schema_dict[vol.Optional(CONF_LOGCAT_EVENTS, default=current_logcat)] = bool
schema_dict[vol.Optional(CONF_LOGCAT_EVENTS)] = bool
# P4-2a: the raw adb_command service is opt-in (default off)
current_flag = self._options.get(CONF_ADB_COMMAND_ENABLED, False)
schema_dict[vol.Optional(CONF_ADB_COMMAND_ENABLED, default=current_flag)] = bool
schema_dict[vol.Optional(CONF_ADB_COMMAND_ENABLED)] = bool
# Receiver input — dropdown from DAG if receiver is set, text otherwise
current_input = self._options.get(CONF_RECEIVER_INPUT, "")
receiver = self._options.get(CONF_RECEIVER_ENTITY)
current_input = self._options_dict.get(CONF_RECEIVER_INPUT, "")
receiver = self._options_dict.get(CONF_RECEIVER_ENTITY)
inputs = _get_receiver_inputs(self.hass, receiver)
if inputs:
@@ -618,43 +622,32 @@ class NvidiaShieldOptionsFlow(config_entries.OptionsFlow):
)
input_key = vol.Optional(CONF_RECEIVER_INPUT)
if current_input:
input_key = vol.Optional(CONF_RECEIVER_INPUT, default=current_input)
input_key = vol.Optional(CONF_RECEIVER_INPUT)
schema_dict[input_key] = input_sel
else:
schema_dict[vol.Optional(
CONF_RECEIVER_INPUT, default=current_input
)] = str
schema_dict[vol.Optional(CONF_RECEIVER_INPUT)] = str
# Power
pwr = self._options.get(CONF_POWER_ENTITY)
pwr = self._options_dict.get(CONF_POWER_ENTITY)
pwr_key = vol.Optional(CONF_POWER_ENTITY)
if pwr:
pwr_key = vol.Optional(CONF_POWER_ENTITY, default=pwr)
pwr_key = vol.Optional(CONF_POWER_ENTITY)
schema_dict[pwr_key] = sel["power"]
# ADB options
schema_dict[vol.Optional(
CONF_ADB_ENABLED,
default=self._options.get(CONF_ADB_ENABLED, False),
)] = bool
schema_dict[vol.Optional(
CONF_ADB_STORE_APP_INFO,
default=self._options.get(CONF_ADB_STORE_APP_INFO, True),
)] = bool
schema_dict[vol.Optional(CONF_ADB_ENABLED)] = bool
schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO)] = bool
# ADB host/port (optional overrides, shown when ADB is enabled)
adb_host = self._options.get(CONF_ADB_HOST, "")
schema_dict[vol.Optional(
CONF_ADB_HOST,
default=adb_host,
)] = str
schema_dict[vol.Optional(
CONF_ADB_PORT,
default=self._options.get(CONF_ADB_PORT, DEFAULT_ADB_PORT),
)] = int
schema_dict[vol.Optional(CONF_ADB_HOST)] = str
schema_dict[vol.Optional(CONF_ADB_PORT)] = int
# t29 G3b: suggested values come from the entry options via the
# documented helper — no per-key `default=` in the schema build.
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(schema_dict),
data_schema=self.add_suggested_values_to_schema(
vol.Schema(schema_dict), self._options_dict
),
errors=errors,
)